<?php

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

class Niwa_AI_Admin_Autonomy_Scheduler {
    const TABLE_SLUG = 'niwa_ai_admin_scheduled_jobs';
    const MAX_ACTIONS_PER_JOB = 50;
    const TARGETED_JOB_HOOK = 'niwa_ai_admin_autonomy_process_scheduled_job';
    const TARGETED_JOB_FALLBACK_DELAY = 60;
    const TARGETED_JOB_RUNNER_GRACE = 30;
    const JOB_EXECUTION_LOCK_TTL = 600;
    const JOB_CREATION_LOCK_TTL = 30;
    const SCHEDULED_ACTOR_RETRY_DELAY = 600;
    const DEFAULT_RESEARCH_DELIVERY_PREWAKE_MINUTES = 5;

    private $admin_tools;
    private $workspace;
    private $telegram;
    private $store;
    private $conversation_store;
    private $callbacks;

    public function __construct( $admin_tools = null, $workspace = null, $telegram = null, $store = null, $conversation_store = null, $callbacks = array() ) {
        $this->admin_tools        = $admin_tools;
        $this->workspace          = $workspace;
        $this->telegram           = $telegram;
        $this->store              = $store;
        $this->conversation_store = $conversation_store;
        $this->callbacks          = is_array( $callbacks ) ? $callbacks : array();
    }

    public function set_callbacks( $callbacks ) {
        $this->callbacks = is_array( $callbacks ) ? $callbacks : array();
    }

