<?php
/**
 * Comfisc — Universal Scheduler (Comptabilité & Fiscalité)
 *
 * One class for ALL languages. Each language gets its own cron hook:
 *   dba_comfisc_{lang}_cron
 *
 * Schedule (days + hour) comes from ADMIN SETTINGS first, then
 * falls back to the language config defaults. This allows the user
 * to change publishing days from the admin panel.
 *
 * posts_per_run: generates N articles per scheduled execution (default 1).
 *
 * @package DamalionBlogAI
 * @subpackage Comfisc
 */

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

class DBA_Comfisc_Scheduler {

    const TIMEZONE = 'Europe/Luxembourg';

    /** @var array All loaded language configs (code => config) */
    private $languages = array();

    private static $instance = null;

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

    private function __construct() {
        // Load all language configs
        $this->languages = DBA_Comfisc_Loader::get_languages();

        add_filter( 'cron_schedules', array( $this, 'register_schedules' ) );

        // Register cron callback for each language
        foreach ( $this->languages as $lang => $config ) {
            $hook = 'dba_comfisc_' . $lang . '_cron';
            add_action( $hook, function() use ( $lang, $config ) {
                $this->run( $lang, $config );
            } );
        }

        // Self-healing catch-up on every page load
        add_action( 'init', array( $this, 'maybe_catch_up_all' ), 100 );

        // Ensure cron registered on admin load
        add_action( 'admin_init', array( $this, 'ensure_all_scheduled' ) );
    }

    /* ──────────────────────────────────────────────────
     *  RESOLVE SCHEDULE — settings override config
     * ────────────────────────────────────────────────── */

    /**
     * Get active publishing days for a language.
     * Priority: admin settings > config defaults.
     *
     * @return array  Numeric day-of-week (1=Mon .. 7=Sun)
     */
    public static function get_posting_days( $lang, $lang_config ) {
        $settings = get_option( 'dba_comfisc_' . $lang . '_settings', array() );

        // Admin-selected days (saved as array of int, e.g. [1,3,5])
        if ( ! empty( $settings['publishing_days'] ) && is_array( $settings['publishing_days'] ) ) {
            return array_map( 'intval', $settings['publishing_days'] );
        }

        // Fall back to config
        return ! empty( $lang_config['schedule']['days'] ) ? $lang_config['schedule']['days'] : array( 1 );
    }

    /**
     * Get number of posts to generate per cron run.
     */
    public static function get_posts_per_run( $lang ) {
        $settings = get_option( 'dba_comfisc_' . $lang . '_settings', array() );
        $n = isset( $settings['posts_per_run'] ) ? absint( $settings['posts_per_run'] ) : 1;
        return max( 1, min( $n, 5 ) ); // Clamp 1–5
    }

    /**
     * Get target hour (from config, not overridable in admin).
     */
    public static function get_hour( $lang_config ) {
        return ! empty( $lang_config['schedule']['hour'] ) ? (int) $lang_config['schedule']['hour'] : 7;
    }

    /* ──────────────────────────────────────────────────
     *  SCHEDULES
     * ────────────────────────────────────────────────── */

    public function register_schedules( $schedules ) {
        if ( ! isset( $schedules['daily'] ) ) {
            $schedules['daily'] = array( 'interval' => 86400, 'display' => 'Once Daily' );
        }
        return $schedules;
    }

    /* ──────────────────────────────────────────────────
     *  ACTIVATE / DEACTIVATE (call for each language)
     * ────────────────────────────────────────────────── */

    public static function activate_language( $lang, $lang_config ) {
        $hook = 'dba_comfisc_' . $lang . '_cron';

        if ( ! isset( wp_get_schedules()['daily'] ) ) {
            add_filter( 'cron_schedules', function( $s ) {
                $s['daily'] = array( 'interval' => 86400, 'display' => 'Once Daily' );
                return $s;
            } );
        }

        if ( ! wp_next_scheduled( $hook ) ) {
            $next = self::next_target_timestamp( $lang, $lang_config );
            wp_schedule_event( $next, 'daily', $hook );
            error_log( 'Comfisc[' . $lang . ']: Cron scheduled — next: ' . gmdate( 'Y-m-d H:i:s', $next ) . ' UTC' );
        }
    }

    public static function deactivate_language( $lang ) {
        wp_clear_scheduled_hook( 'dba_comfisc_' . $lang . '_cron' );
        error_log( 'Comfisc[' . $lang . ']: Cron removed.' );
    }

    /** Activate all loaded languages. */
    public static function activate_all() {
        $languages = DBA_Comfisc_Loader::get_languages();
        foreach ( $languages as $lang => $config ) {
            $settings = get_option( 'dba_comfisc_' . $lang . '_settings', array() );
            if ( ! empty( $settings['enabled'] ) ) {
                self::activate_language( $lang, $config );
            }
        }
    }

    /** Deactivate all loaded languages. */
    public static function deactivate_all() {
        $languages = DBA_Comfisc_Loader::get_languages();
        foreach ( $languages as $lang => $config ) {
            self::deactivate_language( $lang );
        }
    }

    public function ensure_all_scheduled() {
        foreach ( $this->languages as $lang => $config ) {
            $settings = get_option( 'dba_comfisc_' . $lang . '_settings', array() );
            if ( empty( $settings['enabled'] ) ) continue;
            $hook = 'dba_comfisc_' . $lang . '_cron';
            if ( ! wp_next_scheduled( $hook ) ) {
                self::activate_language( $lang, $config );
                error_log( 'Comfisc[' . $lang . ']: Cron missing — rescheduled.' );
            }
        }
    }

    /* ──────────────────────────────────────────────────
     *  NEXT TARGET TIMESTAMP
     * ────────────────────────────────────────────────── */

    private static function next_target_timestamp( $lang, $lang_config ) {
        $days = self::get_posting_days( $lang, $lang_config );
        $hour = self::get_hour( $lang_config );

        try {
            $tz  = new DateTimeZone( self::TIMEZONE );
            $now = new DateTime( 'now', $tz );

            // Check today
            $day_n = (int) $now->format( 'N' );
            if ( in_array( $day_n, $days, true ) ) {
                $today = clone $now;
                $today->setTime( $hour, 0, 0 );
                if ( $today > $now ) return $today->getTimestamp();
            }

            // Find next target day
            $candidate = clone $now;
            for ( $i = 1; $i <= 7; $i++ ) {
                $candidate->modify( '+1 day' );
                if ( in_array( (int) $candidate->format( 'N' ), $days, true ) ) {
                    $candidate->setTime( $hour, 0, 0 );
                    return $candidate->getTimestamp();
                }
            }
        } catch ( \Exception $e ) {}

        return time() + 3600;
    }

    /* ──────────────────────────────────────────────────
     *  SELF-HEALING CATCH-UP (all languages)
     * ────────────────────────────────────────────────── */

    public function maybe_catch_up_all() {
        foreach ( $this->languages as $lang => $config ) {
            $this->maybe_catch_up( $lang, $config );
        }
    }