    public function maybe_ensure_table_schema() {
        global $wpdb;

        $table_name = $this->get_table_name();
        $existing   = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) );

        if ( $table_name !== $existing ) {
            $this->ensure_table_schema();
        }
    }

    public function ensure_table_schema() {
        global $wpdb;

        $table_name      = $this->get_table_name();
        $charset_collate = $wpdb->get_charset_collate();

        require_once ABSPATH . 'wp-admin/includes/upgrade.php';

        $sql = "CREATE TABLE {$table_name} (
            id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
            job_uuid varchar(64) NOT NULL,
            job_type varchar(32) NOT NULL,
            title varchar(255) NOT NULL,
            prompt_text longtext NULL,
            chat_id varchar(64) NOT NULL DEFAULT '',
            session_id varchar(128) NOT NULL DEFAULT '',
            schedule_kind varchar(16) NOT NULL DEFAULT 'once',
            timezone varchar(64) NOT NULL DEFAULT 'UTC',
            schedule_json longtext NULL,
            payload_json longtext NULL,
            context_json longtext NULL,
            status varchar(20) NOT NULL DEFAULT 'active',
            next_run_at datetime NULL,
            last_run_at datetime NULL,
            last_success_at datetime NULL,
            completed_at datetime NULL,
            run_count int(11) NOT NULL DEFAULT 0,
            last_error longtext NULL,
            created_at datetime NOT NULL,
            updated_at datetime NOT NULL,
            PRIMARY KEY  (id),
            UNIQUE KEY job_uuid (job_uuid),
            KEY status_next_run (status, next_run_at),
            KEY chat_id (chat_id),
            KEY created_at (created_at)
        ) {$charset_collate};";

        dbDelta( $sql );
    }

    public function get_table_name() {
        global $wpdb;
        return $wpdb->prefix . self::TABLE_SLUG;
    }

    public function create_reminder_job( $args, $context = array() ) {
        $args    = is_array( $args ) ? $args : array();
        $context = is_array( $context ) ? $context : array();
        $message = isset( $args['message'] ) ? trim( (string) $args['message'] ) : '';

        if ( '' === $message ) {
            return array(
                'success' => false,
                'error'   => 'Reminder message je obavezan.',
            );
        }

        $schedule = $this->build_schedule_payload( $args );
        if ( empty( $schedule['success'] ) ) {
            return $schedule;
        }

        return $this->insert_job(
            'reminder',
            isset( $args['title'] ) ? (string) $args['title'] : 'Podsetnik',
            isset( $args['prompt_text'] ) ? (string) $args['prompt_text'] : $message,
            $schedule,
            array(
                'message' => $message,
            ),
            $context
        );
    }

    public function create_admin_plan_job( $args, $context = array(), $prompt = '' ) {
        $args    = is_array( $args ) ? $args : array();
        $context = is_array( $context ) ? $context : array();
        $actions = isset( $args['actions'] ) && is_array( $args['actions'] ) ? array_values( $args['actions'] ) : array();

        if ( empty( $actions ) ) {
            return array(
                'success' => false,
                'error'   => 'Zakazana izmena mora da sadrzi makar jednu konkretnu akciju.',
            );
        }

        if ( count( $actions ) > self::MAX_ACTIONS_PER_JOB ) {
            return array(
                'success' => false,
                'error'   => 'Previse akcija za jedan zakazani posao.',
            );
        }

        if ( ! $this->admin_tools || ! method_exists( $this->admin_tools, 'preview_tool_call' ) ) {
            return array(
                'success' => false,
                'error'   => 'Admin action layer nije ucitana.',
            );
        }

        $schedule = $this->build_schedule_payload( $args );
        if ( empty( $schedule['success'] ) ) {
            return $schedule;
        }
		if ( 'once' !== (string) $schedule['schedule']['kind'] ) {
			foreach ( $actions as $action_spec ) {
				$tool_name = is_array( $action_spec ) && isset( $action_spec['tool_name'] ) ? trim( (string) $action_spec['tool_name'] ) : '';
				if ( 'publish_blog_post' === $tool_name ) {
					return array(
						'success'    => false,
						'error'      => 'A fixed blog post may only be scheduled for one approved revision. Use separate once jobs for concrete post IDs.',
						'error_code' => 'recurring_fixed_blog_publish_forbidden',
					);
				}
			}
		}
        $frozen_actions = array();
        $warnings       = array();
        $summaries      = array();
        $job_prompt     = '' !== trim( (string) $prompt ) ? (string) $prompt : ( isset( $args['prompt_text'] ) ? (string) $args['prompt_text'] : '' );

        foreach ( $actions as $action_spec ) {
            if ( ! is_array( $action_spec ) ) {
                return array(
                    'success' => false,
                    'error'   => 'Svaka zakazana akcija mora biti objekat sa tool_name i args.',
                );
            }

            $tool_name = isset( $action_spec['tool_name'] ) ? trim( (string) $action_spec['tool_name'] ) : '';
            $tool_args = isset( $action_spec['args'] ) && is_array( $action_spec['args'] ) ? $action_spec['args'] : array();

            if ( '' === $tool_name ) {
                return array(
                    'success' => false,
                    'error'   => 'Zakazana akcija mora imati tool_name.',
                );
            }
            if ( $this->is_scheduler_control_tool( $tool_name ) ) {
                return $this->build_scheduler_control_tool_payload( $tool_name );
            }

            $preview = $this->admin_tools->preview_tool_call( $tool_name, $tool_args, $job_prompt );
            if ( empty( $preview['success'] ) || empty( $preview['action'] ) || ! is_array( $preview['action'] ) ) {
                return array(
                    'success' => false,
                    'error'   => isset( $preview['error'] ) ? (string) $preview['error'] : 'Preview zakazane akcije nije uspeo.',
                );
            }

            $frozen_actions[] = $preview['action'];
            if ( ! empty( $preview['warnings'] ) && is_array( $preview['warnings'] ) ) {
                $warnings = array_merge( $warnings, array_values( array_filter( $preview['warnings'] ) ) );
            }
            if ( ! empty( $preview['action']['summary'] ) ) {
                $summaries[] = (string) $preview['action']['summary'];
            }
        }

        return $this->insert_job(
            'admin_plan',
            isset( $args['title'] ) ? (string) $args['title'] : 'Zakazana izmena sajta',
            $job_prompt,
            $schedule,
            array(
                'actions'  => $frozen_actions,
                'warnings' => array_values( array_unique( $warnings ) ),
                'summary'  => implode( ' | ', array_slice( $summaries, 0, 5 ) ),
            ),
            $context
        );
    }

    public function create_self_wake_job( $args, $context = array(), $prompt = '' ) {
        $args    = is_array( $args ) ? $args : array();
        $context = is_array( $context ) ? $context : array();
        $task    = isset( $args['task'] ) ? trim( (string) $args['task'] ) : '';
        if ( '' === $task ) {
            $task = isset( $args['prompt_text'] ) ? trim( (string) $args['prompt_text'] ) : trim( (string) $prompt );
        }
        if ( '' === $task ) {
            return array(
                'success' => false,
                'error'   => 'Self-wake job mora imati task/prompt_text koji Niwa treba da obradi kada se probudi.',
            );
        }

        $args = $this->normalize_self_wake_schedule_args( $args );
        $schedule = $this->build_schedule_payload( $args );
        if ( empty( $schedule['success'] ) ) {
            return $schedule;
        }

        return $this->insert_job(
            'self_wake',
            isset( $args['title'] ) ? (string) $args['title'] : 'Niwa self-wake',
            $task,
            $schedule,
            array(
                'task'         => $task,
                'delivery_note' => isset( $args['delivery_note'] ) ? sanitize_text_field( (string) $args['delivery_note'] ) : '',
                'job_intent'   => isset( $args['job_intent'] ) ? sanitize_key( (string) $args['job_intent'] ) : '',
                'prewake_minutes' => isset( $args['prewake_minutes'] ) ? max( 0, min( 180, intval( $args['prewake_minutes'] ) ) ) : 0,
                'delivery_deadline_local' => isset( $args['delivery_deadline_local'] ) ? sanitize_text_field( (string) $args['delivery_deadline_local'] ) : '',
            ),
            $context
        );
    }

    public function create_orders_export_job( $args, $context = array() ) {
        $args     = is_array( $args ) ? $args : array();
        $context  = is_array( $context ) ? $context : array();
        $schedule = $this->build_schedule_payload( $args );
        if ( empty( $schedule['success'] ) ) {
            return $schedule;
        }

        $window_hours = isset( $args['window_hours'] ) ? max( 1, min( 168, intval( $args['window_hours'] ) ) ) : 24;

        return $this->insert_job(
            'orders_export',
            isset( $args['title'] ) ? (string) $args['title'] : 'Dnevni XLSX izvestaj porudzbina',
            isset( $args['prompt_text'] ) ? (string) $args['prompt_text'] : 'Recurring orders export',
            $schedule,
            array(
                'window_hours' => $window_hours,
            ),
            $context
        );
    }

    public function list_jobs( $chat_id = '', $status = 'active', $limit = 12, $include_prompt_text = false ) {
        global $wpdb;

        $chat_id = trim( (string) $chat_id );
        $status  = sanitize_key( (string) $status );
        $limit   = max( 1, min( 50, intval( $limit ) ) );
        $where   = array( '1=1' );
        $params  = array();

        if ( '' !== $chat_id ) {
            $where[]  = 'chat_id = %s';
            $params[] = $chat_id;
        }

        if ( '' !== $status && 'all' !== $status ) {
            $where[]  = 'status = %s';
            $params[] = $status;
        }

        $params[] = $limit;
        $sql      = "SELECT * FROM {$this->get_table_name()} WHERE " . implode( ' AND ', $where ) . ' ORDER BY created_at DESC LIMIT %d';
        $rows     = $wpdb->get_results( $wpdb->prepare( $sql, $params ), ARRAY_A );
        $jobs     = array();

        foreach ( (array) $rows as $row ) {
            $jobs[] = $this->format_job_for_output( $this->hydrate_row( $row ), $include_prompt_text );
        }

        return $jobs;
    }

    public function cancel_job( $job_id, $chat_id = '' ) {
        $job = $this->find_job( $job_id, $chat_id );
        if ( empty( $job ) ) {
            return array(
                'success' => false,
                'error'   => 'Zakazani posao nije pronadjen.',
            );
        }

		if ( 'cancelled' === (string) $job['status'] ) {
			$this->cleanup_targeted_processing_event( $job );
			$worker_cancel = $this->call(
				'cancel_scheduled_subagents',
				array( intval( $job['id'] ), (string) $job['job_uuid'], 'Scheduled task cancellation re-verified by founder.' ),
				array( 'success' => true, 'cancelled' => 0, 'signaled' => 0 )
			);
			return array(
				'success' => true,
				'already_cancelled' => true,
				'job'     => $this->format_job_for_output( $job ),
				'workers' => is_array( $worker_cancel ) ? $worker_cancel : array(),
				'error'   => '',
			);
		}

        if ( in_array( (string) $job['status'], array( 'completed', 'failed' ), true ) ) {
            return array(
                'success' => false,
                'error'   => 'Zakazani posao vise nije aktivan.',
            );
        }

        $updated = $this->update_job_row(
            intval( $job['id'] ),
            array(
                'status'       => 'cancelled',
                'completed_at' => current_time( 'mysql', true ),
                'last_error'   => '',
                'next_run_at'  => null,
            )
        );

        if ( ! empty( $updated ) ) {
            $this->cleanup_targeted_processing_event( $updated );
        } else {
            $this->cleanup_targeted_processing_event( $job );
        }

        $worker_cancel = $this->call(
            'cancel_scheduled_subagents',
            array( intval( $job['id'] ), (string) $job['job_uuid'], 'Scheduled task cancelled by founder.' ),
            array( 'success' => true, 'cancelled' => 0, 'signaled' => 0 )
        );

        return array(
            'success' => ! empty( $updated ),
            'job'     => $updated ? $this->format_job_for_output( $updated ) : array(),
            'workers' => is_array( $worker_cancel ) ? $worker_cancel : array(),
            'error'   => $updated ? '' : 'Otkazivanje zakazanog posla nije uspelo.',
        );
    }

    public function process_due_jobs( $limit = 3 ) {
        global $wpdb;

        $limit = max( 1, min( 10, intval( $limit ) ) );
        if ( ! $this->acquire_lock() ) {
            return array(
                'success'   => true,
                'processed' => 0,
                'note'      => 'scheduler_locked',
            );
        }

        try {
            $this->cleanup_recent_terminal_targeted_events();
            $this->recover_stale_running_jobs();
            $this->recover_stale_actor_pending_jobs();

            $query_limit = max( $limit + 5, $limit * 3 );
            $rows = $wpdb->get_results(
                $wpdb->prepare(
                    "SELECT * FROM {$this->get_table_name()} WHERE status = 'active' AND next_run_at IS NOT NULL AND next_run_at <= %s ORDER BY next_run_at ASC LIMIT %d",
                    current_time( 'mysql', true ),
                    $query_limit
                ),
                ARRAY_A
            );

            $processed = 0;
            $errors    = array();

            foreach ( (array) $rows as $row ) {
                $job = $this->hydrate_row( $row );
                if ( $this->should_runner_defer_to_targeted_event( $job ) ) {
                    continue;
                }
                if ( empty( $job ) ) {
                    continue;
                }

                $execution_lock = $this->acquire_job_execution_lock( $job );
                if ( empty( $execution_lock ) ) {
                    continue;
                }

                try {
                    if ( ! $this->claim_job( intval( $job['id'] ) ) ) {
                        continue;
                    }

                    $job              = $this->get_job_by_id( intval( $job['id'] ) );
                    $execution_result = $this->execute_job( $job );
                    $finalize_result  = ! empty( $execution_result['awaiting_actor_result'] )
                        ? array(
                            'success' => true,
                            'job'     => $this->get_job_by_id( intval( isset( $job['id'] ) ? $job['id'] : 0 ) ),
                            'error'   => '',
                        )
                        : $this->finalize_job_execution( $job, $execution_result );
                    $processed++;

                    if ( ! empty( $finalize_result['error'] ) ) {
                        $errors[] = (string) $finalize_result['error'];
                    }
                    if ( $processed >= $limit ) {
                        break;
                    }
                } finally {
                    $this->release_job_execution_lock( $execution_lock );
                }
            }

            return array(
                'success'   => true,
                'processed' => $processed,
                'errors'    => $errors,
            );
        } finally {
            $this->release_lock();
        }
    }

    public function process_targeted_job_event( $job_id, $job_uuid = '' ) {
        $job_id   = intval( $job_id );
        $job_uuid = trim( (string) $job_uuid );

        if ( $job_id < 1 ) {
            return array(
                'success' => false,
                'error'   => 'invalid_job_id',
            );
        }

        if ( ! $this->acquire_lock() ) {
            return array(
                'success' => true,
                'note'    => 'scheduler_locked',
            );
        }

        try {
            $job = $this->get_job_by_id( $job_id );
            if ( empty( $job ) ) {
                $this->cleanup_targeted_processing_event_args( $job_id, $job_uuid );
                return array(
                    'success' => false,
                    'error'   => 'job_not_found',
                );
            }

            if ( '' !== $job_uuid && ! hash_equals( (string) $job['job_uuid'], $job_uuid ) ) {
                $this->cleanup_targeted_processing_event_args( $job_id, $job_uuid );
                return array(
                    'success' => false,
                    'error'   => 'job_uuid_mismatch',
                );
            }

            if ( $this->is_terminal_job_status( isset( $job['status'] ) ? (string) $job['status'] : '' ) ) {
                $this->cleanup_targeted_processing_event( $job );
                return array(
                    'success' => true,
                    'note'    => 'job_already_finished',
                    'job'     => $this->format_job_for_output( $job ),
                );
            }

            if ( 'active' !== (string) $job['status'] ) {
                return array(
                    'success' => true,
                    'note'    => 'job_not_active',
                    'job'     => $this->format_job_for_output( $job ),
                );
            }

            if ( ! $this->is_job_due_now( $job ) ) {
                $this->ensure_job_processing_event( $job );
                return array(
                    'success' => true,
                    'note'    => 'job_not_due',
                    'job'     => $this->format_job_for_output( $job ),
                );
            }

            $execution_lock = $this->acquire_job_execution_lock( $job );
            if ( empty( $execution_lock ) ) {
                return array(
                    'success' => true,
                    'note'    => 'job_execution_locked',
                    'job'     => $this->format_job_for_output( $job ),
                );
            }

            try {
                if ( ! $this->claim_job( $job_id ) ) {
                    $latest = $this->get_job_by_id( $job_id );
                    if ( ! empty( $latest ) && $this->is_terminal_job_status( isset( $latest['status'] ) ? (string) $latest['status'] : '' ) ) {
                        $this->cleanup_targeted_processing_event( $latest );
                    }

                    return array(
                        'success' => true,
                        'note'    => 'job_claim_skipped',
                        'job'     => $this->format_job_for_output( $latest ),
                    );
                }

                $job              = $this->get_job_by_id( $job_id );
                $this->cleanup_targeted_processing_event( $job );
                $execution_result = $this->execute_job( $job );
                $finalize_result  = ! empty( $execution_result['awaiting_actor_result'] )
                    ? array(
                        'success' => true,
                        'job'     => $this->get_job_by_id( $job_id ),
                        'error'   => '',
                    )
                    : $this->finalize_job_execution( $job, $execution_result );

                return array(
                    'success'          => empty( $finalize_result['error'] ),
                    'processed'        => 1,
                    'execution_result' => $execution_result,
                    'job'              => isset( $finalize_result['job'] ) ? $this->format_job_for_output( $finalize_result['job'] ) : array(),
                    'error'            => isset( $finalize_result['error'] ) ? (string) $finalize_result['error'] : '',
                );
            } finally {
                $this->release_job_execution_lock( $execution_lock );
            }
        } finally {
            $this->release_lock();
        }
    }

    public function record_scheduled_actor_result( $session_id, $entry, $result ) {
        unset( $session_id );
        $entry  = is_array( $entry ) ? $entry : array();
        $result = is_array( $result ) ? $result : array();
        $ref    = $this->extract_scheduled_job_reference_from_actor_entry( $entry );
        if ( empty( $ref['id'] ) || empty( $ref['job_uuid'] ) ) {
            return array(
                'success' => true,
                'ignored' => true,
                'reason'  => 'not_scheduled_actor_message',
            );
        }

        $job = $this->get_job_by_id( intval( $ref['id'] ) );
        if ( empty( $job ) || empty( $job['job_uuid'] ) || ! hash_equals( (string) $job['job_uuid'], (string) $ref['job_uuid'] ) ) {
            return array(
                'success'       => true,
                'ignored'       => true,
                'non_retryable' => true,
                'reason'        => 'scheduled_job_not_found_or_mismatch',
            );
        }

        $status = isset( $job['status'] ) ? (string) $job['status'] : '';
        if ( $this->is_terminal_job_status( $status ) || ! in_array( $status, array( 'actor_pending', 'running' ), true ) ) {
            return array(
                'success' => true,
                'ignored' => true,
                'reason'  => 'scheduled_job_not_waiting_for_actor',
                'job'     => $this->format_job_for_output( $job ),
            );
        }

        if ( ! $this->scheduled_actor_attempt_matches( $job, $entry ) ) {
            return array(
                'success'       => true,
                'ignored'       => true,
                'non_retryable' => true,
                'reason'        => 'scheduled_actor_attempt_mismatch',
                'job'           => $this->format_job_for_output( $job ),
            );
        }

        if ( ! empty( $result['continuation'] ) && is_array( $result['continuation'] ) ) {
            $updated = $this->update_job_row(
                intval( $job['id'] ),
                array(
                    'status'     => 'actor_pending',
                    'last_error' => 'actor_continuation_pending',
                )
            );

            return array(
                'success' => ! empty( $updated ),
                'pending' => true,
                'reason'  => 'actor_continuation_pending',
                'job'     => $this->format_job_for_output( ! empty( $updated ) ? $updated : $job ),
            );
        }

        if ( $this->actor_result_has_pending_extension( $result ) ) {
            return $this->park_scheduled_actor_job_for_extension( $job, 'extension_pending_approval', $result );
        }

        if ( empty( $result['success'] ) && ! empty( $result['successful_write_execution'] ) ) {
            $runtime_error = isset( $result['error'] ) ? trim( (string) $result['error'] ) : '';
            $error         = __( 'Scheduled task completed at least one write, but its final response was not confirmed. Automatic retry was suppressed to prevent duplicate side effects.', 'niwa-ai' );
            if ( '' !== $runtime_error ) {
                $error .= ' Runtime: ' . $runtime_error;
            }

            $finalize_result = $this->finalize_job_execution(
                $job,
                array(
                    'success'                      => false,
                    'error'                        => $error,
                    'retry_suppressed_after_write' => true,
                )
            );

            return array(
                'success'          => empty( $finalize_result['error'] ),
                'finalized'        => true,
                'retry_suppressed' => true,
                'reason'           => 'scheduled_actor_retry_suppressed_after_write',
                'job'              => isset( $finalize_result['job'] ) ? $this->format_job_for_output( $finalize_result['job'] ) : array(),
                'error'            => isset( $finalize_result['error'] ) ? (string) $finalize_result['error'] : '',
            );
        }

        if ( array_key_exists( 'processor_success', $result ) && empty( $result['processor_success'] ) ) {
            $error = isset( $result['processor_error'] ) ? trim( (string) $result['processor_error'] ) : '';
            return $this->retry_scheduled_actor_job( $job, '' !== $error ? $error : 'scheduled_actor_processor_failed', self::SCHEDULED_ACTOR_RETRY_DELAY );
        }

        if ( empty( $result['success'] ) ) {
            $error = isset( $result['error'] ) ? trim( (string) $result['error'] ) : '';
            return $this->retry_scheduled_actor_job( $job, '' !== $error ? $error : 'scheduled_actor_failed', self::SCHEDULED_ACTOR_RETRY_DELAY );
        }

        if ( $this->scheduled_research_delivery_is_before_target( $job ) ) {
            if ( $this->actor_result_requires_immediate_preparation_attention( $result ) ) {
                $error = $this->scheduled_preparation_attention_error( $result );
                $finalize_result = $this->finalize_job_execution(
                    $job,
                    array(
                        'success'                    => false,
                        'complete_job'               => true,
                        'error'                      => $error,
                        'skip_scheduler_notification' => true,
                    )
                );

                return array(
                    'success'            => empty( $finalize_result['error'] ),
                    'finalized'          => true,
                    'founder_notified'   => true,
                    'reason'             => 'scheduled_delivery_preparation_immediate_attention',
                    'job'                => isset( $finalize_result['job'] ) ? $this->format_job_for_output( $finalize_result['job'] ) : array(),
                    'error'              => isset( $finalize_result['error'] ) ? (string) $finalize_result['error'] : '',
                );
            }
            return $this->mark_scheduled_delivery_prepared_pending( $job, $result );
        }

        if ( ! $this->actor_result_delivery_succeeded( $result ) ) {
            $delivery_error = '';
            if ( ! empty( $result['delivery'] ) && is_array( $result['delivery'] ) && ! empty( $result['delivery']['error'] ) ) {
                $delivery_error = (string) $result['delivery']['error'];
            }
            return $this->retry_scheduled_actor_job( $job, '' !== trim( $delivery_error ) ? $delivery_error : 'scheduled_actor_delivery_not_confirmed', self::SCHEDULED_ACTOR_RETRY_DELAY );
        }

        $summary = isset( $result['reply'] ) && '' !== trim( (string) $result['reply'] )
            ? 'Scheduled self-wake actor delivered a reply.'
            : 'Scheduled self-wake actor completed.';

        $finalize_result = $this->finalize_job_execution(
            $job,
            array(
                'success'  => true,
                'summary'  => $summary,
                'actor_id' => isset( $entry['id'] ) ? (string) $entry['id'] : '',
            )
        );

        return array(
            'success' => empty( $finalize_result['error'] ),
            'finalized' => true,
            'job'     => isset( $finalize_result['job'] ) ? $this->format_job_for_output( $finalize_result['job'] ) : array(),
            'error'   => isset( $finalize_result['error'] ) ? (string) $finalize_result['error'] : '',
        );
    }

    public function validate_scheduled_actor_entry( $session_id, $entry ) {
        unset( $session_id );
        $entry = is_array( $entry ) ? $entry : array();
        $ref   = $this->extract_scheduled_job_reference_from_actor_entry( $entry );
        if ( empty( $ref['id'] ) || empty( $ref['job_uuid'] ) ) {
            return array(
                'success' => true,
                'valid'   => true,
                'ignored' => true,
                'reason'  => 'not_scheduled_actor_message',
            );
        }

        $job = $this->get_job_by_id( intval( $ref['id'] ) );
        if ( empty( $job ) || empty( $job['job_uuid'] ) || ! hash_equals( (string) $job['job_uuid'], (string) $ref['job_uuid'] ) ) {
            return array(
                'success'       => true,
                'valid'         => false,
                'non_retryable' => true,
                'reason'        => 'scheduled_job_not_found_or_mismatch',
            );
        }

        $status = isset( $job['status'] ) ? (string) $job['status'] : '';
        if ( $this->is_terminal_job_status( $status ) || ! in_array( $status, array( 'actor_pending', 'running' ), true ) ) {
            return array(
                'success'       => true,
                'valid'         => false,
                'non_retryable' => true,
                'reason'        => 'scheduled_job_not_waiting_for_actor',
                'job'           => $this->format_job_for_output( $job ),
            );
        }

        if ( ! $this->scheduled_actor_attempt_matches( $job, $entry ) ) {
            return array(
                'success'       => true,
                'valid'         => false,
                'non_retryable' => true,
                'reason'        => 'scheduled_actor_attempt_mismatch',
                'job'           => $this->format_job_for_output( $job ),
            );
        }

        return array(
            'success' => true,
            'valid'   => true,
            'job'     => $this->format_job_for_output( $job ),
        );
    }

    private function execute_job( $job ) {
        $job_type = isset( $job['job_type'] ) ? (string) $job['job_type'] : '';

        try {
            switch ( $job_type ) {
                case 'reminder':
                    return $this->execute_reminder_job( $job );
                case 'admin_plan':
                    return $this->execute_admin_plan_job( $job );
                case 'self_wake':
                    return $this->execute_self_wake_job( $job );
                case 'orders_export':
                    return $this->execute_orders_export_job( $job );
            }
        } catch ( Throwable $e ) {
            return array(
                'success' => false,
                'error'   => $e->getMessage(),
            );
        }

        return array(
            'success' => false,
            'error'   => 'Nepodrzan scheduler job type: ' . $job_type,
        );
    }

    private function execute_reminder_job( $job ) {
        $message = isset( $job['payload']['message'] ) ? trim( (string) $job['payload']['message'] ) : '';
        if ( '' === $message ) {
            return array(
                'success' => false,
                'error'   => 'Reminder job nema poruku.',
            );
        }

        $text    = $this->normalize_assistant_text( 'Podsetnik: ' . $message );
        $chat_id = $this->resolve_job_chat_id( $job );
        if ( $this->is_admin_ui_job( $job ) ) {
            $delivered = $this->deliver_admin_ui_message( $job, $text );
            if ( empty( $delivered['success'] ) ) {
                return array(
                    'success' => false,
                    'error'   => isset( $delivered['error'] ) ? (string) $delivered['error'] : 'Admin UI reminder delivery nije uspeo.',
                );
            }

            return array(
                'success' => true,
                'summary' => $text,
            );
        }

        if ( '' === $chat_id || ! $this->telegram || ! method_exists( $this->telegram, 'send_message' ) ) {
            return array(
                'success' => false,
                'error'   => 'Telegram runtime nije dostupan za reminder.',
            );
        }

        $sent = $this->telegram->send_message( $chat_id, $text );
        if ( empty( $sent['success'] ) ) {
            return array(
                'success' => false,
                'error'   => isset( $sent['error'] ) ? (string) $sent['error'] : 'Telegram reminder send nije uspeo.',
            );
        }

        return array(
            'success' => true,
            'summary' => $text,
        );
    }

    private function execute_admin_plan_job( $job ) {
        if ( ! $this->admin_tools ) {
            return array(
                'success' => false,
                'error'   => 'Admin tools nisu ucitani.',
            );
        }

        $actions = isset( $job['payload']['actions'] ) && is_array( $job['payload']['actions'] ) ? array_values( $job['payload']['actions'] ) : array();
        if ( empty( $actions ) ) {
            return array(
                'success' => false,
                'error'   => 'Zakazani admin job nema konkretne akcije.',
            );
        }

        $run = array();
        if ( $this->store ) {
            $run = $this->store->create_run(
                array(
                    'source'          => 'scheduled_job',
                    'prompt_text'     => isset( $job['prompt_text'] ) ? (string) $job['prompt_text'] : ( isset( $job['title'] ) ? (string) $job['title'] : 'Scheduled admin job' ),
                    'plan'            => array(
                        'version'       => 2,
                        'kind'          => 'scheduled_job',
                        'agent_model'   => 'scheduler',
                        'summary'       => isset( $job['title'] ) ? (string) $job['title'] : 'Scheduled admin job',
                        'rollback_plan' => 'Rollback is available if the executed actions returned rollback entries.',
                        'actions'       => $actions,
                    ),
                    'approval_state'  => 'approved',
                    'execution_state' => 'planned',
                    'context'         => array(
                        'job_id'     => intval( isset( $job['id'] ) ? $job['id'] : 0 ),
                        'job_uuid'   => isset( $job['job_uuid'] ) ? (string) $job['job_uuid'] : '',
                        'chat_id'    => isset( $job['chat_id'] ) ? (string) $job['chat_id'] : '',
                        'session_id' => isset( $job['session_id'] ) ? (string) $job['session_id'] : '',
                    ),
                )
            );

            if ( ! empty( $run['id'] ) ) {
                $this->store->mark_execution_started( intval( $run['id'] ) );
            }
        }

        $executed = array();
        $rollback = array();
        $notes    = array();

        if ( method_exists( $this->admin_tools, 'set_runtime_context' ) ) {
            $this->admin_tools->set_runtime_context(
                array(
                    'source'                           => 'scheduled_job',
                    'requires_admin_confirmation_gate' => true,
                    'job_id'                           => intval( isset( $job['id'] ) ? $job['id'] : 0 ),
                    'job_uuid'                         => isset( $job['job_uuid'] ) ? (string) $job['job_uuid'] : '',
                    'chat_id'                          => isset( $job['chat_id'] ) ? (string) $job['chat_id'] : '',
                    'session_id'                       => isset( $job['session_id'] ) ? (string) $job['session_id'] : '',
                )
            );
        }

        try {
            foreach ( $actions as $action ) {
                $validation = $this->admin_tools->validate_action( $action );
                if ( empty( $validation['success'] ) ) {
                    $error = isset( $validation['error'] ) ? (string) $validation['error'] : 'Zakazana akcija nije prosla validaciju.';
                    $this->rollback_if_needed( $rollback );
                    $this->finalize_scheduled_run_failure( $run, $executed, $rollback, $error );
                    return array(
                        'success' => false,
                        'error'   => $error,
                    );
                }

                $result = $this->admin_tools->execute_action( $action );
                if ( empty( $result['success'] ) ) {
                    $error = isset( $result['error'] ) ? (string) $result['error'] : 'Zakazana akcija nije izvrsena.';
                    $rb    = $this->rollback_if_needed( $rollback );
                    if ( empty( $rb['success'] ) && ! empty( $rb['error'] ) ) {
                        $error .= ' Rollback problem: ' . (string) $rb['error'];
                    }
                    $this->finalize_scheduled_run_failure( $run, $executed, $rollback, $error );
                    return array(
                        'success' => false,
                        'error'   => $error,
                    );
                }

                Niwa_AI_External_Page_Cache::refresh_seraphinite_after_change( isset( $action['type'] ) ? (string) $action['type'] : 'scheduled_action' );

                if ( ! empty( $result['executed_actions'] ) && is_array( $result['executed_actions'] ) ) {
                    $executed = array_merge( $executed, $result['executed_actions'] );
                }
                if ( ! empty( $result['rollback_entries'] ) && is_array( $result['rollback_entries'] ) ) {
                    $rollback = array_merge( $rollback, $result['rollback_entries'] );
                }
                if ( ! empty( $result['summary'] ) ) {
                    $notes[] = (string) $result['summary'];
                }
            }
        } finally {
            if ( method_exists( $this->admin_tools, 'clear_runtime_context' ) ) {
                $this->admin_tools->clear_runtime_context();
            }
        }

        if ( ! empty( $run['id'] ) && $this->store ) {
            $this->store->mark_execution_result(
                intval( $run['id'] ),
                'succeeded',
                array(
                    'approval_state'   => 'approved',
                    'executed_actions' => $executed,
                    'rollback_entries' => $rollback,
                    'rollback_status'  => ! empty( $rollback ) ? 'available' : 'none',
                )
            );
        }

        return array(
            'success'            => true,
            'summary'            => ! empty( $notes ) ? implode( ' ', array_slice( $notes, 0, 4 ) ) : 'Zakazana izmena je uspesno odradjena.',
            'run_id'             => ! empty( $run['id'] ) ? intval( $run['id'] ) : 0,
            'rollback_available' => ! empty( $rollback ),
        );
    }

    private function execute_self_wake_job( $job ) {
        $task = isset( $job['payload']['task'] ) ? trim( (string) $job['payload']['task'] ) : '';
        if ( '' === $task ) {
            $task = isset( $job['prompt_text'] ) ? trim( (string) $job['prompt_text'] ) : '';
        }
        if ( '' === $task ) {
            return array(
                'success' => false,
                'error'   => 'Self-wake job nema task tekst.',
            );
        }

        $context = isset( $job['context'] ) && is_array( $job['context'] ) ? $job['context'] : array();
        $chat_id = $this->resolve_job_chat_id( $job );
        if ( '' === $chat_id && ! empty( $job['session_id'] ) ) {
            $chat_id = 'scheduled:' . (string) $job['session_id'];
        }

        $prepared_delivery = $this->maybe_deliver_prepared_scheduled_delivery( $job, $chat_id );
        if ( ! empty( $prepared_delivery['handled'] ) ) {
            return isset( $prepared_delivery['result'] ) && is_array( $prepared_delivery['result'] )
                ? $prepared_delivery['result']
                : array(
                    'success' => false,
                    'error'   => 'Prepared scheduled delivery handler nije vratio rezultat.',
                );
        }

        $attempt_token = function_exists( 'wp_generate_uuid4' ) ? wp_generate_uuid4() : uniqid( 'scheduled_actor_', true );
        $message = array(
            'update_id'  => -1 * max( 1, intval( isset( $job['id'] ) ? $job['id'] : 0 ) ),
            'message_id' => 0,
            'chat_id'    => $chat_id,
            'from_id'    => $chat_id,
            'text'       => $task,
            'date'       => time(),
            'channel'    => isset( $context['channel'] ) ? sanitize_key( (string) $context['channel'] ) : 'scheduled',
            'user_role'  => 'admin',
            'source'     => 'scheduled_self_wake',
            'scheduled_job' => array(
                'id'       => intval( isset( $job['id'] ) ? $job['id'] : 0 ),
                'job_uuid' => isset( $job['job_uuid'] ) ? (string) $job['job_uuid'] : '',
                'title'    => isset( $job['title'] ) ? (string) $job['title'] : '',
                'attempt_token' => $attempt_token,
            ),
        );
        $message['context_payload'] = $this->build_self_wake_execution_context( $job, $task, $context );
        $message['context_payload']['scheduled_actor_attempt_token'] = $attempt_token;

        $pending_context = isset( $job['context'] ) && is_array( $job['context'] ) ? $job['context'] : array();
        $pending_context['scheduled_actor_attempt_token'] = $attempt_token;
        $pending_job = $this->update_job_row(
            intval( isset( $job['id'] ) ? $job['id'] : 0 ),
            array(
                'status'       => 'actor_pending',
                'last_run_at'  => current_time( 'mysql', true ),
                'run_count'    => intval( isset( $job['run_count'] ) ? $job['run_count'] : 0 ) + 1,
                'last_error'   => '',
                'context_json' => wp_json_encode( $pending_context ),
            )
        );
        if ( empty( $pending_job ) ) {
            return array(
                'success' => false,
                'error'   => 'Self-wake actor pending state nije mogao da se sacuva.',
            );
        }

        $queued = $this->call( 'enqueue_scheduled_self_wake', array( $message, $job ), array() );
        if ( empty( $queued['success'] ) ) {
            $retry_job = $this->update_job_row(
                intval( isset( $job['id'] ) ? $job['id'] : 0 ),
                array(
                    'status'      => 'active',
                    'next_run_at' => gmdate( 'Y-m-d H:i:s', time() + 60 ),
                    'last_error'  => isset( $queued['error'] ) ? (string) $queued['error'] : 'scheduled_self_wake_enqueue_failed',
                )
            );
            if ( ! empty( $retry_job ) ) {
                $this->ensure_job_processing_event( $retry_job );
            }
            return array(
                'success' => false,
                'error'   => isset( $queued['error'] ) ? (string) $queued['error'] : 'Self-wake enqueue nije uspeo.',
            );
        }

        $pending_context['scheduled_actor_id'] = isset( $queued['actor_id'] ) ? (string) $queued['actor_id'] : '';
        $this->update_job_row(
            intval( isset( $job['id'] ) ? $job['id'] : 0 ),
            array(
                'context_json' => wp_json_encode( $pending_context ),
            )
        );
        $this->call( 'wake_scheduled_actor_runtime', array( ! empty( $pending_job ) ? $pending_job : $job, $queued ), array() );

        return array(
            'success'               => true,
            'awaiting_actor_result' => true,
            'summary'               => 'Niwa self-wake je ubacen u actor queue i ceka actor rezultat pre scheduler finalizacije.',
            'actor_id'              => isset( $queued['actor_id'] ) ? (string) $queued['actor_id'] : '',
        );
    }

    private function normalize_self_wake_schedule_args( $args ) {
        $args   = is_array( $args ) ? $args : array();
        $intent = isset( $args['job_intent'] ) ? sanitize_key( (string) $args['job_intent'] ) : '';
        if ( 'scheduled_research_delivery' !== $intent ) {
            return $args;
        }

        $prewake = isset( $args['prewake_minutes'] ) ? intval( $args['prewake_minutes'] ) : 0;
        if ( $prewake <= 0 ) {
            $args['prewake_minutes'] = self::DEFAULT_RESEARCH_DELIVERY_PREWAKE_MINUTES;
        }

        if ( empty( $args['delivery_deadline_local'] ) && ! empty( $args['run_at_local'] ) ) {
            $args['delivery_deadline_local'] = sanitize_text_field( (string) $args['run_at_local'] );
        }

        return $args;
    }

    private function build_self_wake_execution_context( $job, $task, $base_context = array() ) {
        $job          = is_array( $job ) ? $job : array();
        $base_context = is_array( $base_context ) ? $base_context : array();
        $context      = $base_context;

        foreach ( array(
            'history',
            'recent_work_notes',
            'session_worklog_summary',
            'session_subagent_notes',
            'session_active_tools',
            'session_extension_workflow',
            'session_current_topic',
            'session_immediate_goals',
            'session_recent_progress',
			'session_next_steps',
			'session_restart_recap',
			'session_recent_turn_log',
			'session_notes',
			'memory_summary',
			'companion_memory_summary',
			'agentic_growth_retry_context',
			'_original_context_payload',
			'continuation_context',
			'cancel_probe',
			'turn_progress_callback',
			'soft_deadline',
		) as $stale_key ) {
			unset( $context[ $stale_key ] );
		}
		$context['history'] = array();

        $scheduled_job = array(
            'id'       => intval( isset( $job['id'] ) ? $job['id'] : 0 ),
            'job_uuid' => isset( $job['job_uuid'] ) ? (string) $job['job_uuid'] : '',
            'title'    => isset( $job['title'] ) ? (string) $job['title'] : '',
        );

        $context['source']                                = 'scheduled_self_wake';
        $context['scheduled_self_wake_execution']         = true;
        $context['scheduled_self_wake_task']              = (string) $task;
        $context['scheduled_self_wake_job_id']            = $scheduled_job['id'];
        $context['scheduled_self_wake_job_uuid']          = $scheduled_job['job_uuid'];
        $context['scheduled_self_wake_job_title']         = $scheduled_job['title'];
        $context['scheduled_job']                         = $scheduled_job;
        $context['force_action']                          = true;
        $context['suppress_stale_scheduled_chat_history'] = true;
        $context['is_new_session']                        = false;
        $context['scheduled_actor_result_required']        = true;

        $payload = isset( $job['payload'] ) && is_array( $job['payload'] ) ? $job['payload'] : array();
        if ( ! empty( $payload['job_intent'] ) ) {
            $context['scheduled_job_intent'] = sanitize_key( (string) $payload['job_intent'] );
        }
        if ( ! empty( $payload['prewake_minutes'] ) ) {
            $context['scheduled_prewake_minutes'] = max( 0, min( 180, intval( $payload['prewake_minutes'] ) ) );
        }
        if ( ! empty( $payload['delivery_deadline_local'] ) ) {
            $context['scheduled_delivery_deadline_local'] = sanitize_text_field( (string) $payload['delivery_deadline_local'] );
        } else {
            $delivery_target = $this->derive_scheduled_delivery_target_local( $job );
            if ( '' !== $delivery_target ) {
                $context['scheduled_delivery_deadline_local'] = $delivery_target;
            }
        }
        if ( 'scheduled_research_delivery' === ( isset( $context['scheduled_job_intent'] ) ? (string) $context['scheduled_job_intent'] : '' ) ) {
            $context['scheduled_research_delivery'] = true;
        }
        if ( ! empty( $context['scheduled_delivery_deadline_local'] ) ) {
            $context['scheduled_delivery_phase'] = $this->scheduled_research_delivery_is_before_target( $job ) ? 'preparation' : 'delivery';
        }

        return $context;
    }

    private function derive_scheduled_delivery_target_local( $job ) {
        $job      = is_array( $job ) ? $job : array();
        $schedule = isset( $job['schedule'] ) && is_array( $job['schedule'] ) ? $job['schedule'] : array();
        $context  = isset( $job['context'] ) && is_array( $job['context'] ) ? $job['context'] : array();
        if ( ! empty( $context['current_delivery_target_local'] ) ) {
            return sanitize_text_field( (string) $context['current_delivery_target_local'] );
        }

        $kind = isset( $schedule['kind'] ) ? (string) $schedule['kind'] : '';
        if ( ! empty( $schedule['delivery_target_local'] ) && ! in_array( $kind, array( 'daily', 'weekly' ), true ) ) {
            return sanitize_text_field( (string) $schedule['delivery_target_local'] );
        }

        $prewake  = isset( $schedule['prewake_minutes'] ) ? max( 0, intval( $schedule['prewake_minutes'] ) ) : 0;
        if ( $prewake <= 0 ) {
            return '';
        }

        $timezone = $this->get_timezone( isset( $job['timezone'] ) ? (string) $job['timezone'] : '' );
        $wake     = $this->utc_mysql_to_local_datetime( isset( $job['next_run_at'] ) ? (string) $job['next_run_at'] : '', $timezone );
        if ( ! $wake ) {
            return '';
        }

        return $wake->modify( '+' . $prewake . ' minutes' )->format( 'Y-m-d H:i:s' );
    }

    private function extract_scheduled_job_reference_from_actor_entry( $entry ) {
        $entry      = is_array( $entry ) ? $entry : array();
        $candidates = array();

        if ( ! empty( $entry['message']['scheduled_job'] ) && is_array( $entry['message']['scheduled_job'] ) ) {
            $candidates[] = $entry['message']['scheduled_job'];
        }
        if ( ! empty( $entry['context_payload']['scheduled_job'] ) && is_array( $entry['context_payload']['scheduled_job'] ) ) {
            $candidates[] = $entry['context_payload']['scheduled_job'];
        }
        if ( ! empty( $entry['continuation_context']['scheduled_job'] ) && is_array( $entry['continuation_context']['scheduled_job'] ) ) {
            $candidates[] = $entry['continuation_context']['scheduled_job'];
        }

        foreach ( $candidates as $candidate ) {
            $id       = isset( $candidate['id'] ) ? intval( $candidate['id'] ) : 0;
            $job_uuid = isset( $candidate['job_uuid'] ) ? trim( (string) $candidate['job_uuid'] ) : '';
            if ( $id > 0 && '' !== $job_uuid ) {
                return array(
                    'id'       => $id,
                    'job_uuid' => $job_uuid,
                );
            }
        }

        $context = ! empty( $entry['context_payload'] ) && is_array( $entry['context_payload'] ) ? $entry['context_payload'] : array();
        $id      = isset( $context['scheduled_self_wake_job_id'] ) ? intval( $context['scheduled_self_wake_job_id'] ) : 0;
        $uuid    = isset( $context['scheduled_self_wake_job_uuid'] ) ? trim( (string) $context['scheduled_self_wake_job_uuid'] ) : '';
        if ( $id > 0 && '' !== $uuid ) {
            return array(
                'id'       => $id,
                'job_uuid' => $uuid,
            );
        }

        return array();
    }

    private function scheduled_actor_attempt_matches( $job, $entry ) {
        $job_context = isset( $job['context'] ) && is_array( $job['context'] ) ? $job['context'] : array();
        $expected    = isset( $job_context['scheduled_actor_attempt_token'] ) ? trim( (string) $job_context['scheduled_actor_attempt_token'] ) : '';
        if ( '' === $expected ) {
            return true;
        }

        $entry = is_array( $entry ) ? $entry : array();
        $candidates = array();
        if ( ! empty( $entry['message']['scheduled_job']['attempt_token'] ) ) {
            $candidates[] = (string) $entry['message']['scheduled_job']['attempt_token'];
        }
        if ( ! empty( $entry['context_payload']['scheduled_actor_attempt_token'] ) ) {
            $candidates[] = (string) $entry['context_payload']['scheduled_actor_attempt_token'];
        }
        if ( ! empty( $entry['continuation_context']['scheduled_actor_attempt_token'] ) ) {
            $candidates[] = (string) $entry['continuation_context']['scheduled_actor_attempt_token'];
        }

        foreach ( $candidates as $candidate ) {
            if ( hash_equals( $expected, trim( (string) $candidate ) ) ) {
                return true;
            }
        }

        return false;
    }

    private function actor_result_has_pending_extension( $result ) {
        $result = is_array( $result ) ? $result : array();
        if ( ! empty( $result['extension_pending_approval'] ) || ! empty( $result['retry_original_task'] ) ) {
            return true;
        }
        if ( isset( $result['task_completion_state'] ) && 'pending_extension_approval' === (string) $result['task_completion_state'] ) {
            return true;
        }
        if ( ! empty( $result['executed_tools'] ) && is_array( $result['executed_tools'] ) ) {
            foreach ( $result['executed_tools'] as $tool_name ) {
                if ( 'extension_request_create' === sanitize_key( (string) $tool_name ) ) {
                    return true;
                }
            }
        }

        return false;
    }

    private function actor_result_delivery_succeeded( $result ) {
        $result = is_array( $result ) ? $result : array();
        if ( ! empty( $result['scheduled_delivery_tool_successful'] ) || ! empty( $result['scheduled_delivery_complete'] ) ) {
            return true;
        }
        if ( ! empty( $result['delivery'] ) && is_array( $result['delivery'] ) ) {
            return ! empty( $result['delivery']['success'] ) && empty( $result['delivery']['skipped'] );
        }

        return isset( $result['reply'] ) && '' !== trim( (string) $result['reply'] );
    }

    private function actor_result_requires_immediate_preparation_attention( $result ) {
        $result = is_array( $result ) ? $result : array();
        foreach ( array( 'extension_pending_approval', 'retry_original_task', 'capability_gap', 'execution_incomplete' ) as $flag ) {
            if ( ! empty( $result[ $flag ] ) ) {
                return true;
            }
        }

        $state = isset( $result['task_completion_state'] ) ? sanitize_key( (string) $result['task_completion_state'] ) : '';
        if ( in_array( $state, array( 'pending_extension_approval', 'blocked_after_two_attempts' ), true ) ) {
            return true;
        }

        $budget_state = isset( $result['task_attempt_budget_status'] ) ? sanitize_key( (string) $result['task_attempt_budget_status'] ) : '';
        if ( 'blocked_after_two_attempts' === $budget_state ) {
            return true;
        }

        $next_required = isset( $result['next_required_step'] ) ? sanitize_key( (string) $result['next_required_step'] ) : '';
        if ( '' !== $next_required && false !== strpos( $next_required, 'approval' ) ) {
            return true;
        }

        return array_key_exists( 'success', $result ) && empty( $result['success'] ) && '' !== trim( isset( $result['reply'] ) ? (string) $result['reply'] : '' );
    }

    private function scheduled_preparation_attention_error( $result ) {
        $result = is_array( $result ) ? $result : array();
        if ( ! empty( $result['error'] ) ) {
            return (string) $result['error'];
        }
        if ( ! empty( $result['task_attempt_budget_status'] ) ) {
            return (string) $result['task_attempt_budget_status'];
        }
        if ( ! empty( $result['task_completion_state'] ) ) {
            return (string) $result['task_completion_state'];
        }
        if ( ! empty( $result['next_required_step'] ) ) {
            return (string) $result['next_required_step'];
        }

        return 'scheduled_delivery_preparation_requires_founder_attention';
    }

    private function scheduled_research_delivery_is_before_target( $job ) {
        $job     = is_array( $job ) ? $job : array();
        $payload = isset( $job['payload'] ) && is_array( $job['payload'] ) ? $job['payload'] : array();
        if ( 'scheduled_research_delivery' !== ( isset( $payload['job_intent'] ) ? sanitize_key( (string) $payload['job_intent'] ) : '' ) ) {
            return false;
        }

        $target_local = $this->derive_scheduled_delivery_target_local( $job );
        if ( '' === $target_local ) {
            return false;
        }

        $timezone = $this->get_timezone( isset( $job['timezone'] ) ? (string) $job['timezone'] : '' );
        $target   = $this->parse_local_datetime( $target_local, $timezone );
        if ( ! $target ) {
            return false;
        }

        return $target->getTimestamp() > time() + 30;
    }

    private function mark_scheduled_delivery_prepared_pending( $job, $result = array() ) {
        $job          = is_array( $job ) ? $job : array();
        $result       = is_array( $result ) ? $result : array();
        $target_local = $this->derive_scheduled_delivery_target_local( $job );
        $timezone     = $this->get_timezone( isset( $job['timezone'] ) ? (string) $job['timezone'] : '' );
        $target       = $this->parse_local_datetime( $target_local, $timezone );
        $next_run_at  = $target ? $this->local_datetime_to_utc_mysql( $target ) : gmdate( 'Y-m-d H:i:s', time() + self::SCHEDULED_ACTOR_RETRY_DELAY );
        $context      = isset( $job['context'] ) && is_array( $job['context'] ) ? $job['context'] : array();
        if ( ! empty( $result['scheduled_delivery_child_scheduled'] ) && empty( $result['prepared_delivery_message'] ) ) {
            return $this->retry_scheduled_actor_job( $job, 'scheduled_delivery_child_schedule_without_prepared_message', 60 );
        }
        $prepared     = $this->extract_prepared_scheduled_delivery_message( $result );
        if ( '' === $prepared ) {
            return $this->retry_scheduled_actor_job( $job, 'scheduled_delivery_preparation_missing_message', 60 );
        }

        $update_data  = array(
            'status'      => 'active',
            'next_run_at' => $next_run_at,
            'last_run_at' => current_time( 'mysql', true ),
            'last_error'  => 'prepared_delivery_pending',
        );

        $context['prepared_delivery_message']  = $prepared;
        $context['prepared_delivery_ready_at'] = current_time( 'mysql', true );
        $context['prepared_delivery_turn_id']  = isset( $result['turn_id'] ) ? (string) $result['turn_id'] : '';
        $context['prepared_delivery_job_uuid'] = isset( $job['job_uuid'] ) ? (string) $job['job_uuid'] : '';
        if ( '' !== $target_local ) {
            $context['current_delivery_target_local'] = $target_local;
        }
        if ( $target ) {
            $context['prepared_delivery_target_utc'] = $this->local_datetime_to_utc_mysql( $target );
        }
        $update_data['context_json'] = wp_json_encode( $context );

        $updated = $this->update_job_row(
            intval( isset( $job['id'] ) ? $job['id'] : 0 ),
            $update_data
        );
        if ( ! empty( $updated ) ) {
            $this->ensure_job_processing_event( $updated );
        }

        return array(
            'success' => ! empty( $updated ),
            'pending' => true,
            'reason'  => 'prepared_delivery_pending',
            'job'     => $this->format_job_for_output( ! empty( $updated ) ? $updated : $job ),
        );
    }

    private function extract_prepared_scheduled_delivery_message( $result ) {
        $result = is_array( $result ) ? $result : array();
        if ( ! empty( $result['prepared_delivery_message'] ) ) {
            return trim( (string) $result['prepared_delivery_message'] );
        }

        $reply  = isset( $result['reply'] ) ? trim( (string) $result['reply'] ) : '';
        if ( '' !== $reply ) {
            return $reply;
        }

        return '';
    }

    private function maybe_deliver_prepared_scheduled_delivery( $job, $chat_id = '' ) {
        $job     = is_array( $job ) ? $job : array();
        $payload = isset( $job['payload'] ) && is_array( $job['payload'] ) ? $job['payload'] : array();
        if ( 'scheduled_research_delivery' !== ( isset( $payload['job_intent'] ) ? sanitize_key( (string) $payload['job_intent'] ) : '' ) ) {
            return array( 'handled' => false );
        }
        if ( $this->scheduled_research_delivery_is_before_target( $job ) ) {
            return array( 'handled' => false );
        }

        $context = isset( $job['context'] ) && is_array( $job['context'] ) ? $job['context'] : array();
        $message = isset( $context['prepared_delivery_message'] ) ? trim( (string) $context['prepared_delivery_message'] ) : '';
        if ( '' === $message ) {
            return array( 'handled' => false );
        }

        $target_local = $this->derive_scheduled_delivery_target_local( $job );
        $prepared_target_local = isset( $context['current_delivery_target_local'] ) ? trim( (string) $context['current_delivery_target_local'] ) : '';
        if ( '' !== $target_local && '' !== $prepared_target_local && $target_local !== $prepared_target_local ) {
            return array(
                'handled' => true,
                'result'  => array(
                    'success' => false,
                    'error'   => 'prepared_delivery_target_mismatch',
                ),
            );
        }

        $prepared_job_uuid = isset( $context['prepared_delivery_job_uuid'] ) ? trim( (string) $context['prepared_delivery_job_uuid'] ) : '';
        if ( '' !== $prepared_job_uuid && ! empty( $job['job_uuid'] ) && ! hash_equals( (string) $job['job_uuid'], $prepared_job_uuid ) ) {
            return array(
                'handled' => true,
                'result'  => array(
                    'success' => false,
                    'error'   => 'prepared_delivery_job_mismatch',
                ),
            );
        }

        $prepared_ready_at = isset( $context['prepared_delivery_ready_at'] ) ? strtotime( (string) $context['prepared_delivery_ready_at'] . ' UTC' ) : 0;
        $stale_window = defined( 'DAY_IN_SECONDS' ) ? DAY_IN_SECONDS : 86400;
        if ( $prepared_ready_at > 0 && $prepared_ready_at < time() - $stale_window ) {
            return array(
                'handled' => true,
                'result'  => array(
                    'success' => false,
                    'error'   => 'prepared_delivery_stale',
                ),
            );
        }

        $delivered = $this->is_admin_ui_job( $job )
            ? $this->deliver_admin_ui_message( $job, $message )
            : $this->deliver_self_wake_message( $job, $message, $chat_id );
        if ( empty( $delivered['success'] ) ) {
            return array(
                'handled' => true,
                'result'  => array(
                    'success' => false,
                    'error'   => isset( $delivered['error'] ) ? (string) $delivered['error'] : 'Prepared scheduled delivery nije poslat.',
                ),
            );
        }

        unset( $context['prepared_delivery_message'], $context['prepared_delivery_ready_at'], $context['prepared_delivery_turn_id'], $context['prepared_delivery_job_uuid'], $context['prepared_delivery_target_utc'], $context['current_delivery_target_local'] );
        $this->update_job_row(
            intval( isset( $job['id'] ) ? $job['id'] : 0 ),
            array(
                'context_json' => wp_json_encode( $context ),
            )
        );

        return array(
            'handled' => true,
            'result'  => array(
                'success'  => true,
                'summary'  => 'Prepared scheduled research delivery sent.',
                'delivery' => $delivered,
            ),
        );
    }

    private function park_scheduled_actor_job_for_extension( $job, $error, $result = array() ) {
        $job     = is_array( $job ) ? $job : array();
        $result  = is_array( $result ) ? $result : array();
        $updated = $this->update_job_row(
            intval( isset( $job['id'] ) ? $job['id'] : 0 ),
            array(
                'status'      => 'extension_pending',
                'next_run_at' => null,
                'last_run_at' => current_time( 'mysql', true ),
                'run_count'   => intval( isset( $job['run_count'] ) ? $job['run_count'] : 0 ) + 1,
                'last_error'  => (string) $error,
            )
        );
        $this->cleanup_targeted_processing_event( ! empty( $updated ) ? $updated : $job );
        $notify = $this->notify_founder_extension_pending_job( $job, $result );

        return array(
            'success'          => ! empty( $updated ),
            'pending'          => true,
            'reason'           => 'extension_pending',
            'founder_notified' => ! empty( $notify['success'] ) || ! empty( $notify['already_delivered'] ),
            'job'              => $this->format_job_for_output( ! empty( $updated ) ? $updated : $job ),
        );
    }

    private function notify_founder_extension_pending_job( $job, $result ) {
        $job    = is_array( $job ) ? $job : array();
        $result = is_array( $result ) ? $result : array();

        $delivery = isset( $result['delivery'] ) && is_array( $result['delivery'] ) ? $result['delivery'] : array();
        if ( ! empty( $delivery['success'] ) && empty( $delivery['skipped'] ) ) {
            // The actor turn already delivered its reply (the approval ask) to
            // the founder chat, so a second scheduler message would duplicate it.
            return array(
                'success'           => true,
                'already_delivered' => true,
            );
        }

        $title = isset( $job['title'] ) ? trim( (string) $job['title'] ) : '';
        if ( '' === $title ) {
            $title = 'Zakazani zadatak';
        }

        $text = isset( $result['reply'] ) ? trim( (string) $result['reply'] ) : '';
        if ( '' === $text ) {
            $text = 'Pauzirala sam zakazani zadatak "' . $title . '" jer mi za njega nedostaje sposobnost koju mogu da napravim samo uz tvoje odobrenje (runtime self-extension). '
                . 'Zadatak ceka tvoju odluku i nece se ponavljati dok ne odgovoris. '
                . 'Odgovori "odobravam" da napravim tu sposobnost i nastavim zadatak, ili "ne" da otkazem.';
        }

        return $this->deliver_self_wake_message( $job, $text );
    }

    public function resume_extension_pending_jobs_for_session( $session_id, $approved = true ) {
        global $wpdb;

        $session_id = trim( (string) $session_id );
        if ( '' === $session_id ) {
            return array(
                'success' => false,
                'resumed' => 0,
                'error'   => 'missing_session_id',
            );
        }

        if ( ! empty( $approved ) ) {
            return array(
                'success'  => true,
                'resumed'  => 0,
                'deferred' => true,
                'reason'   => 'extension_approval_waiting_for_implementation',
            );
        }

        $rows = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT * FROM {$this->get_table_name()} WHERE status = 'extension_pending' AND session_id = %s ORDER BY updated_at ASC LIMIT %d",
                $session_id,
                10
            ),
            ARRAY_A
        );

        $count = 0;
        foreach ( (array) $rows as $row ) {
            $job = $this->hydrate_row( $row );
            $updated = $this->update_job_row(
                intval( isset( $job['id'] ) ? $job['id'] : 0 ),
                array(
                    'status'       => 'failed',
                    'next_run_at'  => null,
                    'completed_at' => current_time( 'mysql', true ),
                    'last_error'   => 'extension_denied',
                )
            );
            if ( ! empty( $updated ) ) {
                $this->cleanup_targeted_processing_event( $updated );
                $count++;
            }
        }

        return array(
            'success' => true,
            'resumed' => $count,
        );
    }

    private function retry_scheduled_actor_job( $job, $error, $delay_seconds = 0 ) {
        $job           = is_array( $job ) ? $job : array();
        $delay_seconds = max( 60, intval( $delay_seconds ) );
        $next_run      = gmdate( 'Y-m-d H:i:s', time() + $delay_seconds );
        $updated       = $this->update_job_row(
            intval( isset( $job['id'] ) ? $job['id'] : 0 ),
            array(
                'status'      => 'active',
                'next_run_at' => $next_run,
                'last_run_at' => current_time( 'mysql', true ),
                'run_count'   => intval( isset( $job['run_count'] ) ? $job['run_count'] : 0 ) + 1,
                'last_error'  => (string) $error,
            )
        );

        if ( ! empty( $updated ) ) {
            $this->ensure_job_processing_event( $updated );
        }

        return array(
            'success' => ! empty( $updated ),
            'retry'   => true,
            'error'   => empty( $updated ) ? 'scheduled_actor_retry_update_failed' : (string) $error,
            'job'     => $this->format_job_for_output( ! empty( $updated ) ? $updated : $job ),
        );
    }

	private function deliver_self_wake_message( $job, $text, $chat_id = '' ) {
		$text = trim( (string) $text );
		if ( '' === $text ) {
			return array(
				'success' => false,
				'error'   => 'Self-wake poruka je prazna.',
			);
		}

		$chat_id = trim( (string) $chat_id );
        if ( '' === $chat_id ) {
			$chat_id = $this->resolve_job_chat_id( is_array( $job ) ? $job : array() );
		}
		if ( '' === $chat_id || ! $this->telegram || ! method_exists( $this->telegram, 'send_message' ) ) {
			return array(
				'success' => false,
				'error'   => 'Telegram runtime nije dostupan za self-wake poruku.',
			);
		}

		$sent = $this->telegram->send_message( $chat_id, $this->normalize_assistant_text( $text ) );
		if ( empty( $sent['success'] ) ) {
			return array(
				'success' => false,
				'error'   => isset( $sent['error'] ) ? (string) $sent['error'] : 'Telegram self-wake send nije uspeo.',
			);
		}

		return array(
			'success' => true,
		);
	}

    private function call( $name, $args = array(), $default = null ) {
        if ( isset( $this->callbacks[ $name ] ) && is_callable( $this->callbacks[ $name ] ) ) {
            return call_user_func_array( $this->callbacks[ $name ], is_array( $args ) ? $args : array() );
        }

        return $default;
    }

    private function execute_orders_export_job( $job ) {
        if ( ! $this->workspace || ! method_exists( $this->workspace, 'export_orders_xlsx_window' ) ) {
            return array(
                'success' => false,
                'error'   => 'Workspace export layer nije ucitana.',
            );
        }

        $timezone  = $this->get_timezone( isset( $job['timezone'] ) ? (string) $job['timezone'] : '' );
        $due_utc   = isset( $job['next_run_at'] ) ? (string) $job['next_run_at'] : '';
        $due_local = $this->utc_mysql_to_local_datetime( $due_utc, $timezone );
        if ( ! $due_local ) {
            $due_local = new DateTimeImmutable( 'now', $timezone );
        }

        $window_hours = isset( $job['payload']['window_hours'] ) ? max( 1, min( 168, intval( $job['payload']['window_hours'] ) ) ) : 24;
        $start_local  = $due_local->modify( '-' . $window_hours . ' hours' );
        $end_local    = $due_local->modify( '-1 second' );
        $label        = 'scheduled-' . $due_local->format( 'Ymd-Hi' );
        $export       = $this->workspace->export_orders_xlsx_window( $start_local->format( 'Y-m-d H:i:s' ), $end_local->format( 'Y-m-d H:i:s' ), $label );

        if ( empty( $export['success'] ) ) {
            return array(
                'success' => false,
                'error'   => isset( $export['error'] ) ? (string) $export['error'] : 'XLSX export nije uspeo.',
            );
        }

        $chat_id = $this->resolve_job_chat_id( $job );
        if ( '' === $chat_id || ! $this->telegram || ! method_exists( $this->telegram, 'send_document' ) ) {
            return array(
                'success' => false,
                'error'   => 'Telegram runtime nije dostupan za XLSX slanje.',
            );
        }

        $caption = 'XLSX izvestaj porudzbina: ' . $start_local->format( 'd.m.Y H:i' ) . ' - ' . $due_local->format( 'd.m.Y H:i' );
        $document = '';
        if ( ! empty( $export['path'] ) && is_file( (string) $export['path'] ) ) {
            $document = (string) $export['path'];
        } elseif ( ! empty( $export['url'] ) ) {
            $document = (string) $export['url'];
        }
        $sent = $this->telegram->send_document( $chat_id, $document, $caption );
        if ( empty( $sent['success'] ) ) {
            return array(
                'success' => false,
                'error'   => isset( $sent['error'] ) ? (string) $sent['error'] : 'Telegram sendDocument nije uspeo.',
            );
        }

        return array(
            'success' => true,
            'summary' => 'Poslala sam XLSX izvestaj za period ' . $start_local->format( 'd.m.Y H:i' ) . ' - ' . $due_local->format( 'd.m.Y H:i' ) . '.',
            'export'  => $export,
        );
    }

    private function finalize_job_execution( $job, $execution_result ) {
        $execution_result = is_array( $execution_result ) ? $execution_result : array();
        $job              = is_array( $job ) ? $job : array();
        $current_job      = $this->get_job_by_id( intval( $job['id'] ?? 0 ) );
        if ( 'cancelled' === (string) ( $current_job['status'] ?? '' ) ) {
            $this->cleanup_targeted_processing_event( $current_job );
            return array(
                'success' => true,
                'job'     => $current_job,
                'note'    => 'job_cancelled_during_execution',
                'error'   => '',
            );
        }
        $timezone         = $this->get_timezone( isset( $job['timezone'] ) ? (string) $job['timezone'] : '' );
        $schedule         = isset( $job['schedule'] ) && is_array( $job['schedule'] ) ? $job['schedule'] : array();
        $next_run_local   = ! empty( $execution_result['complete_job'] )
            ? null
            : $this->compute_next_run_after_execution( $schedule, $timezone );
        $updates          = array(
            'last_run_at' => current_time( 'mysql', true ),
            'last_error'  => ! empty( $execution_result['error'] ) ? (string) $execution_result['error'] : '',
            'run_count'   => intval( isset( $job['run_count'] ) ? $job['run_count'] : 0 ) + 1,
        );

        if ( ! empty( $execution_result['success'] ) ) {
            $updates['last_success_at'] = current_time( 'mysql', true );
        }

        if ( $next_run_local ) {
            $updates['status']      = 'active';
            $updates['next_run_at'] = $this->local_datetime_to_utc_mysql( $next_run_local );
        } else {
            $updates['status']       = ! empty( $execution_result['success'] ) ? 'completed' : 'failed';
            $updates['next_run_at']  = null;
            $updates['completed_at'] = current_time( 'mysql', true );
        }

        $updated = $this->update_job_row( intval( isset( $job['id'] ) ? $job['id'] : 0 ), $updates );
        if ( ! empty( $updated['next_run_at'] ) ) {
            $this->ensure_job_processing_event( $updated );
        } else {
            $this->cleanup_targeted_processing_event( ! empty( $updated ) ? $updated : $job );
        }
        if ( empty( $execution_result['skip_scheduler_notification'] ) ) {
            $this->notify_job_result( $job, $execution_result, $updated );
        }

        return array(
            'success' => ! empty( $updated ),
            'job'     => $updated,
            'error'   => empty( $updated ) ? 'Scheduler job state update nije uspeo.' : '',
        );
    }

    private function notify_job_result( $job, $execution_result, $updated_job = array() ) {
        if ( $this->is_admin_ui_job( $job ) ) {
            $job_type = isset( $job['job_type'] ) ? (string) $job['job_type'] : '';
            if ( ! empty( $execution_result['success'] ) && in_array( $job_type, array( 'reminder', 'orders_export', 'self_wake' ), true ) ) {
                return;
            }

            $title   = isset( $job['title'] ) ? trim( (string) $job['title'] ) : 'Zakazani zadatak';
            $summary = isset( $execution_result['summary'] ) ? trim( (string) $execution_result['summary'] ) : '';
            $error   = isset( $execution_result['error'] ) ? trim( (string) $execution_result['error'] ) : '';
            $text    = ! empty( $execution_result['success'] )
                ? 'Uspela sam. ' . $title . ' je zavrsen.'
                : 'Nisam uspela da zavrsim zakazani zadatak: ' . $title . '.';
            if ( '' !== $summary && ! empty( $execution_result['success'] ) ) {
                $text .= "\n" . $summary;
            }
            if ( '' !== $error && empty( $execution_result['success'] ) ) {
                $text .= "\nPoteskoce: " . $error;
            }
            $this->deliver_admin_ui_message( $job, $this->normalize_assistant_text( $text ) );
            return;
        }

        $chat_id = $this->resolve_job_chat_id( ! empty( $updated_job ) ? $updated_job : $job );
        if ( '' === $chat_id || ! $this->telegram || ! method_exists( $this->telegram, 'send_message' ) ) {
            return;
        }

        $job_type = isset( $job['job_type'] ) ? (string) $job['job_type'] : '';
        if ( ! empty( $execution_result['success'] ) && in_array( $job_type, array( 'reminder', 'orders_export', 'self_wake' ), true ) ) {
            return;
        }

        $title   = isset( $job['title'] ) ? trim( (string) $job['title'] ) : 'Zakazani zadatak';
        $summary = isset( $execution_result['summary'] ) ? trim( (string) $execution_result['summary'] ) : '';
        $error   = isset( $execution_result['error'] ) ? trim( (string) $execution_result['error'] ) : '';

        if ( ! empty( $execution_result['success'] ) ) {
            $text = 'Uspela sam. ' . $title . ' je zavrsen.';
            if ( '' !== $summary ) {
                $text .= "\n" . $summary;
            }
        } else {
            $text = 'Nisam uspela da zavrsim zakazani zadatak: ' . $title . '.';
            if ( '' !== $error ) {
                $text .= "\nPoteskoce: " . $error;
            }
        }

        $this->telegram->send_message( $chat_id, $this->normalize_assistant_text( $text ) );
    }

    private function insert_job( $job_type, $title, $prompt_text, $schedule, $payload, $context ) {
        global $wpdb;

        $context    = is_array( $context ) ? $context : array();
        $payload    = is_array( $payload ) ? $payload : array();
        $chat_id    = isset( $context['chat_id'] ) ? trim( (string) $context['chat_id'] ) : $this->get_owner_chat_id();
        $session_id = isset( $context['session_id'] ) ? trim( (string) $context['session_id'] ) : '';
        $job_type   = sanitize_key( (string) $job_type );
        $dedupe_key = $this->build_job_dedupe_key(
            $job_type,
            $chat_id,
            $session_id,
            isset( $schedule['schedule'] ) ? $schedule['schedule'] : array(),
            $payload
        );
        $lock_key   = $this->acquire_job_creation_lock( $dedupe_key );

        if ( '' === $lock_key ) {
            $existing = $this->find_equivalent_open_job( $job_type, $chat_id, $session_id, $dedupe_key );
            if ( ! empty( $existing ) ) {
                return $this->duplicate_job_result( $existing );
            }

            return array(
                'success' => false,
                'error'   => 'Scheduler is already creating this job. Retry the request instead of creating another copy.',
            );
        }

        try {
            $existing = $this->find_equivalent_open_job( $job_type, $chat_id, $session_id, $dedupe_key );
            if ( ! empty( $existing ) ) {
                return $this->duplicate_job_result( $existing );
            }

            $now                              = current_time( 'mysql', true );
            $job_uuid                         = function_exists( 'wp_generate_uuid4' ) ? wp_generate_uuid4() : uniqid( 'niwa_job_', true );
            $context['_scheduler_dedupe_key'] = $dedupe_key;
            $data                             = array(
                'job_uuid'      => sanitize_text_field( $job_uuid ),
                'job_type'      => $job_type,
                'title'         => $this->sanitize_title_text( $title ),
                'prompt_text'   => (string) $prompt_text,
                'chat_id'       => $chat_id,
                'session_id'    => $session_id,
                'schedule_kind' => sanitize_key( (string) $schedule['schedule']['kind'] ),
                'timezone'      => (string) $schedule['schedule']['timezone'],
                'schedule_json' => wp_json_encode( $schedule['schedule'] ),
                'payload_json'  => wp_json_encode( $payload ),
                'context_json'  => wp_json_encode( $context ),
                'status'        => 'active',
                'next_run_at'   => (string) $schedule['next_run_at_utc'],
                'created_at'    => $now,
                'updated_at'    => $now,
            );

            $inserted = $wpdb->insert(
                $this->get_table_name(),
                $data,
                array( '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s' )
            );

            if ( false === $inserted ) {
                return array(
                    'success' => false,
                    'error'   => 'Upis zakazanog posla nije uspeo.',
                );
            }

            $job = $this->get_job_by_id( intval( $wpdb->insert_id ) );
            $this->ensure_job_processing_event( $job );
        } finally {
            $this->release_job_creation_lock( $lock_key );
        }

        return array(
            'success' => true,
            'job'     => $this->format_job_for_output( $job ),
        );
    }

    private function duplicate_job_result( $job ) {
        return array(
            'success'              => true,
            'duplicate_suppressed' => true,
            'job'                  => $this->format_job_for_output( $job ),
        );
    }

    private function find_equivalent_open_job( $job_type, $chat_id, $session_id, $dedupe_key ) {
        global $wpdb;

        if ( ! method_exists( $wpdb, 'get_results' ) ) {
            return array();
        }

        $sql  = "SELECT * FROM {$this->get_table_name()} WHERE job_type = %s AND chat_id = %s AND session_id = %s AND status IN ('active', 'running', 'actor_pending', 'extension_pending') ORDER BY id ASC LIMIT 100";
        $rows = $wpdb->get_results( $wpdb->prepare( $sql, $job_type, $chat_id, $session_id ), ARRAY_A );
        foreach ( is_array( $rows ) ? $rows : array() as $row ) {
            $job        = $this->hydrate_row( $row );
            $context    = isset( $job['context'] ) && is_array( $job['context'] ) ? $job['context'] : array();
            $stored_key = isset( $context['_scheduler_dedupe_key'] ) ? trim( (string) $context['_scheduler_dedupe_key'] ) : '';
            if ( '' === $stored_key ) {
                $stored_key = $this->build_job_dedupe_key(
                    isset( $job['job_type'] ) ? $job['job_type'] : '',
                    isset( $job['chat_id'] ) ? $job['chat_id'] : '',
                    isset( $job['session_id'] ) ? $job['session_id'] : '',
                    isset( $job['schedule'] ) ? $job['schedule'] : array(),
                    isset( $job['payload'] ) ? $job['payload'] : array()
                );
            }
            if ( hash_equals( $dedupe_key, $stored_key ) ) {
                return $job;
            }
        }

        return array();
    }

    private function build_job_dedupe_key( $job_type, $chat_id, $session_id, $schedule, $payload ) {
        $schedule = is_array( $schedule ) ? $schedule : array();
        $kind     = isset( $schedule['kind'] ) ? sanitize_key( (string) $schedule['kind'] ) : '';
        $identity = array( 'kind' => $kind );

        if ( 'interval' === $kind ) {
            $identity['interval_seconds'] = intval( isset( $schedule['interval_seconds'] ) ? $schedule['interval_seconds'] : 0 );
        } else {
            $identity['timezone'] = isset( $schedule['timezone'] ) ? (string) $schedule['timezone'] : '';
            foreach ( array( 'run_at_local', 'hour', 'minute', 'weekdays' ) as $key ) {
                if ( array_key_exists( $key, $schedule ) ) {
                    $identity[ $key ] = $schedule[ $key ];
                }
            }
        }
        if ( array_key_exists( 'prewake_minutes', $schedule ) ) {
            $identity['prewake_minutes'] = intval( $schedule['prewake_minutes'] );
        }

        $canonical = $this->canonicalize_dedupe_value(
            array(
                'job_type'   => sanitize_key( (string) $job_type ),
                'chat_id'    => trim( (string) $chat_id ),
                'session_id' => trim( (string) $session_id ),
                'schedule'   => $identity,
                'payload'    => is_array( $payload ) ? $payload : array(),
            )
        );

        return hash( 'sha256', (string) wp_json_encode( $canonical ) );
    }

    private function canonicalize_dedupe_value( $value ) {
        if ( ! is_array( $value ) ) {
            return $value;
        }

        $keys    = array_keys( $value );
        $is_list = empty( $keys ) || $keys === range( 0, count( $keys ) - 1 );
        if ( ! $is_list ) {
            ksort( $value, SORT_STRING );
        }
        foreach ( $value as $key => $item ) {
            $value[ $key ] = $this->canonicalize_dedupe_value( $item );
        }

        return $value;
    }

    private function acquire_job_creation_lock( $dedupe_key ) {
        if ( ! function_exists( 'add_option' ) ) {
            return '';
        }

        $lock_key = 'niwa_ai_admin_job_create_' . substr( (string) $dedupe_key, 0, 32 );
        $now      = time();
        if ( add_option( $lock_key, (string) $now, '', 'no' ) ) {
            return $lock_key;
        }

        $existing = function_exists( 'get_option' ) ? intval( get_option( $lock_key, 0 ) ) : 0;
        if ( $existing > 0 && $existing < ( $now - self::JOB_CREATION_LOCK_TTL ) && function_exists( 'delete_option' ) ) {
            delete_option( $lock_key );
            if ( add_option( $lock_key, (string) $now, '', 'no' ) ) {
                return $lock_key;
            }
        }

        return '';
    }

    private function release_job_creation_lock( $lock_key ) {
        $lock_key = trim( (string) $lock_key );
        if ( '' !== $lock_key && function_exists( 'delete_option' ) ) {
            delete_option( $lock_key );
        }
    }

    private function build_schedule_payload( $args ) {
        $args     = is_array( $args ) ? $args : array();
        $timezone = $this->get_timezone_string( isset( $args['timezone'] ) ? (string) $args['timezone'] : '' );
        $kind     = isset( $args['schedule_kind'] ) ? sanitize_key( (string) $args['schedule_kind'] ) : '';
        if ( '' === $kind ) {
            if ( array_key_exists( 'interval_seconds', $args ) || array_key_exists( 'interval_minutes', $args ) || array_key_exists( 'interval_hours', $args ) ) {
                $kind = 'interval';
            } elseif ( ! empty( $args['run_at_local'] ) ) {
                $kind = 'once';
            } elseif ( ! empty( $args['weekdays'] ) ) {
                $kind = 'weekly';
            } elseif ( array_key_exists( 'hour', $args ) || array_key_exists( 'minute', $args ) ) {
                $kind = 'daily';
            } else {
                return array(
                    'success' => false,
                    'error'   => 'Zakazani posao mora da ima jasan termin ili recurring schedule.',
                );
            }
        }

        $schedule  = array(
            'kind'     => $kind,
            'timezone' => $timezone,
        );
        $tz        = $this->get_timezone( $timezone );
        $now_local = new DateTimeImmutable( 'now', $tz );
        $next_local = null;

        if ( 'once' === $kind ) {
            $run_at_local = isset( $args['run_at_local'] ) ? trim( (string) $args['run_at_local'] ) : '';
            $next_local   = $this->parse_local_datetime( $run_at_local, $tz );
            if ( ! $next_local ) {
                return array(
                    'success' => false,
                    'error'   => 'run_at_local mora biti datum/vreme u formatu YYYY-MM-DD HH:MM.',
                );
            }
            if ( $next_local <= $now_local ) {
                return array(
                    'success' => false,
                    'error'   => 'Zakazano vreme mora biti u buducnosti.',
                );
            }
            $schedule['run_at_local'] = $next_local->format( 'Y-m-d H:i:s' );
        } elseif ( 'interval' === $kind ) {
            $interval_seconds = $this->normalize_interval_seconds( $args );
            if ( $interval_seconds < 30 ) {
                return array(
                    'success' => false,
                    'error'   => 'Interval schedule zahteva interval_seconds, interval_minutes ili interval_hours od najmanje 30 sekundi.',
                );
            }

            $run_at_local = isset( $args['run_at_local'] ) ? trim( (string) $args['run_at_local'] ) : '';
            if ( '' === $run_at_local && isset( $args['start_at_local'] ) ) {
                $run_at_local = trim( (string) $args['start_at_local'] );
            }

            if ( '' !== $run_at_local ) {
                $next_local = $this->parse_local_datetime( $run_at_local, $tz );
                if ( ! $next_local ) {
                    return array(
                        'success' => false,
                        'error'   => 'start_at_local/run_at_local mora biti datum/vreme u formatu YYYY-MM-DD HH:MM.',
                    );
                }
                if ( $next_local <= $now_local ) {
                    return array(
                        'success' => false,
                        'error'   => 'Prvo intervalno zakazano vreme mora biti u buducnosti.',
                    );
                }
                $schedule['start_at_local'] = $next_local->format( 'Y-m-d H:i:s' );
            } else {
                $next_local = $this->next_interval_occurrence( $now_local, $interval_seconds );
            }

            $schedule['interval_seconds'] = $interval_seconds;
            $schedule['interval_minutes'] = round( $interval_seconds / 60, 4 );
        } elseif ( 'daily' === $kind ) {
            $hour               = max( 0, min( 23, intval( isset( $args['hour'] ) ? $args['hour'] : 0 ) ) );
            $minute             = max( 0, min( 59, intval( isset( $args['minute'] ) ? $args['minute'] : 0 ) ) );
            $schedule['hour']   = $hour;
            $schedule['minute'] = $minute;
            $next_local         = $this->next_daily_occurrence( $now_local, $hour, $minute );
        } elseif ( 'weekly' === $kind ) {
            $hour      = max( 0, min( 23, intval( isset( $args['hour'] ) ? $args['hour'] : 0 ) ) );
            $minute    = max( 0, min( 59, intval( isset( $args['minute'] ) ? $args['minute'] : 0 ) ) );
            $weekdays  = $this->normalize_weekdays( isset( $args['weekdays'] ) ? $args['weekdays'] : array() );
            if ( empty( $weekdays ) ) {
                return array(
                    'success' => false,
                    'error'   => 'Weekly schedule zahteva weekdays.',
                );
            }
            $schedule['hour']     = $hour;
            $schedule['minute']   = $minute;
            $schedule['weekdays'] = $weekdays;
            $next_local           = $this->next_weekly_occurrence( $now_local, $weekdays, $hour, $minute );
        } else {
            return array(
                'success' => false,
                'error'   => 'Nepodrzan schedule_kind. Koristi once, interval, daily ili weekly.',
            );
        }

        $prewake_minutes = isset( $args['prewake_minutes'] ) ? max( 0, min( 180, intval( $args['prewake_minutes'] ) ) ) : 0;
        if ( $prewake_minutes > 0 && $next_local ) {
            $schedule['prewake_minutes']      = $prewake_minutes;
            $schedule['delivery_target_local'] = $next_local->format( 'Y-m-d H:i:s' );
            if ( in_array( $kind, array( 'once', 'daily', 'weekly' ), true ) ) {
                $wake_local = $next_local->modify( '-' . $prewake_minutes . ' minutes' );
                if ( $wake_local <= $now_local ) {
                    $wake_local = 'once' === $kind
                        ? $now_local->modify( '+30 seconds' )
                        : $this->next_prewake_occurrence_after( $schedule, $tz, $now_local );
                }
                if ( $wake_local ) {
                    $next_local = $wake_local;
                }
            }
        }

        return array(
            'success'         => true,
            'schedule'        => $schedule,
            'next_run_local'  => $next_local ? $next_local->format( 'Y-m-d H:i:s' ) : '',
            'next_run_at_utc' => $next_local ? $this->local_datetime_to_utc_mysql( $next_local ) : '',
        );
    }

    private function finalize_scheduled_run_failure( $run, $executed, $rollback, $error ) {
        if ( empty( $run['id'] ) || ! $this->store ) {
            return;
        }

        $this->store->mark_execution_result(
            intval( $run['id'] ),
            'failed',
            array(
                'approval_state'   => 'approved',
                'executed_actions' => $executed,
                'rollback_entries' => $rollback,
                'rollback_status'  => 'none',
                'last_error'       => (string) $error,
            )
        );
    }

    private function rollback_if_needed( $rollback_entries ) {
        if ( empty( $rollback_entries ) || ! $this->admin_tools || ! method_exists( $this->admin_tools, 'rollback_entries' ) ) {
            return array( 'success' => true );
        }

        return $this->admin_tools->rollback_entries( $rollback_entries );
    }

    private function is_scheduler_control_tool( $tool_name ) {
        return in_array(
            sanitize_key( trim( (string) $tool_name ) ),
            array(
                'schedule_admin_plan',
                'schedule_self_wake',
                'schedule_reminder',
                'schedule_orders_export_report',
                'scheduled_tasks_cancel',
            ),
            true
        );
    }

    private function build_scheduler_control_tool_payload( $tool_name ) {
        $tool_name = sanitize_key( trim( (string) $tool_name ) );

        return array(
            'success'        => false,
            'ok'             => false,
            'error_code'     => 'scheduler_control_tool_not_admin_plan',
            'blocked_tool'   => $tool_name,
            'error'          => 'Scheduler/control tool ne ide unutar frozen admin_plan: ' . $tool_name,
            'safe_user_message' => 'Taj scheduler/control korak treba pozvati direktno sada, a ne pakovati ga kao frozen admin_plan akciju. Za buduce razmisljanje koristi schedule_self_wake.',
        );
    }

    private function compute_next_run_after_execution( $schedule, DateTimeZone $timezone ) {
        $schedule  = is_array( $schedule ) ? $schedule : array();
        $kind      = isset( $schedule['kind'] ) ? (string) $schedule['kind'] : 'once';
        $now_local = new DateTimeImmutable( 'now', $timezone );
        $prewake_minutes = isset( $schedule['prewake_minutes'] ) ? max( 0, intval( $schedule['prewake_minutes'] ) ) : 0;

        if ( 'once' === $kind ) {
            return null;
        }
        if ( 'daily' === $kind ) {
            if ( $prewake_minutes > 0 ) {
                return $this->next_prewake_occurrence_after( $schedule, $timezone, $now_local );
            }
            return $this->next_daily_occurrence( $now_local, intval( isset( $schedule['hour'] ) ? $schedule['hour'] : 0 ), intval( isset( $schedule['minute'] ) ? $schedule['minute'] : 0 ) );
        }
        if ( 'weekly' === $kind ) {
            if ( $prewake_minutes > 0 ) {
                return $this->next_prewake_occurrence_after( $schedule, $timezone, $now_local );
            }
            return $this->next_weekly_occurrence( $now_local, isset( $schedule['weekdays'] ) ? (array) $schedule['weekdays'] : array(), intval( isset( $schedule['hour'] ) ? $schedule['hour'] : 0 ), intval( isset( $schedule['minute'] ) ? $schedule['minute'] : 0 ) );
        }
        if ( 'interval' === $kind ) {
            return $this->next_interval_occurrence( $now_local, intval( isset( $schedule['interval_seconds'] ) ? $schedule['interval_seconds'] : 0 ) );
        }

        return null;
    }

    private function next_prewake_occurrence_after( $schedule, DateTimeZone $timezone, DateTimeImmutable $from ) {
        $schedule        = is_array( $schedule ) ? $schedule : array();
        $kind            = isset( $schedule['kind'] ) ? (string) $schedule['kind'] : '';
        $prewake_minutes = isset( $schedule['prewake_minutes'] ) ? max( 0, intval( $schedule['prewake_minutes'] ) ) : 0;
        if ( $prewake_minutes <= 0 ) {
            return null;
        }

        $cursor = $from;
        for ( $i = 0; $i < 10; $i++ ) {
            if ( 'daily' === $kind ) {
                $target = $this->next_daily_occurrence( $cursor, intval( isset( $schedule['hour'] ) ? $schedule['hour'] : 0 ), intval( isset( $schedule['minute'] ) ? $schedule['minute'] : 0 ) );
            } elseif ( 'weekly' === $kind ) {
                $target = $this->next_weekly_occurrence( $cursor, isset( $schedule['weekdays'] ) ? (array) $schedule['weekdays'] : array(), intval( isset( $schedule['hour'] ) ? $schedule['hour'] : 0 ), intval( isset( $schedule['minute'] ) ? $schedule['minute'] : 0 ) );
            } else {
                return null;
            }

            if ( ! $target ) {
                return null;
            }

            $wake = $target->modify( '-' . $prewake_minutes . ' minutes' );
            if ( $wake > $from ) {
                return $wake;
            }

            $cursor = $target->modify( '+1 second' );
        }

        return null;
    }

    private function normalize_interval_seconds( $args ) {
        $args = is_array( $args ) ? $args : array();

        if ( array_key_exists( 'interval_seconds', $args ) ) {
            return max( 0, intval( $args['interval_seconds'] ) );
        }
        if ( array_key_exists( 'interval_minutes', $args ) ) {
            return max( 0, intval( round( floatval( $args['interval_minutes'] ) * 60 ) ) );
        }
        if ( array_key_exists( 'interval_hours', $args ) ) {
            return max( 0, intval( round( floatval( $args['interval_hours'] ) * 3600 ) ) );
        }

        return 0;
    }

    private function next_interval_occurrence( DateTimeImmutable $from, $interval_seconds ) {
        $interval_seconds = max( 30, intval( $interval_seconds ) );

        return $from->modify( '+' . $interval_seconds . ' seconds' );
    }

    private function next_daily_occurrence( DateTimeImmutable $from, $hour, $minute ) {
        $candidate = $from->setTime( max( 0, min( 23, intval( $hour ) ) ), max( 0, min( 59, intval( $minute ) ) ), 0 );
        if ( $candidate <= $from ) {
            $candidate = $candidate->modify( '+1 day' );
        }

        return $candidate;
    }

    private function next_weekly_occurrence( DateTimeImmutable $from, $weekdays, $hour, $minute ) {
        $weekdays = $this->normalize_weekdays( $weekdays );
        if ( empty( $weekdays ) ) {
            return null;
        }

        for ( $offset = 0; $offset < 8; $offset++ ) {
            $candidate = $from->modify( '+' . $offset . ' day' )->setTime( max( 0, min( 23, intval( $hour ) ) ), max( 0, min( 59, intval( $minute ) ) ), 0 );
            $weekday   = strtolower( $candidate->format( 'D' ) );
            if ( in_array( $weekday, $weekdays, true ) && $candidate > $from ) {
                return $candidate;
            }
        }

        return $from->modify( '+7 day' )->setTime( max( 0, min( 23, intval( $hour ) ) ), max( 0, min( 59, intval( $minute ) ) ), 0 );
    }

    private function normalize_weekdays( $weekdays ) {
        $map = array(
            '1' => 'mon',
            '2' => 'tue',
            '3' => 'wed',
            '4' => 'thu',
            '5' => 'fri',
            '6' => 'sat',
            '7' => 'sun',
            'mon' => 'mon',
            'monday' => 'mon',
            'tue' => 'tue',
            'tuesday' => 'tue',
            'wed' => 'wed',
            'wednesday' => 'wed',
            'thu' => 'thu',
            'thursday' => 'thu',
            'fri' => 'fri',
            'friday' => 'fri',
            'sat' => 'sat',
            'saturday' => 'sat',
            'sun' => 'sun',
            'sunday' => 'sun',
        );

        $normalized = array();
        foreach ( (array) $weekdays as $weekday ) {
            $key = strtolower( trim( (string) $weekday ) );
            if ( isset( $map[ $key ] ) ) {
                $normalized[] = $map[ $key ];
            }
        }

        return array_values( array_unique( $normalized ) );
    }

    private function parse_local_datetime( $value, DateTimeZone $timezone ) {
        $value = trim( (string) $value );
        if ( '' === $value ) {
            return null;
        }

        $formats = array(
            'Y-m-d H:i:s',
            'Y-m-d H:i',
            DateTimeInterface::ATOM,
            'Y-m-d\TH:i:s',
            'Y-m-d\TH:i',
        );

        foreach ( $formats as $format ) {
            $dt = DateTimeImmutable::createFromFormat( $format, $value, $timezone );
            if ( $dt instanceof DateTimeImmutable ) {
                return $dt;
            }
        }

        try {
            return new DateTimeImmutable( $value, $timezone );
        } catch ( Throwable $e ) {
            return null;
        }
    }

    private function local_datetime_to_utc_mysql( DateTimeImmutable $datetime ) {
        return $datetime->setTimezone( new DateTimeZone( 'UTC' ) )->format( 'Y-m-d H:i:s' );
    }

    private function utc_mysql_to_local_datetime( $utc_mysql, DateTimeZone $timezone ) {
        $utc_mysql = trim( (string) $utc_mysql );
        if ( '' === $utc_mysql ) {
            return null;
        }

        try {
            $utc = new DateTimeImmutable( $utc_mysql, new DateTimeZone( 'UTC' ) );
            return $utc->setTimezone( $timezone );
        } catch ( Throwable $e ) {
            return null;
        }
    }

    private function ensure_job_processing_event( $job ) {
        if ( empty( $job ) || ! is_array( $job ) || empty( $job['next_run_at'] ) ) {
            $this->ensure_processing_event();
            return;
        }

        if ( isset( $job['status'] ) && 'active' !== (string) $job['status'] ) {
            $this->cleanup_targeted_processing_event( $job );
            return;
        }

        $timestamp = $this->get_job_due_timestamp( $job );
        if ( $timestamp > 0 ) {
            $this->ensure_targeted_processing_event( $job, max( time() + 5, $timestamp ) );
        }

        $this->ensure_processing_event( (string) $job['next_run_at'], self::TARGETED_JOB_FALLBACK_DELAY, $this->get_fallback_processing_event_args( $job ) );
    }

    private function ensure_processing_event( $next_run_at_utc = '', $delay_after_due = 0, $event_args = array( 5 ) ) {
        $timestamp       = time() + 5;
        $next_run_at_utc = trim( (string) $next_run_at_utc );
        $delay_after_due = max( 0, intval( $delay_after_due ) );
        $event_args      = is_array( $event_args ) && ! empty( $event_args ) ? array_values( $event_args ) : array( 5 );

        if ( '' !== $next_run_at_utc ) {
            try {
                $due       = new DateTimeImmutable( $next_run_at_utc, new DateTimeZone( 'UTC' ) );
                $timestamp = max( time() + 5, $due->getTimestamp() + $delay_after_due );
            } catch ( Throwable $e ) {
                $timestamp = time() + 5;
            }
        }

        if ( function_exists( 'wp_schedule_single_event' ) ) {
            if ( function_exists( 'wp_next_scheduled' ) ) {
                $existing = wp_next_scheduled( 'niwa_ai_admin_autonomy_process_scheduled_tasks', $event_args );
                if ( false !== $existing && abs( intval( $existing ) - $timestamp ) <= 1 ) {
                    return;
                }
                if ( false !== $existing ) {
                    $this->cleanup_scheduled_hook_events( 'niwa_ai_admin_autonomy_process_scheduled_tasks', $event_args );
                }
            }

            wp_schedule_single_event(
                $timestamp,
                'niwa_ai_admin_autonomy_process_scheduled_tasks',
                $event_args
            );
        }

        if ( function_exists( 'do_action' ) ) {
            do_action( 'niwa_ai_admin_autonomy_wake_background', 'scheduled-job' );
        }
    }

    private function ensure_targeted_processing_event( $job, $timestamp ) {
        if ( ! function_exists( 'wp_schedule_single_event' ) || ! function_exists( 'wp_next_scheduled' ) ) {
            return;
        }

        $args = $this->get_targeted_processing_event_args( $job );
        if ( empty( $args ) ) {
            return;
        }

        $timestamp = max( time() + 5, intval( $timestamp ) );
        $existing  = wp_next_scheduled( self::TARGETED_JOB_HOOK, $args );
        if ( false !== $existing && abs( intval( $existing ) - $timestamp ) <= 1 ) {
            return;
        }

        $this->cleanup_targeted_processing_event_args( $args[0], $args[1] );
        wp_schedule_single_event( $timestamp, self::TARGETED_JOB_HOOK, $args );
    }

    private function cleanup_recent_terminal_targeted_events() {
        global $wpdb;

        if ( ! function_exists( 'wp_next_scheduled' ) || ! function_exists( 'wp_unschedule_event' ) ) {
            return;
        }

        $rows = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT * FROM {$this->get_table_name()} WHERE status IN ('completed', 'cancelled', 'failed') ORDER BY updated_at DESC LIMIT %d",
                25
            ),
            ARRAY_A
        );

        foreach ( (array) $rows as $row ) {
            $this->cleanup_targeted_processing_event( $this->hydrate_row( $row ) );
        }
    }

    private function recover_stale_running_jobs() {
        global $wpdb;

        $cutoff = gmdate( 'Y-m-d H:i:s', time() - self::JOB_EXECUTION_LOCK_TTL );
        $rows   = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT * FROM {$this->get_table_name()} WHERE status = 'running' AND updated_at < %s ORDER BY updated_at ASC LIMIT %d",
                $cutoff,
                5
            ),
            ARRAY_A
        );

        foreach ( (array) $rows as $row ) {
            $job     = $this->hydrate_row( $row );
            $updated = $this->update_job_row(
                intval( isset( $job['id'] ) ? $job['id'] : 0 ),
                array(
                    'status'      => 'active',
                    'next_run_at' => current_time( 'mysql', true ),
                    'last_error'  => 'running_job_timeout',
                )
            );
            if ( ! empty( $updated ) ) {
                $this->ensure_job_processing_event( $updated );
            }
        }
    }

    private function recover_stale_actor_pending_jobs() {
        global $wpdb;

        $cutoff = gmdate( 'Y-m-d H:i:s', time() - max( self::JOB_EXECUTION_LOCK_TTL, self::SCHEDULED_ACTOR_RETRY_DELAY ) );
        $rows   = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT * FROM {$this->get_table_name()} WHERE status = 'actor_pending' AND updated_at < %s ORDER BY updated_at ASC LIMIT %d",
                $cutoff,
                5
            ),
            ARRAY_A
        );

        foreach ( (array) $rows as $row ) {
            $job   = $this->hydrate_row( $row );
            $actor = $this->find_scheduled_actor_message_for_job( $job );
            if ( ! empty( $actor['found'] ) ) {
                $state            = isset( $actor['state'] ) ? (string) $actor['state'] : '';
                $lease_expires_at = intval( isset( $actor['lease_expires_at'] ) ? $actor['lease_expires_at'] : 0 );
                if ( 'pending' === $state || $lease_expires_at > time() + 5 ) {
                    continue;
                }

                $this->recover_orphaned_actor_messages_for_scheduler();
                $actor_after_recovery = $this->find_scheduled_actor_message_for_job( $job );
                if ( ! empty( $actor_after_recovery['found'] ) ) {
                    continue;
                }
            }

            $job     = $this->get_job_by_id( intval( isset( $job['id'] ) ? $job['id'] : 0 ) );
            if ( empty( $job ) || 'actor_pending' !== ( isset( $job['status'] ) ? (string) $job['status'] : '' ) ) {
                continue;
            }
            $updated = $this->update_job_row(
                intval( isset( $job['id'] ) ? $job['id'] : 0 ),
                array(
                    'status'      => 'active',
                    'next_run_at' => current_time( 'mysql', true ),
                    'last_error'  => 'actor_result_timeout',
                )
            );
            if ( ! empty( $updated ) ) {
                $this->ensure_job_processing_event( $updated );
            }
        }
    }

    private function find_scheduled_actor_message_for_job( $job ) {
        $job = is_array( $job ) ? $job : array();
        if ( empty( $job['id'] ) || empty( $job['job_uuid'] ) || empty( $job['session_id'] ) ) {
            return array( 'found' => false );
        }

        $context       = isset( $job['context'] ) && is_array( $job['context'] ) ? $job['context'] : array();
        $attempt_token = isset( $context['scheduled_actor_attempt_token'] ) ? trim( (string) $context['scheduled_actor_attempt_token'] ) : '';

        return $this->call(
            'find_scheduled_actor_message',
            array(
                (string) $job['session_id'],
                intval( $job['id'] ),
                (string) $job['job_uuid'],
                $attempt_token,
            ),
            array( 'found' => false )
        );
    }

    private function recover_orphaned_actor_messages_for_scheduler() {
        return $this->call(
            'recover_orphaned_actor_messages',
            array(
                'Scheduled actor message lease expired while scheduler was waiting for a result.',
                50,
                1,
                false,
            ),
            array(
                'requeued'     => 0,
                'cancelled'    => 0,
                'skipped_live' => 0,
                'sessions'     => array(),
            )
        );
    }

    private function cleanup_targeted_processing_event( $job ) {
        $args = $this->get_targeted_processing_event_args( $job );
        if ( empty( $args ) ) {
            return;
        }

        $this->cleanup_targeted_processing_event_args( $args[0], $args[1] );
    }

    private function cleanup_targeted_processing_event_args( $job_id, $job_uuid ) {
        if ( ! function_exists( 'wp_next_scheduled' ) || ! function_exists( 'wp_unschedule_event' ) ) {
            return;
        }

        $targeted_args = array( intval( $job_id ), (string) $job_uuid );
        $fallback_args = array( 5, 'scheduled-job-fallback', intval( $job_id ), (string) $job_uuid );

        $this->cleanup_scheduled_hook_events( self::TARGETED_JOB_HOOK, $targeted_args );
        $this->cleanup_scheduled_hook_events( 'niwa_ai_admin_autonomy_process_scheduled_tasks', $fallback_args );
    }

    private function cleanup_scheduled_hook_events( $hook, $args ) {
        if ( ! function_exists( 'wp_next_scheduled' ) || ! function_exists( 'wp_unschedule_event' ) ) {
            return;
        }

        $hook = (string) $hook;
        $args = is_array( $args ) ? array_values( $args ) : array();
        for ( $i = 0; $i < 10; $i++ ) {
            $next = wp_next_scheduled( $hook, $args );
            if ( false === $next ) {
                break;
            }

            wp_unschedule_event( intval( $next ), $hook, $args );
        }
    }

    private function should_runner_defer_to_targeted_event( $job ) {
        if ( empty( $job ) || ! is_array( $job ) ) {
            return false;
        }

        $scheduled = $this->get_scheduled_targeted_event_timestamp( $job );
        if ( $scheduled < 1 ) {
            return false;
        }

        if ( $scheduled >= time() - self::TARGETED_JOB_RUNNER_GRACE ) {
            return true;
        }

        $this->cleanup_targeted_processing_event( $job );
        return false;
    }

    private function get_scheduled_targeted_event_timestamp( $job ) {
        if ( ! function_exists( 'wp_next_scheduled' ) ) {
            return 0;
        }

        $args = $this->get_targeted_processing_event_args( $job );
        if ( empty( $args ) ) {
            return 0;
        }

        $scheduled = wp_next_scheduled( self::TARGETED_JOB_HOOK, $args );
        return false === $scheduled ? 0 : intval( $scheduled );
    }

    private function get_targeted_processing_event_args( $job ) {
        if ( empty( $job ) || ! is_array( $job ) || empty( $job['id'] ) || empty( $job['job_uuid'] ) ) {
            return array();
        }

        return array( intval( $job['id'] ), (string) $job['job_uuid'] );
    }

    private function get_fallback_processing_event_args( $job ) {
        $targeted_args = $this->get_targeted_processing_event_args( $job );
        if ( empty( $targeted_args ) ) {
            return array( 5 );
        }

        return array( 5, 'scheduled-job-fallback', $targeted_args[0], $targeted_args[1] );
    }

    private function acquire_job_execution_lock( $job ) {
        $key = $this->get_job_execution_lock_key( $job );
        if ( '' === $key || ! function_exists( 'add_option' ) ) {
            return '';
        }

        $now = time();
        if ( add_option( $key, (string) $now, '', 'no' ) ) {
            return $key;
        }

        $existing = function_exists( 'get_option' ) ? intval( get_option( $key, 0 ) ) : 0;
        if ( $existing > 0 && $existing < ( $now - self::JOB_EXECUTION_LOCK_TTL ) && function_exists( 'delete_option' ) ) {
            delete_option( $key );
            if ( add_option( $key, (string) $now, '', 'no' ) ) {
                return $key;
            }
        }

        return '';
    }

    private function release_job_execution_lock( $lock_key ) {
        $lock_key = trim( (string) $lock_key );
        if ( '' === $lock_key || ! function_exists( 'delete_option' ) ) {
            return;
        }

        delete_option( $lock_key );
    }

    private function get_job_execution_lock_key( $job ) {
        if ( empty( $job ) || ! is_array( $job ) || empty( $job['id'] ) ) {
            return '';
        }

        $uuid = isset( $job['job_uuid'] ) ? (string) $job['job_uuid'] : '';
        return 'niwa_ai_admin_job_exec_lock_' . intval( $job['id'] ) . '_' . md5( $uuid );
    }

    private function is_terminal_job_status( $status ) {
        return in_array( (string) $status, array( 'completed', 'cancelled', 'failed' ), true );
    }

    private function is_job_due_now( $job ) {
        $timestamp = $this->get_job_due_timestamp( $job );
        return $timestamp > 0 && $timestamp <= time();
    }

    private function get_job_due_timestamp( $job ) {
        $next_run_at = isset( $job['next_run_at'] ) ? trim( (string) $job['next_run_at'] ) : '';
        if ( '' === $next_run_at ) {
            return 0;
        }

        try {
            $due = new DateTimeImmutable( $next_run_at, new DateTimeZone( 'UTC' ) );
            return $due->getTimestamp();
        } catch ( Throwable $e ) {
            return 0;
        }
    }

    private function get_timezone_string( $value = '' ) {
        $value = trim( (string) $value );
        if ( '' !== $value ) {
            return $value;
        }

        if ( function_exists( 'wp_timezone_string' ) ) {
            $wp_timezone = wp_timezone_string();
            if ( '' !== trim( (string) $wp_timezone ) ) {
                return (string) $wp_timezone;
            }
        }

        return 'UTC';
    }

    private function get_timezone( $value = '' ) {
        $timezone_string = $this->get_timezone_string( $value );
        try {
            return new DateTimeZone( $timezone_string );
        } catch ( Throwable $e ) {
            return new DateTimeZone( 'UTC' );
        }
    }

    private function resolve_job_chat_id( $job ) {
        $chat_id = isset( $job['chat_id'] ) ? trim( (string) $job['chat_id'] ) : '';
        if ( '' !== $chat_id ) {
            return $chat_id;
        }

        return $this->get_owner_chat_id();
    }

    private function is_admin_ui_job( $job ) {
        $context = isset( $job['context'] ) && is_array( $job['context'] ) ? $job['context'] : array();
        $channel = isset( $context['channel'] ) ? sanitize_key( (string) $context['channel'] ) : '';
        $session_id = isset( $job['session_id'] ) ? trim( (string) $job['session_id'] ) : '';

        return 'admin_ui' === $channel && '' !== $session_id;
    }

    private function deliver_admin_ui_message( $job, $message ) {
        $session_id = isset( $job['session_id'] ) ? trim( (string) $job['session_id'] ) : '';
        $message    = trim( (string) $message );
        if ( '' === $session_id || '' === $message ) {
            return array(
                'success' => false,
                'error'   => 'Admin UI delivery nema session ili poruku.',
            );
        }

        $context = isset( $job['context'] ) && is_array( $job['context'] ) ? $job['context'] : array();
        $meta    = array(
            'channel' => 'admin_ui',
            'source'  => 'scheduled_job',
            'job_id'  => intval( isset( $job['id'] ) ? $job['id'] : 0 ),
        );
        if ( ! empty( $context['admin_user_id'] ) ) {
            $meta['admin_user_id'] = intval( $context['admin_user_id'] );
        }
        if ( ! empty( $context['admin_user_login'] ) ) {
            $meta['admin_user_login'] = (string) $context['admin_user_login'];
        }

        if ( $this->conversation_store && method_exists( $this->conversation_store, 'append' ) ) {
            $this->conversation_store->append( $session_id, 'bot', $message, array(), $meta );
        }

        return array( 'success' => true );
    }

    private function get_owner_chat_id() {
        if ( ! $this->telegram || ! method_exists( $this->telegram, 'get_settings' ) ) {
            return '';
        }

        $settings = $this->telegram->get_settings();
        return isset( $settings['owner_chat_id'] ) ? trim( (string) $settings['owner_chat_id'] ) : '';
    }

    private function find_job( $job_id, $chat_id = '' ) {
        global $wpdb;

        $job_id  = trim( (string) $job_id );
        $chat_id = trim( (string) $chat_id );
        if ( '' === $job_id ) {
            return array();
        }

        if ( ctype_digit( $job_id ) ) {
            $sql    = "SELECT * FROM {$this->get_table_name()} WHERE id = %d";
            $params = array( intval( $job_id ) );
        } else {
            $sql    = "SELECT * FROM {$this->get_table_name()} WHERE job_uuid = %s OR job_uuid LIKE %s";
            $params = array( $job_id, $job_id . '%' );
        }

        if ( '' !== $chat_id ) {
            $sql     .= ' AND chat_id = %s';
            $params[] = $chat_id;
        }

        $sql .= ' ORDER BY id DESC LIMIT 1';
        $row  = $wpdb->get_row( $wpdb->prepare( $sql, $params ), ARRAY_A );

        return $this->hydrate_row( $row );
    }

    private function get_job_by_id( $job_id ) {
        global $wpdb;

        $row = $wpdb->get_row(
            $wpdb->prepare( "SELECT * FROM {$this->get_table_name()} WHERE id = %d LIMIT 1", intval( $job_id ) ),
            ARRAY_A
        );

        return $this->hydrate_row( $row );
    }

    private function claim_job( $job_id ) {
        global $wpdb;

        $updated = $wpdb->update(
            $this->get_table_name(),
            array(
                'status'     => 'running',
                'updated_at' => current_time( 'mysql', true ),
            ),
            array(
                'id'     => intval( $job_id ),
                'status' => 'active',
            ),
            array( '%s', '%s' ),
            array( '%d', '%s' )
        );

        return false !== $updated && $updated > 0;
    }

    private function update_job_row( $job_id, $data ) {
        global $wpdb;

        $payload = array();
        $formats = array();
        $map     = array(
            'status'          => '%s',
            'next_run_at'     => '%s',
            'last_run_at'     => '%s',
            'last_success_at' => '%s',
            'completed_at'    => '%s',
            'run_count'       => '%d',
            'last_error'      => '%s',
            'payload_json'     => '%s',
            'context_json'     => '%s',
        );

        foreach ( $map as $field => $format ) {
            if ( ! array_key_exists( $field, $data ) ) {
                continue;
            }
            $payload[ $field ] = $data[ $field ];
            $formats[]         = $format;
        }

        $payload['updated_at'] = current_time( 'mysql', true );
        $formats[]             = '%s';

        $updated = $wpdb->update(
            $this->get_table_name(),
            $payload,
            array( 'id' => intval( $job_id ) ),
            $formats,
            array( '%d' )
        );

        if ( false === $updated ) {
            return array();
        }

        return $this->get_job_by_id( $job_id );
    }

    private function hydrate_row( $row ) {
        if ( empty( $row ) || ! is_array( $row ) ) {
            return array();
        }

        $row['id']        = isset( $row['id'] ) ? intval( $row['id'] ) : 0;
        $row['run_count'] = isset( $row['run_count'] ) ? intval( $row['run_count'] ) : 0;
        $row['schedule']  = $this->decode_json( isset( $row['schedule_json'] ) ? $row['schedule_json'] : '' );
        $row['payload']   = $this->decode_json( isset( $row['payload_json'] ) ? $row['payload_json'] : '' );
        $row['context']   = $this->decode_json( isset( $row['context_json'] ) ? $row['context_json'] : '' );

        return $row;
    }

    private function format_job_for_output( $job, $include_prompt_text = false ) {
        if ( empty( $job ) ) {
            return array();
        }

        $timezone = $this->get_timezone( isset( $job['timezone'] ) ? (string) $job['timezone'] : '' );
        $next_run = $this->utc_mysql_to_local_datetime( isset( $job['next_run_at'] ) ? (string) $job['next_run_at'] : '', $timezone );

        $output = array(
            'id'             => intval( isset( $job['id'] ) ? $job['id'] : 0 ),
            'job_uuid'       => isset( $job['job_uuid'] ) ? (string) $job['job_uuid'] : '',
            'job_type'       => isset( $job['job_type'] ) ? (string) $job['job_type'] : '',
			'execution_lane' => 'self_wake' === ( isset( $job['job_type'] ) ? (string) $job['job_type'] : '' ) ? 'subagent' : 'native',
            'title'          => isset( $job['title'] ) ? (string) $job['title'] : '',
            'status'         => isset( $job['status'] ) ? (string) $job['status'] : '',
            'schedule_kind'  => isset( $job['schedule_kind'] ) ? (string) $job['schedule_kind'] : '',
            'timezone'       => isset( $job['timezone'] ) ? (string) $job['timezone'] : '',
            'next_run_local' => $next_run ? $next_run->format( 'Y-m-d H:i:s' ) : '',
            'delivery_target_local' => $this->derive_scheduled_delivery_target_local( $job ),
            'run_count'      => intval( isset( $job['run_count'] ) ? $job['run_count'] : 0 ),
            'last_error'     => isset( $job['last_error'] ) ? (string) $job['last_error'] : '',
        );

        if ( $include_prompt_text ) {
            $output['prompt_text'] = isset( $job['prompt_text'] ) ? (string) $job['prompt_text'] : '';
        }

        return $output;
    }

    private function sanitize_title_text( $title ) {
        $title = trim( sanitize_text_field( (string) $title ) );
        return '' !== $title ? $title : 'Zakazani zadatak';
    }

    private function decode_json( $value ) {
        if ( '' === (string) $value ) {
            return array();
        }

        $decoded = json_decode( (string) $value, true );
        return is_array( $decoded ) ? $decoded : array();
    }

    private function acquire_lock() {
        $lock_key = 'niwa_ai_admin_autonomy_scheduler_lock';
        if ( get_transient( $lock_key ) ) {
            return false;
        }

        set_transient( $lock_key, 1, 120 );
        return true;
    }

    private function release_lock() {
        delete_transient( 'niwa_ai_admin_autonomy_scheduler_lock' );
    }

    private function normalize_assistant_text( $text ) {
        if ( $this->workspace && method_exists( $this->workspace, 'normalize_assistant_text' ) ) {
            return $this->workspace->normalize_assistant_text( $text );
        }

        return (string) $text;
    }
}