    private function maybe_catch_up( $lang, $lang_config ) {
        $settings = get_option( 'dba_comfisc_' . $lang . '_settings', array() );
        if ( empty( $settings['enabled'] ) ) return;

        $days = self::get_posting_days( $lang, $lang_config );
        $hour = self::get_hour( $lang_config );

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

        if ( ! in_array( (int) $now->format( 'N' ), $days, true ) ) return;
        if ( (int) $now->format( 'G' ) < $hour ) return;

        $lock_key = 'dba_comfisc_' . $lang . '_ran_' . $now->format( 'Y-m-d' );
        if ( get_transient( $lock_key ) ) return;

        error_log( 'Comfisc[' . $lang . '] catch-up: ' . $now->format( 'l H:i' ) . ' CET — running now.' );
        set_transient( $lock_key, true, DAY_IN_SECONDS );

        // Capture $lang/$config for the closure
        $captured_lang   = $lang;
        $captured_config = $lang_config;

        add_action( 'shutdown', function() use ( $captured_lang, $captured_config ) {
            ignore_user_abort( true );
            if ( function_exists( 'set_time_limit' ) ) @set_time_limit( 600 );

            // v2.15.0: Global conflict lock
            $hook = 'dba_comfisc_' . $captured_lang . '_cron';
            if ( class_exists( 'DBA_Conflict_Center' ) && ! DBA_Conflict_Center::acquire( 'comfisc_' . $captured_lang, $hook ) ) {
                try {
                    $tz  = new DateTimeZone( 'Europe/Luxembourg' );
                    $now = new DateTime( 'now', $tz );
                    delete_transient( 'dba_comfisc_' . $captured_lang . '_ran_' . $now->format( 'Y-m-d' ) );
                } catch ( \Exception $e ) {}
                error_log( 'Comfisc[' . $captured_lang . '] catch-up: Another module running — will retry' );
                return;
            }

            // Multi-post: generate N posts per run
            $posts_per_run = DBA_Comfisc_Scheduler::get_posts_per_run( $captured_lang );
            $results = array();

            for ( $i = 0; $i < $posts_per_run; $i++ ) {
                $result = DBA_Comfisc_Generator::run( $captured_lang, $captured_config );

                if ( is_wp_error( $result ) ) {
                    error_log( 'Comfisc[' . $captured_lang . '] catch-up post ' . ( $i + 1 ) . '/' . $posts_per_run . ' error: ' . $result->get_error_message() );
                    break; // Stop on first error
                } else {
                    error_log( 'Comfisc[' . $captured_lang . '] catch-up post ' . ( $i + 1 ) . '/' . $posts_per_run . ' success: Post #' . $result['post_id'] );
                    $results[] = $result;
                }

                // Breathing room between posts (avoid API rate limits)
                if ( $i < $posts_per_run - 1 ) sleep( 5 );
            }

            if ( empty( $results ) ) {
                try {
                    $tz  = new DateTimeZone( 'Europe/Luxembourg' );
                    $now = new DateTime( 'now', $tz );
                    delete_transient( 'dba_comfisc_' . $captured_lang . '_ran_' . $now->format( 'Y-m-d' ) );
                } catch ( \Exception $e ) {}
            }

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

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

    public function run( $lang, $lang_config ) {
        $settings = get_option( 'dba_comfisc_' . $lang . '_settings', array() );
        if ( empty( $settings['enabled'] ) ) return;

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

        // Use admin-selected days (settings override config)
        $days = self::get_posting_days( $lang, $lang_config );
        if ( ! in_array( (int) $now->format( 'N' ), $days, true ) ) {
            error_log( 'Comfisc[' . $lang . ']: ' . $now->format( 'l' ) . ' is not a target day. Skipping.' );
            return;
        }

        $lock_key = 'dba_comfisc_' . $lang . '_ran_' . $now->format( 'Y-m-d' );
        if ( get_transient( $lock_key ) ) return;
        set_transient( $lock_key, true, DAY_IN_SECONDS );

        // v2.15.0: Global conflict lock — prevents overlap with ALL modules
        $hook = 'dba_comfisc_' . $lang . '_cron';
        if ( class_exists( 'DBA_Conflict_Center' ) && ! DBA_Conflict_Center::acquire( 'comfisc_' . $lang, $hook ) ) {
            delete_transient( $lock_key ); // Let catch-up retry
            error_log( 'Comfisc[' . $lang . ']: Another module running — rescheduled +10min' );
            return;
        }

        // Multi-post: generate N posts per run
        $posts_per_run = self::get_posts_per_run( $lang );
        $results = array();

        for ( $i = 0; $i < $posts_per_run; $i++ ) {
            $result = DBA_Comfisc_Generator::run( $lang, $lang_config );

            if ( is_wp_error( $result ) ) {
                error_log( 'Comfisc[' . $lang . ']: Post ' . ( $i + 1 ) . '/' . $posts_per_run . ' Error — ' . $result->get_error_message() );
                if ( $i === 0 ) delete_transient( $lock_key ); // Retry if first post fails
                break;
            } else {
                error_log( 'Comfisc[' . $lang . ']: Post ' . ( $i + 1 ) . '/' . $posts_per_run . ' Success — Post #' . $result['post_id'] . ' (' . $result['words'] . ' words)' );
                $results[] = $result;
            }

            // Breathing room between posts
            if ( $i < $posts_per_run - 1 ) sleep( 5 );
        }

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

    /* ──────────────────────────────────────────────────
     *  MANUAL RUN
     * ────────────────────────────────────────────────── */

    public function manual_run( $lang ) {
        $config = isset( $this->languages[ $lang ] ) ? $this->languages[ $lang ] : null;
        if ( ! $config ) {
            return new WP_Error( 'unknown_lang', 'Language "' . $lang . '" not found.' );
        }

        $settings = get_option( 'dba_comfisc_' . $lang . '_settings', array() );
        if ( empty( $settings['enabled'] ) ) {
            return new WP_Error( 'disabled', 'Comfisc ' . $lang . ' is disabled.' );
        }

        return DBA_Comfisc_Generator::run( $lang, $config, true );
    }

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

    public static function get_schedule_info( $lang, $lang_config ) {
        $days = self::get_posting_days( $lang, $lang_config );
        $hour = self::get_hour( $lang_config );
        $ppr  = self::get_posts_per_run( $lang );

        $day_names = array( 1 => 'Lundi', 2 => 'Mardi', 3 => 'Mercredi', 4 => 'Jeudi', 5 => 'Vendredi', 6 => 'Samedi', 7 => 'Dimanche' );
        $day_labels = array_map( function( $d ) use ( $day_names ) { return $day_names[ $d ] ?? '?'; }, $days );

        try {
            $tz  = new DateTimeZone( self::TIMEZONE );
            $now = new DateTime( 'now', $tz );
        } catch ( \Exception $e ) {
            return array( 'next_run' => 'Error', 'frequency' => implode( ' & ', $day_labels ) . ' at ' . $hour . ':00', 'cron_active' => false, 'posts_per_run' => $ppr );
        }

        $hook = 'dba_comfisc_' . $lang . '_cron';
        $ts   = wp_next_scheduled( $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 = '⚠️ NON PROGRAMMÉ — visitez l\'admin pour corriger';
        }

        return array(
            'next_run'      => $next_run,
            'timezone'      => self::TIMEZONE,
            'frequency'     => implode( ', ', $day_labels ) . ' à ' . sprintf( '%02d:00', $hour ) . ' (auto-réparation)',
            'cron_active'   => (bool) $ts,
            'lock_active'   => (bool) get_transient( 'dba_comfisc_' . $lang . '_ran_' . $now->format( 'Y-m-d' ) ),
            'posts_per_run' => $ppr,
        );
    }
}
