<?php
/**
 * Trait: Chat Heartbeat
 *
 * Heartbeat-related methods extracted from Niwa_AI_Admin_Autonomy_Chat.
 *
 * @package suspended
 */

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

trait Niwa_AI_Chat_Heartbeat {

	public function maybe_send_workspace_heartbeat( $force = false ) {
		$settings = $this->get_admin_autonomy_settings();
		if ( empty( $settings['enabled'] ) || ! $this->telegram ) {
			return array(
				'success' => true,
				'sent'    => false,
				'reason'  => 'disabled',
			);
		}

		$chat_id = trim( (string) $settings['owner_chat_id'] );
		if ( '' === $chat_id ) {
			return array(
				'success' => false,
				'sent'    => false,
				'reason'  => 'missing_chat',
				'error'   => 'Owner chat ID nije podesen za heartbeat.',
			);
		}

		if ( method_exists( $this->telegram, 'is_outbound_enabled' ) && ! $this->telegram->is_outbound_enabled( $chat_id ) ) {
			return array(
				'success' => true,
				'sent'    => false,
				'reason'  => 'telegram_outbound_disabled',
			);
		}

		$hb_config = $this->get_heartbeat_config();

		if ( ! empty( $hb_config['paused'] ) && ! $force ) {
			return array(
				'success' => true,
				'sent'    => false,
				'reason'  => 'paused',
			);
		}

		$lock_token = $this->acquire_workspace_heartbeat_lock();
		if ( '' === $lock_token ) {
			return array(
				'success' => true,
				'sent'    => false,
				'reason'  => 'workspace_heartbeat_already_running',
			);
		}

		try {
			return $this->maybe_send_workspace_heartbeat_unlocked( $chat_id, $hb_config, $force );
		} finally {
			$this->release_workspace_heartbeat_lock( $lock_token );
		}
	}

	private function maybe_send_workspace_heartbeat_unlocked( $chat_id, $hb_config, $force = false ) {
		$state = $this->get_heartbeat_state();
		$has_custom = $this->has_custom_heartbeat_instructions();

		if ( ! $force && ! $this->is_heartbeat_schedule_due( $hb_config, intval( isset( $state['last_checked_at'] ) ? $state['last_checked_at'] : 0 ) ) ) {
			return array(
				'success' => true,
				'sent'    => false,
				'reason'  => 'not_due',
			);
		}

		if ( $has_custom ) {
			return $this->run_custom_heartbeat( $chat_id, $state );
		}

		return $this->run_proactive_improvement_heartbeat( $chat_id, $state, $hb_config );
	}

	private function acquire_workspace_heartbeat_lock() {
		$key   = 'niwa_ai_workspace_hb_lock';
		$token = time() . ':' . wp_generate_uuid4();

		if ( add_option( $key, $token, '', false ) ) {
			return $token;
		}

		$existing = get_option( $key, '' );
		if ( $this->is_workspace_heartbeat_lock_stale( $existing ) ) {
			delete_option( $key );
			if ( add_option( $key, $token, '', false ) ) {
				return $token;
			}
		}

		return '';
	}

	private function release_workspace_heartbeat_lock( $token ) {
		$key     = 'niwa_ai_workspace_hb_lock';
		$current = get_option( $key, '' );
		if ( is_string( $current ) && hash_equals( $current, (string) $token ) ) {
			delete_option( $key );
		}
	}

	private function is_workspace_heartbeat_lock_stale( $value ) {
		if ( ! is_string( $value ) || '' === $value ) {
			return true;
		}

		$parts = explode( ':', $value, 2 );
		$ts    = intval( $parts[0] );
		if ( $ts <= 0 ) {
			return true;
		}

		return $ts < ( time() - 10 * MINUTE_IN_SECONDS );
	}

	private function has_custom_heartbeat_instructions() {
		if ( ! $this->workspace || ! method_exists( $this->workspace, 'is_heartbeat_effectively_empty' ) ) {
			return false;
		}

		if ( $this->workspace->is_heartbeat_effectively_empty() ) {
			return false;
		}

		$record = $this->workspace->read_bootstrap_file( 'HEARTBEAT.md' );
		if ( empty( $record['content'] ) ) {
			return false;
		}

		$content = trim( (string) $record['content'] );
		if ( false !== strpos( $content, 'proactive_improvement_mode: true' ) ) {
			return false;
		}

		return true;
	}

	private function run_custom_heartbeat( $chat_id, $state ) {
		$snapshot = $this->build_heartbeat_snapshot();
		$result   = $this->generate_ai_heartbeat_result( $snapshot );
		$retryable = $this->is_heartbeat_retryable_result( $result );
		if ( ! $retryable ) {
			$state['last_checked_at'] = time();
		}

		if ( empty( $result['sent'] ) ) {
			$reason = isset( $result['reason'] ) ? (string) $result['reason'] : 'heartbeat_ok';
			$state['last_status']  = $reason;
			$state['last_summary'] = ! empty( $result['error'] ) ? $this->truncate_block( (string) $result['error'], 280 ) : '';
			$this->save_heartbeat_state( $state );

			return $result;
		}

		$message = $this->prepare_workspace_heartbeat_message( isset( $result['message'] ) ? (string) $result['message'] : '' );
		$hash = md5( $message );
		if (
			! empty( $state['last_hash'] )
			&& hash_equals( (string) $state['last_hash'], $hash )
			&& intval( isset( $state['last_sent_at'] ) ? $state['last_sent_at'] : 0 ) > ( time() - 6 * HOUR_IN_SECONDS )
		) {
			$state['last_status']  = 'duplicate_suppressed';
			$state['last_summary'] = $this->truncate_block( $message, 280 );
			$this->save_heartbeat_state( $state );

			return array(
				'success' => true,
				'sent'    => false,
				'reason'  => 'duplicate_suppressed',
			);
		}

		$sent = $this->telegram->send_message( $chat_id, $message );
		if ( empty( $sent['success'] ) ) {
			$state['last_status']  = 'send_failed';
			$state['last_summary'] = '';
			$this->save_heartbeat_state( $state );

			return array(
				'success' => false,
				'sent'    => false,
				'reason'  => 'send_failed',
				'error'   => isset( $sent['error'] ) ? (string) $sent['error'] : 'Heartbeat send failed.',
			);
		}

		$state['last_sent_at'] = time();
		$state['last_status']  = 'sent';
		$state['last_hash']    = $hash;
		$state['last_summary'] = $this->truncate_block( $message, 280 );
		$this->save_heartbeat_state( $state );

		return array(
			'success' => true,
			'sent'    => true,
			'reason'  => 'sent',
			'message' => $message,
		);
	}

	private function run_proactive_improvement_heartbeat( $chat_id, $state, $hb_config ) {
		$task = $this->pick_heartbeat_task( $hb_config, $state );

		if ( 'action' === $task['type'] ) {
			$result = $this->execute_heartbeat_action_task( $task, $chat_id );
		} else {
			$result = $this->execute_heartbeat_analysis_task( $task, $chat_id );
		}

		$retryable = $this->is_heartbeat_retryable_result( $result );
		if ( ! $retryable ) {
			$state['last_checked_at'] = time();
		}
		$state['last_task']       = $task['id'];

		if ( ! empty( $result['sent'] ) ) {
			$state['last_sent_at'] = time();
			$state['last_status']  = 'sent';
			$state['last_hash']    = md5( isset( $result['message'] ) ? (string) $result['message'] : '' );
			$state['last_summary'] = $this->truncate_block( isset( $result['message'] ) ? (string) $result['message'] : '', 280 );

			$task_log   = isset( $hb_config['task_log'] ) && is_array( $hb_config['task_log'] ) ? $hb_config['task_log'] : array();
			$task_log[] = array(
				'task_id' => $task['id'],
				'at'      => time(),
			);
			$hb_config['task_log'] = array_slice( $task_log, -30 );
			$this->save_heartbeat_config( $hb_config );
		} else {
			$reason = isset( $result['reason'] ) ? (string) $result['reason'] : 'no_output';
			$state['last_status'] = $reason;

			// Preserve LLM error detail in summary for admin diagnostics.
			if ( 'llm_error' === $reason && ! empty( $result['llm_error'] ) ) {
				$state['last_summary'] = $this->truncate_block( (string) $result['llm_error'], 280 );
			} elseif ( ! empty( $result['error'] ) ) {
				$state['last_summary'] = $this->truncate_block( (string) $result['error'], 280 );
			} else {
				$state['last_summary'] = '';
			}
		}

		$this->save_heartbeat_state( $state );

		return $result;
	}

	private function is_heartbeat_retryable_result( $result ) {
		if ( ! is_array( $result ) ) {
			return false;
		}

		$reason = isset( $result['reason'] ) ? sanitize_key( (string) $result['reason'] ) : '';
		if ( 'partial_send' === $reason ) {
			return true;
		}

		if ( ! empty( $result['sent'] ) ) {
			return false;
		}
		if ( empty( $result['success'] ) ) {
			return in_array(
				$reason,
				array(
					'send_failed',
					'exception',
					'llm_error',
					'rate_limited',
					'timeout',
					'transport_failed',
					'telegram_unavailable',
				),
				true
			);
		}

		return in_array(
			$reason,
			array(
				'send_failed',
				'exception',
				'llm_error',
				'rate_limited',
				'timeout',
				'transport_failed',
				'telegram_unavailable',
			),
			true
		);
	}

	private function acquire_operative_heartbeat_lock( $role ) {
		$role       = $this->normalize_operative_heartbeat_role( $role );
		$option_key = 'stock_manager' === $role ? 'niwa_ai_stock_operative_hb_lock' : 'niwa_ai_operative_hb_lock';
		$token      = function_exists( 'wp_generate_uuid4' ) ? wp_generate_uuid4() : bin2hex( random_bytes( 16 ) );
		$now        = time();
		$ttl        = max( 120, ( method_exists( 'Niwa_AI_Admin_Autonomy', 'get_turn_worker_time_limit' ) ? intval( Niwa_AI_Admin_Autonomy::get_turn_worker_time_limit() ) : 420 ) + 90 );
		$payload    = array(
			'token'      => $token,
			'started_at' => $now,
			'role'       => $role,
		);

		if ( add_option( $option_key, $payload, '', false ) ) {
			return array(
				'success' => true,
				'key'     => $option_key,
				'token'   => $token,
			);
		}

		$current = get_option( $option_key, array() );
		$started = is_array( $current ) && isset( $current['started_at'] ) ? intval( $current['started_at'] ) : intval( $current );
		if ( $started > 0 && $started < ( $now - $ttl ) ) {
			$this->delete_option_if_current_value_matches( $option_key, $current );
			delete_transient( $option_key );
			if ( add_option( $option_key, $payload, '', false ) ) {
				return array(
					'success'       => true,
					'key'           => $option_key,
					'token'         => $token,
					'stale_cleared' => true,
				);
			}
		}

		return array(
			'success' => false,
			'key'     => $option_key,
			'error'   => 'locked',
		);
	}

	private function release_operative_heartbeat_lock( $lock ) {
		if ( ! is_array( $lock ) || empty( $lock['key'] ) ) {
			return;
		}

		$key     = (string) $lock['key'];
		$token   = isset( $lock['token'] ) ? (string) $lock['token'] : '';
		$current = get_option( $key, array() );
		if ( is_array( $current ) && isset( $current['token'] ) && '' !== $token && ! hash_equals( (string) $current['token'], $token ) ) {
			return;
		}

		$this->delete_option_if_current_value_matches( $key, $current );
		delete_transient( $key );
	}

	private function acquire_static_operative_heartbeat_dedupe( $dedupe_key, $ttl ) {
		$dedupe_key = sanitize_key( (string) $dedupe_key );
		$ttl        = max( 120, intval( $ttl ) );
		$now        = time();
		$payload    = array(
			'claimed_at' => $now,
		);

		if ( add_option( $dedupe_key, $payload, '', false ) ) {
			return array( 'success' => true );
		}

		$current = get_option( $dedupe_key, array() );
		$claimed = is_array( $current ) && isset( $current['claimed_at'] ) ? intval( $current['claimed_at'] ) : intval( $current );
		if ( $claimed > 0 && $claimed < ( $now - $ttl ) ) {
			$this->delete_option_if_current_value_matches( $dedupe_key, $current );
			delete_transient( $dedupe_key );
			if ( add_option( $dedupe_key, $payload, '', false ) ) {
				return array( 'success' => true, 'stale_cleared' => true );
			}
		}

		return array( 'success' => false, 'error' => 'duplicate_slot' );
	}

	private function release_static_operative_heartbeat_dedupe( $dedupe_key ) {
		$dedupe_key = sanitize_key( (string) $dedupe_key );
		if ( '' === $dedupe_key ) {
			return;
		}

		delete_option( $dedupe_key );
		delete_transient( $dedupe_key );
	}

	private function delete_option_if_current_value_matches( $option_key, $expected_value ) {
		global $wpdb;
		if ( ! $wpdb || empty( $wpdb->options ) ) {
			return false;
		}

		$serialized = function_exists( 'maybe_serialize' ) ? maybe_serialize( $expected_value ) : serialize( $expected_value );
		$deleted = $wpdb->delete(
			$wpdb->options,
			array(
				'option_name'  => (string) $option_key,
				'option_value' => $serialized,
			),
			array( '%s', '%s' )
		);

		if ( false !== $deleted && intval( $deleted ) > 0 ) {
			if ( function_exists( 'wp_cache_delete' ) ) {
				wp_cache_delete( (string) $option_key, 'options' );
				wp_cache_delete( 'alloptions', 'options' );
				wp_cache_delete( 'notoptions', 'options' );
			}
			return true;
		}

		return false;
	}

	public function get_heartbeat_config() {
		$stored_config = get_option( Niwa_AI_Constants::OPT_AUTONOMY_HEARTBEAT_CONFIG, null );
		$config = is_array( $stored_config ) ? $stored_config : array();
		if ( ! is_array( $config ) ) {
			$config = array();
		}

		$config = wp_parse_args(
			$config,
			array(
				'scheduled_hours'    => array( 14 ),
				'blog_per_day'       => 1,
				'paused'             => true,
				'engagement_enabled' => false,
				'task_log'           => array(),
			)
		);
		$config['engagement_enabled'] = false;
		return $config;
	}

	public function save_heartbeat_config( $config ) {
		$hours = isset( $config['scheduled_hours'] ) && is_array( $config['scheduled_hours'] ) ? $config['scheduled_hours'] : array( 14 );
		$hours = array_values( array_unique( array_map( 'intval', array_filter( $hours, function ( $h ) {
			return is_numeric( $h ) && intval( $h ) >= 0 && intval( $h ) <= 23;
		} ) ) ) );
		sort( $hours );
		if ( empty( $hours ) ) {
			$hours = array( 14 );
		}

		update_option(
			Niwa_AI_Constants::OPT_AUTONOMY_HEARTBEAT_CONFIG,
			array(
				'scheduled_hours'    => $hours,
				'blog_per_day'       => max( 0, min( 3, intval( isset( $config['blog_per_day'] ) ? $config['blog_per_day'] : 1 ) ) ),
				'paused'             => ! empty( $config['paused'] ),
				'engagement_enabled' => false,
				'task_log'           => isset( $config['task_log'] ) && is_array( $config['task_log'] ) ? array_slice( $config['task_log'], -30 ) : array(),
				'_configured'        => true,
			),
			false
		);
	}

	private function is_heartbeat_schedule_due( $hb_config, $last_checked_at ) {
		$tz  = wp_timezone();
		$now = new DateTimeImmutable( 'now', $tz );

		$scheduled_hours = isset( $hb_config['scheduled_hours'] ) && is_array( $hb_config['scheduled_hours'] )
			? array_map( 'intval', $hb_config['scheduled_hours'] )
			: array( 14 );
		sort( $scheduled_hours );

		if ( empty( $scheduled_hours ) ) {
			return false;
		}

		$current_hour = intval( $now->format( 'H' ) );

		$most_recent = null;
		foreach ( array_reverse( $scheduled_hours ) as $hour ) {
			if ( $current_hour >= $hour ) {
				$most_recent = $now->setTime( $hour, 0, 0 );
				break;
			}
		}

		if ( null === $most_recent ) {
			$last_hour   = end( $scheduled_hours );
			$most_recent = $now->modify( '-1 day' )->setTime( $last_hour, 0, 0 );
		}

		return $last_checked_at < $most_recent->getTimestamp();
	}

	private function pick_heartbeat_task( $hb_config, $state ) {
		$all_tasks  = $this->filter_heartbeat_tasks_for_site( $this->get_heartbeat_task_definitions() );
		$task_log   = isset( $hb_config['task_log'] ) && is_array( $hb_config['task_log'] ) ? $hb_config['task_log'] : array();
		$recent_ids = array();
		$last_blog  = 0;

		foreach ( $task_log as $entry ) {
			if ( ! is_array( $entry ) ) {
				continue;
			}

			$at = isset( $entry['at'] ) ? intval( $entry['at'] ) : 0;
			if ( $at > ( time() - 48 * HOUR_IN_SECONDS ) ) {
				$recent_ids[] = isset( $entry['task_id'] ) ? (string) $entry['task_id'] : '';
			}

			if ( isset( $entry['task_id'] ) && 'write_blog' === (string) $entry['task_id'] ) {
				$last_blog = max( $last_blog, $at );
			}
		}

		$blog_cooldown = max( 1, intval( isset( $hb_config['blog_per_day'] ) ? $hb_config['blog_per_day'] : 1 ) );
		$blog_eligible = ( $last_blog < ( time() - intval( 24 / $blog_cooldown ) * HOUR_IN_SECONDS ) );

		$candidates = array();
		foreach ( $all_tasks as $task ) {
			if ( 'write_blog' === $task['id'] && ! $blog_eligible ) {
				continue;
			}
			if ( in_array( $task['id'], $recent_ids, true ) && count( $all_tasks ) > count( $recent_ids ) ) {
				continue;
			}
			$candidates[] = $task;
		}

		if ( empty( $candidates ) ) {
			$candidates = array_values(
				array_filter(
					$all_tasks,
					function ( $t ) use ( $blog_eligible ) {
						return $blog_eligible || 'write_blog' !== $t['id'];
					}
				)
			);
		}

		if ( empty( $candidates ) ) {
			$candidates = $all_tasks;
		}

		return $candidates[ array_rand( $candidates ) ];
	}

	private function filter_heartbeat_tasks_for_site( $tasks ) {
		$site_profile = $this->get_site_operating_profile();

		return array_values(
			array_filter(
				(array) $tasks,
				static function ( $task ) use ( $site_profile ) {
					$scope = is_array( $task ) && isset( $task['scope'] ) ? (string) $task['scope'] : 'site';
					if ( 'transactions' === $scope ) {
						return ! empty( $site_profile['transactions_available'] );
					}
					if ( 'catalog' === $scope ) {
						return ! empty( $site_profile['catalog_available'] );
					}

					return true;
				}
			)
		);
	}

	private function get_heartbeat_task_definitions() {
		return array(
			array(
				'id'     => 'weekly_bestsellers',
				'type'   => 'analysis',
				'scope'  => 'transactions',
				'label'  => 'Najprodavaniji proizvodi ove nedelje',
				'prompt' => implode( "\n", array(
					'Analiziraj WooCommerce podatke i javi koji su najprodavaniji proizvodi ove nedelje.',
					'Ukljuci kolicine, prihod, i bilo kakve trendove u poredjenju sa prosle nedelje.',
					'Daj konkretne preporuke - koje proizvode vise promovisati, koje dopuniti na zalihama.',
					'Budi specificna sa imenima proizvoda i brojevima.',
				) ),
			),
			array(
				'id'     => 'sales_trend_analysis',
				'type'   => 'analysis',
				'scope'  => 'transactions',
				'label'  => 'Trendovi prodaje i konverzija',
				'prompt' => implode( "\n", array(
					'Analiziraj trendove prodaje: uporedi danas/ovu nedelju sa prethodnim periodima.',
					'Identifikuj obrasce konverzije, promene prosecne vrednosti narudzbine, signale napustanja korpe.',
					'Daj 2-3 specificne, odmah primenljive preporuke za poboljsanje konverzija.',
				) ),
			),
			array(
				'id'     => 'write_blog',
				'type'   => 'action',
				'label'  => 'Pisanje blog posta',
				'prompt' => implode( "\n", array(
					'PRVO ucitaj Blog Post Design skill koristeci skill_get("blog-post-design") i prati SVE instrukcije iz tog skill-a.',
					'Na osnovu teme, publike, ponude i postojeceg sadrzaja ovog sajta, napisi kvalitetan SEO-friendly blog post koji cilja relevantnu kljucnu rec.',
					'Pun blog normalno cilja najmanje 800 korisnih reci, ali ne dodaj SEO padding ako je namera potpuno odgovorena krace. Ne pretpostavljaj da je sajt prodavnica.',
					'OBAVEZNO pre pisanja posta:',
					'1. Skeniraj postojeci sadrzaj i izaberi 3-6 stvarnih kontekstualnih internih linkova, ukljucujuci relevantnu money stranicu kada postoji.',
					'2. Koristi samo stvarnu relevantnu Media Library sliku sa opisnim alt tekstom. Ako je nema, blog bez slike je ispravan; ne forsiraj ni featured ni inline sliku.',
					'3. Strukturiraj telo sa H2 sekcijama i H3 podsekcijama; tema poseduje jedini H1.',
					'4. Preferiraj postojecu jednu kategoriju i 0-3 korisna postojeca taga; ne stvaraj termine bez eksplicitnog opt-in-a.',
					'Kreiraj draft koristeci create_niwa_blog. Ako structured quality report vrati critical stavke, popravi ih i ponovi isti canonical poziv; nikada ne prelazi na raw fallback.',
					'Zatim javi founderu o cemu je blog i pitaj da li zeli da ga objavi.',
					'Pisi na jeziku sajta - ako je sajt na srpskom, pisi na srpskom. Ako je na engleskom, pisi na engleskom.',
				) ),
			),
			array(
				'id'     => 'seo_image_audit',
				'type'   => 'action',
				'label'  => 'SEO audit slika',
				'prompt' => implode( "\n", array(
					'Proveri slike na vaznim stranicama i u medijskoj biblioteci za SEO i accessibility probleme: nedostaje alt tekst, genericki nazivi fajlova ili nejasni naslovi.',
					'Koristi site_image_lookup i dostupne site/content alate na reprezentativnom uzorku.',
					'Prijavi konkretne URL-ove ili nazive stranica i predloge za optimizovane alt tekstove.',
				) ),
			),
			array(
				'id'     => 'competitive_landscape',
				'type'   => 'analysis',
				'label'  => 'Konkurentska analiza i pozicioniranje',
				'prompt' => implode( "\n", array(
					'Na osnovu nise, publike, ponude i sadrzaja sajta, daj uvide o konkurentskom pozicioniranju.',
					'Predlozi poboljsanja pozicioniranja, poruka i ponude, kao i praznine koje bi mogle privuci vise pravih posetilaca.',
					'Fokusiraj se na konkretne, primenljive uvide, a ne genericke savete.',
				) ),
			),
			array(
				'id'     => 'content_gap_analysis',
				'type'   => 'analysis',
				'label'  => 'Analiza nedostajuceg sadrzaja',
				'prompt' => implode( "\n", array(
					'Analiziraj postojece stranice i blog postove.',
					'Ako postoje recent_customer_questions u snapshot podacima, koristi ih da identifikujes teme koje kupce zanimaju a nisu pokrivene sadrzajem.',
					'Identifikuj kljucne reci i teme koje bi sajt trebalo da pokrije ali jos nije.',
					'Predlozi 3-5 konkretnih blog postova ili FAQ tema sa ciljnim kljucnim recima koje bi privukle organski saobracaj.',
				) ),
			),
			array(
				'id'     => 'marketing_insights',
				'type'   => 'analysis',
				'label'  => 'Saveti za optimizaciju marketinga',
				'prompt' => implode( "\n", array(
					'Pregledaj performanse sajta, publiku i ponudu pa predlozi marketinska poboljsanja.',
					'Predlozi koje poruke, stranice ili delove ponude treba istaci u kampanjama i koje uglove oglasa vredi testirati.',
					'Budi specificna sa nazivima stranica, ponudama i konkretnim brojevima koji postoje u snapshotu.',
					'Ako postoje podaci o ad spendu, analiziraj ROI i predlozi optimizacije.',
				) ),
			),
			array(
				'id'     => 'customer_faq_suggestions',
				'type'   => 'analysis',
				'label'  => 'Predlozi za FAQ i Q&A na osnovu pravih pitanja kupaca',
				'prompt' => implode( "\n", array(
					'U snapshot podacima imas recent_customer_questions - to su PRAVA pitanja koja su kupci postavljali na frontendu sajta.',
					'Analiziraj ta pitanja: koje teme se ponavljaju, sta kupce najvise zanima, gde Niwa nije imala dobar odgovor.',
					'Na osnovu toga predlozi:',
					'1. Konkretna pitanja i odgovore za FAQ/Q&A stranicu sajta.',
					'2. Blog teme koje bi pokrile najcesce nedoumice kupaca.',
					'3. Poboljsanja vaznih stranica ako posetioci pitaju o necemu sto bi vec trebalo jasno da pise na sajtu.',
					'Ako nema logiranih pitanja posetilaca, koristi stvarni sadrzaj, ponudu i navigaciju sajta da predvidis verovatna pitanja.',
					'Formatiraj kao gotov tekst koji se moze odmah staviti na sajt.',
				) ),
			),
			array(
				'id'     => 'conversion_optimization',
				'type'   => 'analysis',
				'label'  => 'Optimizacija stope konverzije',
				'prompt' => implode( "\n", array(
					'Analiziraj ciljeve, kljucne stranice i dostupne podatke sajta pa predlozi konkretne optimizacije konverzije.',
					'Fokusiraj se na jasnocu ponude, CTA elemente, signale poverenja i uklanjanje prepreka u stvarnom korisnickom toku.',
					'Fokusiraj se na brze pobede koje se mogu primeniti odmah.',
				) ),
			),
			array(
				'id'     => 'inventory_health',
				'type'   => 'action',
				'scope'  => 'transactions',
				'label'  => 'Provera zdravlja inventara',
				'prompt' => implode( "\n", array(
					'Proveri inventar proizvoda za probleme: artikli bez zaliha koji su jos vidljivi, artikli sa malim zalihama za dopunu.',
					'Proizvodi bez prodaje 30+ dana koji mozda trebaju korekciju cene ili uklanjanje.',
					'Koristi woo_product_lookup i inventory_cleanup alate da proveris stanje.',
					'Prijavi nalaze sa konkretnim imenima proizvoda i preporucenim akcijama.',
				) ),
			),
			array(
				'id'     => 'product_description_review',
				'type'   => 'analysis',
				'scope'  => 'catalog',
				'label'  => 'Pregled kvaliteta opisa proizvoda',
				'prompt' => implode( "\n", array(
					'Pregled uzorka opisa proizvoda za kvalitet: SEO optimizacija, nedostajuci kljucni detalji, nedosledno formatiranje.',
					'Predlozi jasniji, potpuniji copywriting koji odgovara katalog sajtu bez pretpostavljanja online kupovine.',
					'Izaberi 3-5 proizvoda i daj konkretne predloge za prepisivanje.',
				) ),
			),
		);
	}

	private function execute_heartbeat_action_task( $task, $chat_id ) {
		if ( ! $this->turn_engine || ! $this->auth_manager || ! $this->openai_client ) {
			return $this->execute_heartbeat_analysis_task( $task, $chat_id );
		}

		$session_id = 'admin-telegram-heartbeat';
		$context    = $this->prepare_context( $session_id );
		$context['session_id']      = $session_id;
		$context['is_heartbeat']   = true;
		$context['is_proactive_task'] = true;
		$context['heartbeat_task'] = $task;

		$identity_profile = $this->workspace && method_exists( $this->workspace, 'get_identity_profile' )
			? (array) $this->workspace->get_identity_profile()
			: array();
		$assistant_name = ! empty( $identity_profile['assistant_name'] ) ? (string) $identity_profile['assistant_name'] : 'Niwa';

			$task_prompt = implode( "\n", array(
				'[HEARTBEAT PROACTIVE TASK: ' . $task['label'] . ']',
				'',
				$task['prompt'],
				'',
				'Ti si ' . $assistant_name . ' i ovo je tvoj proaktivni heartbeat task.',
				'Koristi dostupne alate da uradis zadatak. Odgovori na jeziku foundera.',
				'Na kraju, posalji jednu Telegram poruku koja zvuci kao samoiniciran proaktivni update, ne kao odgovor na pristigli upit.',
				'Ne pisi uvode tipa "Razumem", "Vaz(i)", "Odlicno", "Evo plana", "Pogledacu", "Na osnovu snapshota" ili bilo kakvo potvrdivanje da si dobila zadatak.',
				'Ne zvuci kao da nastavljas razgovor. Nema potvrde prijema, nema obecanja da ces tek pogledati, nema meta-komentara o promptu ili snapshotu.',
				'Kreni odmah od konkretnog nalaza, akcije ili preporuke.',
				'Telegram poruka moze imati do 400 reci kada nalaz to zasluzuje; neka bude pregledna, konkretna i bez ponavljanja.',
				'Nikad ne menjaj cene, sale cene, stanje zaliha, status proizvoda ili varijacije iz heartbeat/proaktivnog taska. Za takve stvari smes samo da das predlog sa konkretnim vrednostima i da trazis admin potvrdu.',
				'Ne spominji da te je heartbeat pokrenuo - samo javi rezultat prirodno.',
				'Na kraju vrati samo JSON objekat: {"send":true,"message":"..."} ako founder treba da dobije poruku, ili {"send":false,"message":""} ako nema korisnog rezultata za slanje.',
			) );

		$turn_options = array(
			'mode'             => 'main',
			'allow_writes'     => true,
			'force_action'     => false,
			'internal'         => true,
			'visibility'       => 'internal',
		);

		$result = $this->turn_engine->run(
			$this,
			$task_prompt,
			$context,
			$turn_options
		);

		if ( ! empty( $result['empty_response_diagnostic'] ) ) {
			return array(
				'success' => false,
				'sent'    => false,
				'reason'  => 'action_no_reply',
				'error'   => isset( $result['reply'] ) ? (string) $result['reply'] : 'Heartbeat action returned an empty LLM diagnostic.',
			);
		}

		if ( empty( $result['success'] ) || empty( $result['reply'] ) ) {
			return array(
				'success' => false,
				'sent'    => false,
				'reason'  => 'action_no_reply',
				'error'   => isset( $result['error'] ) ? (string) $result['error'] : 'Heartbeat action did not return a reply.',
			);
		}

		$reply = trim( (string) $result['reply'] );
		$decision = $this->parse_heartbeat_send_decision( $reply );
		if ( empty( $decision['send'] ) ) {
			return array(
				'success' => true,
				'sent'    => false,
				'reason'  => 'heartbeat_ok',
			);
		}

		$message = $this->prepare_workspace_heartbeat_message( (string) $decision['message'] );
		$sent    = $this->telegram->send_message( $chat_id, $message );

		return array(
			'success' => ! empty( $sent['success'] ),
			'sent'    => ! empty( $sent['success'] ),
			'reason'  => ! empty( $sent['success'] ) ? 'sent' : 'send_failed',
			'message' => $message,
			'error'   => empty( $sent['success'] ) && ! empty( $sent['error'] ) ? (string) $sent['error'] : '',
		);
	}

	private function execute_heartbeat_analysis_task( $task, $chat_id ) {
		if ( ! $this->auth_manager || ! $this->openai_client ) {
			return array(
				'success' => false,
				'sent'    => false,
				'reason'  => 'no_runtime',
				'error'   => 'Heartbeat LLM runtime is not available.',
			);
		}

		$snapshot = $this->build_heartbeat_snapshot();

		$identity_profile = $this->workspace && method_exists( $this->workspace, 'get_identity_profile' )
			? (array) $this->workspace->get_identity_profile()
			: array();
		$assistant_name = ! empty( $identity_profile['assistant_name'] ) ? (string) $identity_profile['assistant_name'] : 'Niwa';

		$prompt_payload = array(
			'host'                    => isset( $snapshot['host'] ) ? (string) $snapshot['host'] : '',
			'site_name'               => isset( $snapshot['site_name'] ) ? (string) $snapshot['site_name'] : '',
			'checked_at'              => isset( $snapshot['checked_at'] ) ? (string) $snapshot['checked_at'] : '',
			'site_profile'            => isset( $snapshot['site_profile'] ) && is_array( $snapshot['site_profile'] ) ? $snapshot['site_profile'] : array(),
			'site_context'            => isset( $snapshot['site_context'] ) ? (string) $snapshot['site_context'] : '',
			'recent_work'             => isset( $snapshot['recent_work'] ) ? (string) $snapshot['recent_work'] : '',
			'today_metrics'           => isset( $snapshot['today_metrics'] ) ? $snapshot['today_metrics'] : array(),
			'weekly_metrics'          => isset( $snapshot['weekly_metrics'] ) ? $snapshot['weekly_metrics'] : array(),
			'existing_blog_posts'     => isset( $snapshot['existing_blog_posts'] ) ? $snapshot['existing_blog_posts'] : array(),
			'daily_report'            => isset( $snapshot['daily_report'] ) ? $snapshot['daily_report'] : array(),
		);
		if ( isset( $snapshot['weekly_bestsellers'] ) ) {
			$prompt_payload['weekly_bestsellers'] = $snapshot['weekly_bestsellers'];
		}
		if ( isset( $snapshot['product_catalog_summary'] ) ) {
			$prompt_payload['product_catalog_summary'] = $snapshot['product_catalog_summary'];
		}
		$prompt_data = wp_json_encode( $prompt_payload );

			$system = implode( "\n", array(
				'You are ' . $assistant_name . ' running a proactive improvement heartbeat for a WordPress site.',
				'',
				'CURRENT TASK: ' . $task['label'],
			'',
			$task['prompt'],
			'',
				'RULES:',
				'- Keep the response natural, direct, actionable, and written as a self-initiated proactive update.',
				'- Write using this preferred founder language hint when the target language is not otherwise obvious: ' . $this->get_turn_language_label( $this->get_default_ui_turn_language() ) . '.',
				'- If you write Serbian, Croatian, or Bosnian, always use feminine first-person grammar that matches that language.',
				'- Do not mention that this came from a heartbeat or automated job.',
				'- This is NOT a reply to a user message. Do not acknowledge, confirm, or thank. Do not say you understand, that you will look into it, that you are checking, or that you are making a plan.',
				'- Do not use openings like "Razumem", "Odlicno", "Super", "Pogledala sam", "Evo plana", "Na osnovu dostupnog snapshota", "Here is the plan", or similar prompt-reply phrasing.',
				'- Start immediately with the finding, action taken, or recommendation itself.',
				'- Telegram message may be up to 400 words when the finding deserves it; keep it scannable, concrete, and non-repetitive.',
				'- Treat site_profile as authoritative. Never introduce WooCommerce or transactional-store scope when the profile does not support it.',
				'- Be specific with page, offering, or product names, numbers, and concrete recommendations that fit the site profile.',
				'- Only mention facts present in the JSON snapshot. Do not invent data.',
				'- SILENCE IS PREFERRED over boring or generic updates.',
				'- Do NOT send generic advice, filler, or observations that the founder already knows. Only send a message if it contains a specific insight, number, recommendation, or finding that would genuinely help.',
				'- Return only JSON: {"send":true,"message":"..."} when a Telegram message should be sent, or {"send":false,"message":""} when there is nothing useful to send.',
			'',
			'[WORKSPACE CONTEXT]',
			$this->build_workspace_context_block( 'main' ),
			'[/WORKSPACE CONTEXT]',
		) );

		$auth_mode = $this->auth_manager->get_mode();
		$creds     = $this->auth_manager->get_credentials();
		$model     = $this->get_founder_model();

		$result = $this->openai_client->generate(
			$model,
			$prompt_data,
			$system,
			$auth_mode,
			$creds,
			90
		);

		if ( empty( $result['success'] ) ) {
			$llm_error = isset( $result['error'] ) ? (string) $result['error'] : '';
			$reason    = '' === $llm_error ? 'no_reply' : 'llm_error';

			return array(
				'success'   => false,
				'sent'      => false,
				'reason'    => $reason,
				'llm_error' => $llm_error,
				'error'     => '' === $llm_error ? 'Heartbeat analysis did not return a reply.' : $llm_error,
			);
		}

		$answer = trim( (string) ( isset( $result['answer'] ) ? $result['answer'] : '' ) );
		if ( '' === $answer ) {
			return array(
				'success' => false,
				'sent'    => false,
				'reason'  => 'no_reply',
				'error'   => 'Heartbeat analysis returned an empty reply.',
			);
		}

		$decision = $this->parse_heartbeat_send_decision( $answer );
		if ( empty( $decision['send'] ) ) {
			return array(
				'success' => true,
				'sent'    => false,
				'reason'  => 'heartbeat_ok',
			);
		}

		$message = $this->prepare_workspace_heartbeat_message( (string) $decision['message'] );
		$sent    = $this->telegram->send_message( $chat_id, $message );

		return array(
			'success' => ! empty( $sent['success'] ),
			'sent'    => ! empty( $sent['success'] ),
			'reason'  => ! empty( $sent['success'] ) ? 'sent' : 'send_failed',
			'message' => $message,
		);
	}

	private function prepare_workspace_heartbeat_message( $message, $limit = 3200, $word_limit = 400 ) {
		$message = $this->prepare_founder_telegram_message( (string) $message );
		$message = $this->truncate_heartbeat_message_words( $message, $word_limit );
		return $this->truncate_block( $message, max( 80, intval( $limit ) ) );
	}

	private function truncate_heartbeat_message_words( $message, $word_limit = 400 ) {
		$word_limit = max( 1, intval( $word_limit ) );
		$parts      = preg_split( '/(\s+)/u', (string) $message, -1, PREG_SPLIT_DELIM_CAPTURE );
		if ( ! is_array( $parts ) ) {
			return (string) $message;
		}

		$out   = '';
		$words = 0;
		foreach ( $parts as $part ) {
			if ( '' === $part ) {
				continue;
			}
			if ( preg_match( '/^\s+$/u', $part ) ) {
				$out .= $part;
				continue;
			}

			++$words;
			if ( $words > $word_limit ) {
				return rtrim( $out ) . '...';
			}
			$out .= $part;
		}

		return $out;
	}

	public function get_heartbeat_state() {
		$state = get_option( Niwa_AI_Constants::OPT_AUTONOMY_HEARTBEAT_STATE, array() );
		if ( ! is_array( $state ) ) {
			$state = array();
		}

		return wp_parse_args(
			$state,
			array(
				'last_checked_at' => 0,
				'last_sent_at'    => 0,
				'last_status'     => '',
				'last_hash'       => '',
				'last_summary'    => '',
				'last_task'       => '',
				'last_error'      => '',
			)
		);
	}

	private function save_heartbeat_state( $state ) {
		$status = isset( $state['last_status'] ) ? (string) $state['last_status'] : '';

		update_option(
			Niwa_AI_Constants::OPT_AUTONOMY_HEARTBEAT_STATE,
			array(
				'last_checked_at' => isset( $state['last_checked_at'] ) ? intval( $state['last_checked_at'] ) : 0,
				'last_sent_at'    => isset( $state['last_sent_at'] ) ? intval( $state['last_sent_at'] ) : 0,
				'last_status'     => $status,
				'last_hash'       => isset( $state['last_hash'] ) ? (string) $state['last_hash'] : '',
				'last_summary'    => isset( $state['last_summary'] ) ? (string) $state['last_summary'] : '',
				'last_task'       => isset( $state['last_task'] ) ? (string) $state['last_task'] : '',
				'last_error'      => 'llm_error' === $status && isset( $state['last_summary'] ) ? (string) $state['last_summary'] : '',
			),
			false
		);
	}

	private function build_heartbeat_snapshot() {
		$settings             = $this->get_admin_autonomy_settings();
		$owner_chat_id        = isset( $settings['owner_chat_id'] ) ? trim( (string) $settings['owner_chat_id'] ) : '';
		$heartbeat_session_id = $this->build_founder_heartbeat_session_id( $owner_chat_id, 'admin-telegram-heartbeat' );
		$heartbeat_record = $this->workspace && method_exists( $this->workspace, 'read_bootstrap_file' )
			? $this->workspace->read_bootstrap_file( 'HEARTBEAT.md' )
			: array();
		$identity_profile = $this->workspace && method_exists( $this->workspace, 'get_identity_profile' )
			? (array) $this->workspace->get_identity_profile()
			: array();
		$sessions = $this->session_state && method_exists( $this->session_state, 'list_sessions' )
			? $this->session_state->list_sessions( 6, 'admin-telegram-' )
			: array();
		$recent_runs = $this->store && method_exists( $this->store, 'list_recent_runs' )
			? $this->store->list_recent_runs( 6 )
			: array();

		$site_profile = $this->get_site_operating_profile();
		$snapshot     = array(
			'checked_at'              => wp_date( 'Y-m-d H:i:s' ),
			'host'                    => wp_parse_url( home_url( '/' ), PHP_URL_HOST ),
			'site_name'               => get_bloginfo( 'name' ),
			'site_profile'            => $site_profile,
			'site_context'            => $this->build_site_context_blob(),
			'recent_work'             => $this->build_recent_work_notes(),
			'daily_report'            => $this->get_daily_report_state(),
			'today_metrics'           => $this->get_metrics_tool_payload( 'today' ),
			'weekly_metrics'          => $this->get_metrics_tool_payload( 'week' ),
			'existing_blog_posts'     => $this->collect_existing_blog_posts(),
			'recent_customer_questions' => $this->collect_recent_customer_questions(),
			'sessions'                => array_values( (array) $sessions ),
			'recent_runs'             => array_values( (array) $recent_runs ),
			'subagents'               => ! empty( $sessions[0]['recent_subagents'] ) ? array_values( (array) $sessions[0]['recent_subagents'] ) : array(),
			'identity_profile'        => $identity_profile,
			'companion_memory'        => ( $this->companion_memory_manager && method_exists( $this->companion_memory_manager, 'build_prompt_summary' ) )
				? (string) $this->companion_memory_manager->build_prompt_summary( $heartbeat_session_id, 1400 )
				: '',
			'heartbeat_prompt'        => ! empty( $heartbeat_record['content'] ) ? (string) $heartbeat_record['content'] : '',
		);
		if ( ! empty( $site_profile['catalog_available'] ) ) {
			$snapshot['product_catalog_summary'] = $this->collect_product_catalog_summary();
		}
		if ( ! empty( $site_profile['transactions_available'] ) ) {
			$snapshot['weekly_bestsellers'] = $this->collect_weekly_bestsellers();
		}

		return $snapshot;
	}

	private function generate_ai_heartbeat_result( $snapshot ) {
		if ( ! $this->auth_manager || ! $this->openai_client ) {
			return array(
				'success' => false,
				'sent'    => false,
				'reason'  => 'no_runtime',
				'error'   => 'Heartbeat LLM runtime is not available.',
			);
		}

		$instructions = trim( (string) ( isset( $snapshot['heartbeat_prompt'] ) ? $snapshot['heartbeat_prompt'] : '' ) );
		if ( '' === $instructions ) {
			return array(
				'success' => true,
				'sent'    => false,
				'reason'  => 'no_instructions',
			);
		}

		$auth_mode = $this->auth_manager->get_mode();
		$creds     = $this->auth_manager->get_credentials();
		$model     = $this->get_founder_model();
		$identity_profile = $this->workspace && method_exists( $this->workspace, 'get_identity_profile' )
			? (array) $this->workspace->get_identity_profile()
			: array();
		$assistant_name = ! empty( $identity_profile['assistant_name'] ) ? (string) $identity_profile['assistant_name'] : 'Niwa';
		$prompt    = wp_json_encode(
			array(
				'host'          => isset( $snapshot['host'] ) ? (string) $snapshot['host'] : '',
				'site_name'     => isset( $snapshot['site_name'] ) ? (string) $snapshot['site_name'] : '',
				'checked_at'    => isset( $snapshot['checked_at'] ) ? (string) $snapshot['checked_at'] : '',
				'site_profile'  => isset( $snapshot['site_profile'] ) && is_array( $snapshot['site_profile'] ) ? $snapshot['site_profile'] : array(),
				'site_context'  => isset( $snapshot['site_context'] ) ? (string) $snapshot['site_context'] : '',
				'recent_work'   => isset( $snapshot['recent_work'] ) ? (string) $snapshot['recent_work'] : '',
				'today_metrics' => isset( $snapshot['today_metrics'] ) ? $snapshot['today_metrics'] : array(),
				'recent_runs'   => isset( $snapshot['recent_runs'] ) ? $snapshot['recent_runs'] : array(),
				'sessions'      => isset( $snapshot['sessions'] ) ? $snapshot['sessions'] : array(),
				'subagents'     => isset( $snapshot['subagents'] ) ? $snapshot['subagents'] : array(),
				'daily_report'  => isset( $snapshot['daily_report'] ) ? $snapshot['daily_report'] : array(),
			)
		);
		$result = $this->openai_client->generate(
			$model,
			$prompt,
			implode(
				"\n",
				array(
					'You are ' . $assistant_name . ' running a proactive founder heartbeat for Telegram.',
					'Use the custom workspace heartbeat instructions below as the operating policy.',
					'If there is no meaningful new signal, no actionable issue, and no noteworthy positive movement, return only {"send":false,"message":""}.',
					'If you do send a message, keep it under 240 words, natural, direct, and use this preferred founder language hint when the target language is not otherwise obvious: ' . $this->get_turn_language_label( $this->get_default_ui_turn_language() ) . '.',
					'Return only JSON: {"send":true,"message":"..."} or {"send":false,"message":""}.',
					'If you write Serbian, Croatian, or Bosnian, always use feminine first-person grammar that matches that language.',
					'Do not mention missing data, zeros, internal schemas, or that you were prompted by a heartbeat job.',
					'Treat site_profile as authoritative and do not introduce unsupported WooCommerce or transactional-store scope.',
					'Only mention facts present in the JSON snapshot.',
					'',
					'[WORKSPACE CONTEXT]',
					$this->build_workspace_context_block( 'main' ),
					'[/WORKSPACE CONTEXT]',
					'',
					'[HEARTBEAT INSTRUCTIONS]',
					$instructions,
					'[/HEARTBEAT INSTRUCTIONS]',
				)
			),
			$auth_mode,
			$creds,
			90
		);

		if ( empty( $result['success'] ) ) {
			return array(
				'success' => false,
				'sent'    => false,
				'reason'  => 'llm_error',
				'error'   => isset( $result['error'] ) ? (string) $result['error'] : 'Custom heartbeat LLM call failed.',
			);
		}

		$answer = trim( (string) ( isset( $result['answer'] ) ? $result['answer'] : '' ) );
		if ( '' === $answer ) {
			return array(
				'success' => false,
				'sent'    => false,
				'reason'  => 'no_reply',
				'error'   => 'Custom heartbeat LLM call returned an empty reply.',
			);
		}

		$decision = $this->parse_heartbeat_send_decision( $answer );
		if ( empty( $decision['send'] ) ) {
			return array(
				'success' => true,
				'sent'    => false,
				'reason'  => 'heartbeat_ok',
			);
		}

		return array(
			'success' => true,
			'sent'    => true,
			'reason'  => 'message_ready',
			'message' => $this->prepare_workspace_heartbeat_message( (string) $decision['message'] ),
		);
	}

	private function parse_heartbeat_send_decision( $raw ) {
		if ( class_exists( 'Niwa_AI_Admin_Autonomy_Delivery_Envelope_Normalizer' ) ) {
			$normalized = Niwa_AI_Admin_Autonomy_Delivery_Envelope_Normalizer::normalize( $raw );
			return array(
				'send'    => ! empty( $normalized['send'] ),
				'message' => isset( $normalized['message'] ) ? (string) $normalized['message'] : '',
			);
		}

		$raw = trim( (string) $raw );
		return array(
			'send'    => '' !== $raw,
			'message' => $raw,
		);
	}

	/* ================================================================
	 * Operative Heartbeat
	 * ============================================================= */

	/**
	 * Run the operative heartbeat if due.
	 *
	 * Called every minute from poll_telegram_updates(). Checks if the
	 * operative heartbeat is enabled and if enough time has passed since
	 * the last run, then executes the tasks defined in OPERATIVE_HEARTBEAT.md
	 * via a full agent loop with tools.
	 *
	 * @return array Status array.
	 */
	public function maybe_run_operative_heartbeat() {
		return $this->maybe_run_role_operative_heartbeat( 'admin' );
	}

	public function maybe_run_stock_manager_operative_heartbeat() {
		return $this->maybe_run_role_operative_heartbeat( 'stock_manager' );
	}

	private function maybe_run_role_operative_heartbeat( $role = 'admin' ) {
		$role     = $this->normalize_operative_heartbeat_role( $role );
		$settings = $this->get_admin_autonomy_settings();
		$config = $this->get_operative_heartbeat_config( $role, true );
		$delivery_channel = $this->normalize_operative_heartbeat_delivery_channel( isset( $config['delivery_channel'] ) ? $config['delivery_channel'] : '' );
		if ( 'admin_ui' !== $delivery_channel && empty( $settings['enabled'] ) ) {
			return array( 'success' => true, 'sent' => false, 'reason' => 'disabled' );
		}

		if ( 'whatsapp' === $delivery_channel && ( ! $this->whatsapp || ! method_exists( $this->whatsapp, 'send_message' ) ) ) {
			return array( 'success' => false, 'sent' => false, 'reason' => 'whatsapp_unavailable' );
		}

		if ( 'telegram' === $delivery_channel && ! $this->telegram ) {
			return array( 'success' => false, 'sent' => false, 'reason' => 'telegram_unavailable' );
		}

		if ( 'telegram' === $delivery_channel && method_exists( $this->telegram, 'is_outbound_transport_enabled' ) && ! $this->telegram->is_outbound_transport_enabled() ) {
			return array( 'success' => true, 'sent' => false, 'reason' => 'telegram_outbound_disabled' );
		}

		if ( empty( $config['enabled'] ) ) {
			return array( 'success' => true, 'sent' => false, 'reason' => 'operative_disabled' );
		}

		if ( $this->is_operative_heartbeat_config_expired( $config ) ) {
			$config['enabled']       = false;
			$state                   = $this->get_operative_heartbeat_state( $role );
			$state['last_attempt_at'] = time();
			$state['last_status']     = 'ended';
			$state['last_summary']    = 'Operative heartbeat hard end time reached.';
			$this->save_operative_heartbeat_config( $config, $role );
			$this->save_operative_heartbeat_state( $state, $role );
			return array( 'success' => true, 'sent' => false, 'reason' => 'ended' );
		}

		$recipient_chat_ids = $this->resolve_operative_heartbeat_recipient_chat_ids( $role, $settings, $config );
		if ( empty( $recipient_chat_ids ) ) {
			return array( 'success' => false, 'sent' => false, 'reason' => 'missing_chat' );
		}

		$interval_minutes = max( 0.5, round( floatval( isset( $config['interval_minutes'] ) ? $config['interval_minutes'] : 60 ), 2 ) );
		$interval_seconds = max( 30, intval( round( $interval_minutes * MINUTE_IN_SECONDS ) ) );
		$state            = $this->get_operative_heartbeat_state( $role );
		$last_run         = intval( isset( $state['last_run_at'] ) ? $state['last_run_at'] : 0 );
		$next_retry_at    = intval( isset( $state['next_retry_at'] ) ? $state['next_retry_at'] : 0 );
		if ( $next_retry_at > time() ) {
			return array( 'success' => true, 'sent' => false, 'reason' => 'retry_wait', 'next_retry_at' => $next_retry_at );
		}
		$scheduled_hours  = $this->sanitize_operative_heartbeat_scheduled_hours( isset( $config['scheduled_hours'] ) ? $config['scheduled_hours'] : array() );
		$current_slot_key = '';
		$is_static_exact  = $this->operative_heartbeat_config_is_static_exact_message( $config );

		if ( ! empty( $scheduled_hours ) ) {
			$slot = $this->get_operative_heartbeat_current_scheduled_slot( $config, $state );
			if ( empty( $slot['due'] ) ) {
				return array(
					'success' => true,
					'sent'    => false,
					'reason'  => isset( $slot['reason'] ) ? (string) $slot['reason'] : 'not_due_scheduled_slot',
				);
			}
			$current_slot_key = isset( $slot['slot_key'] ) ? (string) $slot['slot_key'] : '';
		} elseif ( $last_run > 0 && ( time() - $last_run ) < $interval_seconds ) {
			return array( 'success' => true, 'sent' => false, 'reason' => 'not_due' );
		}

		$instructions = $this->get_operative_heartbeat_instructions( $role );
		if ( '' === $instructions && ! $is_static_exact ) {
			return array( 'success' => true, 'sent' => false, 'reason' => 'no_instructions' );
		}

		$lock = $this->acquire_operative_heartbeat_lock( $role );
		if ( empty( $lock['success'] ) ) {
			return array( 'success' => true, 'sent' => false, 'reason' => 'locked' );
		}

		$fresh_config = $this->get_operative_heartbeat_config( $role, true );
		if ( empty( $fresh_config['enabled'] ) ) {
			$this->release_operative_heartbeat_lock( $lock );
			return array( 'success' => true, 'sent' => false, 'reason' => 'operative_disabled_after_due_check' );
		}
		if ( ! hash_equals( $this->build_operative_heartbeat_config_signature( $config ), $this->build_operative_heartbeat_config_signature( $fresh_config ) ) ) {
			$this->release_operative_heartbeat_lock( $lock );
			return array( 'success' => true, 'sent' => false, 'reason' => 'config_changed' );
		}
		$config          = $fresh_config;
		$is_static_exact = $this->operative_heartbeat_config_is_static_exact_message( $config );

		$state            = $this->get_operative_heartbeat_state( $role );
		$last_run         = intval( isset( $state['last_run_at'] ) ? $state['last_run_at'] : 0 );
		$next_retry_at    = intval( isset( $state['next_retry_at'] ) ? $state['next_retry_at'] : 0 );
		$scheduled_hours  = $this->sanitize_operative_heartbeat_scheduled_hours( isset( $config['scheduled_hours'] ) ? $config['scheduled_hours'] : array() );
		$current_slot_key = '';
		if ( $next_retry_at > time() ) {
			$this->release_operative_heartbeat_lock( $lock );
			return array( 'success' => true, 'sent' => false, 'reason' => 'retry_wait_after_lock', 'next_retry_at' => $next_retry_at );
		}
		if ( ! empty( $scheduled_hours ) ) {
			$slot = $this->get_operative_heartbeat_current_scheduled_slot( $config, $state );
			if ( empty( $slot['due'] ) ) {
				$this->release_operative_heartbeat_lock( $lock );
				return array(
					'success' => true,
					'sent'    => false,
					'reason'  => isset( $slot['reason'] ) ? (string) $slot['reason'] . '_after_lock' : 'not_due_scheduled_slot_after_lock',
				);
			}
			$current_slot_key = isset( $slot['slot_key'] ) ? (string) $slot['slot_key'] : '';
		} elseif ( $last_run > 0 && ( time() - $last_run ) < $interval_seconds ) {
			$this->release_operative_heartbeat_lock( $lock );
			return array( 'success' => true, 'sent' => false, 'reason' => 'not_due_after_lock' );
		}

		try {
			if ( $is_static_exact ) {
				$result = $this->execute_static_operative_heartbeat( $recipient_chat_ids, $state, $role, $config, $current_slot_key, $interval_seconds );
			} else {
				$result = $this->execute_operative_heartbeat( $recipient_chat_ids[0], $recipient_chat_ids, $instructions, $state, $role, $config );
			}
		} catch ( Throwable $e ) {
			$result = array(
				'success' => false,
				'sent'    => false,
				'reason'  => 'exception',
				'error'   => $e->getMessage(),
			);
		} finally {
			$retryable = $this->is_heartbeat_retryable_result( $result );
			$state['last_attempt_at'] = time();
			if ( $retryable ) {
				$state['next_retry_at'] = time() + max( 60, min( 300, intval( ceil( $interval_seconds / 4 ) ) ) );
			} else {
				$state['next_retry_at'] = 0;
				$state['last_run_at'] = time();
				$state['run_count']   = intval( isset( $state['run_count'] ) ? $state['run_count'] : 0 ) + 1;
				if ( '' !== $current_slot_key ) {
					$state['last_slot_key'] = $current_slot_key;
				}
			}

			if ( ! empty( $result['sent'] ) ) {
				$state['last_sent_at'] = time();
				$state['last_status']  = 'sent';
				$state['last_summary'] = $this->truncate_block(
					isset( $result['message'] ) ? (string) $result['message'] : '', 280
				);

				$log   = isset( $state['execution_log'] ) && is_array( $state['execution_log'] ) ? $state['execution_log'] : array();
				$log[] = array(
					'at'      => time(),
					'summary' => $this->truncate_block( isset( $result['message'] ) ? (string) $result['message'] : '', 200 ),
				);
				$state['execution_log'] = array_slice( $log, -20 );
			} else {
				$state['last_status']  = isset( $result['reason'] ) ? (string) $result['reason'] : 'no_output';
				$state['last_summary'] = ! empty( $result['error'] ) ? $this->truncate_block( (string) $result['error'], 280 ) : '';
			}

			$this->save_operative_heartbeat_state( $state, $role );
			if ( ! empty( $result['dedupe_key'] ) && empty( $result['sent'] ) ) {
				$this->release_static_operative_heartbeat_dedupe( (string) $result['dedupe_key'] );
			}
			$this->release_operative_heartbeat_lock( $lock );
		}

		return $result;
	}

	private function operative_heartbeat_config_is_static_exact_message( $config ) {
		$config = is_array( $config ) ? $config : array();
		return isset( $config['execution_mode'], $config['static_message'] )
			&& 'static_exact_message' === sanitize_key( (string) $config['execution_mode'] )
			&& '' !== trim( (string) $config['static_message'] );
	}

	private function execute_static_operative_heartbeat( $recipient_chat_ids, $state, $role, $config, $current_slot_key = '', $interval_seconds = 60 ) {
		$role             = $this->normalize_operative_heartbeat_role( $role );
		$config           = is_array( $config ) ? $config : array();
		$fresh_config     = $this->get_operative_heartbeat_config( $role, true );
		if ( empty( $fresh_config['enabled'] ) ) {
			return array( 'success' => true, 'sent' => false, 'reason' => 'operative_disabled_before_static_send' );
		}
		if ( ! hash_equals( $this->build_operative_heartbeat_config_signature( $config ), $this->build_operative_heartbeat_config_signature( $fresh_config ) ) ) {
			return array( 'success' => true, 'sent' => false, 'reason' => 'config_changed_before_static_send' );
		}
		$config           = $fresh_config;
		$delivery_channel = $this->normalize_operative_heartbeat_delivery_channel( isset( $config['delivery_channel'] ) ? $config['delivery_channel'] : '' );
		$message          = (string) $config['static_message'];
		if ( '' === trim( $message ) ) {
			return array( 'success' => false, 'sent' => false, 'reason' => 'empty_static_message', 'error' => 'Static heartbeat message is empty.' );
		}

		$slot_key = '' !== (string) $current_slot_key
			? (string) $current_slot_key
			: (string) ( floor( time() / max( 30, intval( $interval_seconds ) ) ) * max( 30, intval( $interval_seconds ) ) );
		$recipients = array_values( array_unique( array_filter( array_map( 'strval', (array) $recipient_chat_ids ) ) ) );
		$dedupe_key = 'niwa_ai_static_operative_hb_' . md5( $role . '|' . $delivery_channel . '|' . implode( ',', $recipients ) . '|' . md5( $message ) . '|' . $slot_key );
		$dedupe     = $this->acquire_static_operative_heartbeat_dedupe( $dedupe_key, max( 120, intval( $interval_seconds ) * 2 ) );
		if ( empty( $dedupe['success'] ) ) {
			return array(
				'success' => true,
				'sent'    => false,
				'reason'  => 'duplicate_slot',
				'message' => $message,
			);
		}

		if ( 'admin_ui' === $delivery_channel ) {
			$session_id = ! empty( $config['delivery_session_id'] )
				? (string) $config['delivery_session_id']
				: $this->build_operative_heartbeat_session_id( $role, isset( $recipients[0] ) ? (string) $recipients[0] : '' );
			$delivered = $this->deliver_operative_heartbeat_admin_ui_message( $session_id, $message, $config );
			if ( empty( $delivered['success'] ) ) {
				$this->release_static_operative_heartbeat_dedupe( $dedupe_key );
			}
			return array(
				'success'         => ! empty( $delivered['success'] ),
				'sent'            => ! empty( $delivered['success'] ),
				'reason'          => ! empty( $delivered['success'] ) ? 'sent' : 'send_failed',
				'message'         => $message,
				'sent_count'      => ! empty( $delivered['success'] ) ? 1 : 0,
				'recipient_count' => 1,
				'errors'          => empty( $delivered['success'] ) && ! empty( $delivered['error'] ) ? array( (string) $delivered['error'] ) : array(),
				'dedupe_key'      => $dedupe_key,
			);
		}

		$sent_count  = 0;
		$send_errors = array();
		foreach ( $recipients as $recipient_chat_id ) {
			$sent = $this->send_operative_heartbeat_channel_message(
				$delivery_channel,
				$recipient_chat_id,
				$message,
				array(
					'session_id'  => isset( $config['delivery_session_id'] ) ? (string) $config['delivery_session_id'] : '',
					'workflow_key' => $dedupe_key,
				)
			);
			if ( ! empty( $sent['success'] ) ) {
				$sent_count++;
			} elseif ( ! empty( $sent['error'] ) ) {
				$send_errors[] = (string) $sent['error'];
			}
		}
		if ( 0 === $sent_count ) {
			$this->release_static_operative_heartbeat_dedupe( $dedupe_key );
		}

		return array(
			'success'         => $sent_count > 0,
			'sent'            => $sent_count > 0,
			'reason'          => $sent_count > 0 ? ( $sent_count === count( $recipients ) ? 'sent' : 'partial_send' ) : 'send_failed',
			'message'         => $message,
			'sent_count'      => $sent_count,
			'recipient_count' => count( $recipients ),
			'errors'          => $send_errors,
			'dedupe_key'      => $dedupe_key,
		);
	}

	private function send_operative_heartbeat_channel_message( $channel, $chat_id, $message, $context = array() ) {
		$channel = $this->normalize_operative_heartbeat_delivery_channel( $channel );
		if ( 'whatsapp' === $channel ) {
			return $this->whatsapp && method_exists( $this->whatsapp, 'send_message' )
				? $this->whatsapp->send_message( $chat_id, $message, is_array( $context ) ? $context : array() )
				: array( 'success' => false, 'error' => 'whatsapp_unavailable' );
		}

		return $this->telegram && method_exists( $this->telegram, 'send_message' )
			? $this->telegram->send_message( $chat_id, $message )
			: array( 'success' => false, 'error' => 'telegram_unavailable' );
	}

	/**
	 * Get operative heartbeat instructions from the role-specific heartbeat brief.
	 *
	 * @return string Trimmed instructions or empty string.
	 */
	private function get_operative_heartbeat_instructions( $role = 'admin' ) {
		if ( ! $this->workspace || ! method_exists( $this->workspace, 'read_bootstrap_file' ) ) {
			return '';
		}

		$brief_file = $this->get_operative_heartbeat_brief_filename( $role );
		$record = $this->workspace->read_bootstrap_file( $brief_file );
		if ( empty( $record['content'] ) ) {
			return '';
		}

		$content = trim( (string) $record['content'] );

		// Consider effectively empty if only default template content.
		if ( false !== strpos( $content, '<!-- No ' ) && false === strpos( $content, '## Task' ) ) {
			return '';
		}

		return $content;
	}

	public function has_operative_heartbeat_instructions( $role = 'admin' ) {
		return '' !== $this->get_operative_heartbeat_instructions( $role );
	}

	private function get_operative_heartbeat_current_scheduled_slot( $config, $state = array() ) {
		$config          = is_array( $config ) ? $config : array();
		$state           = is_array( $state ) ? $state : array();
		$scheduled_hours = $this->sanitize_operative_heartbeat_scheduled_hours( isset( $config['scheduled_hours'] ) ? $config['scheduled_hours'] : array() );
		if ( empty( $scheduled_hours ) ) {
			return array( 'due' => true, 'slot_key' => '' );
		}

		$timezone_string = function_exists( 'wp_timezone_string' ) ? wp_timezone_string() : '';
		if ( '' === trim( (string) $timezone_string ) ) {
			$timezone_string = 'UTC';
		}
		try {
			$timezone = new DateTimeZone( $timezone_string );
		} catch ( Throwable $e ) {
			$timezone = new DateTimeZone( 'UTC' );
		}

		$now              = new DateTimeImmutable( 'now', $timezone );
		$current_hour     = intval( $now->format( 'G' ) );
		$current_minute   = intval( $now->format( 'i' ) );
		$scheduled_minute = $this->sanitize_operative_heartbeat_scheduled_minute( isset( $config['scheduled_minute'] ) ? $config['scheduled_minute'] : 0 );

		if ( ! in_array( $current_hour, $scheduled_hours, true ) || $current_minute < $scheduled_minute ) {
			return array( 'due' => false, 'reason' => 'not_due_scheduled_slot' );
		}

		$slot_key = $now->format( 'Y-m-d' ) . '-' . sprintf( '%02d:%02d', $current_hour, $scheduled_minute );
		if ( isset( $state['last_slot_key'] ) && (string) $state['last_slot_key'] === $slot_key ) {
			return array( 'due' => false, 'reason' => 'scheduled_slot_already_processed', 'slot_key' => $slot_key );
		}

		return array( 'due' => true, 'slot_key' => $slot_key );
	}

	private function sanitize_operative_heartbeat_scheduled_hours( $value ) {
		if ( is_string( $value ) ) {
			$value = preg_split( '/[\s,;]+/', $value );
		}
		if ( ! is_array( $value ) ) {
			$value = array();
		}

		$hours = array();
		foreach ( $value as $hour ) {
			if ( is_array( $hour ) ) {
				continue;
			}
			$hour = intval( $hour );
			if ( $hour >= 0 && $hour <= 23 ) {
				$hours[] = $hour;
			}
		}

		$hours = array_values( array_unique( $hours ) );
		sort( $hours, SORT_NUMERIC );
		return $hours;
	}

	private function sanitize_operative_heartbeat_scheduled_minute( $value ) {
		return max( 0, min( 59, intval( $value ) ) );
	}

	public function write_operative_heartbeat_task_brief( $task_brief, $role = 'admin', $config = array(), $meta = array() ) {
		$task_brief = trim( (string) $task_brief );
		if ( '' === $task_brief ) {
			return array(
				'success' => false,
				'error'   => 'Operative heartbeat task brief je prazan.',
			);
		}

		if ( ! $this->workspace || ! method_exists( $this->workspace, 'write_bootstrap_file' ) ) {
			return array(
				'success' => false,
				'error'   => 'Workspace runtime nije ucitan.',
			);
		}

		$role       = $this->normalize_operative_heartbeat_role( $role );
		$config     = is_array( $config ) ? $config : array();
		$meta       = is_array( $meta ) ? $meta : array();
		$brief_file = $this->get_operative_heartbeat_brief_filename( $role );
		$content    = $this->build_operative_heartbeat_task_brief_content( $task_brief, $role, $config, $meta );
		$written    = $this->workspace->write_bootstrap_file( $brief_file, $content, false );
		if ( empty( $written['success'] ) ) {
			return $written;
		}

		return array(
			'success'  => true,
			'filename' => $brief_file,
			'path'     => isset( $written['path'] ) ? (string) $written['path'] : '',
		);
	}

	private function build_operative_heartbeat_task_brief_content( $task_brief, $role = 'admin', $config = array(), $meta = array() ) {
		$role       = $this->normalize_operative_heartbeat_role( $role );
		$config     = is_array( $config ) ? $config : array();
		$meta       = is_array( $meta ) ? $meta : array();
		$brief_file = $this->get_operative_heartbeat_brief_filename( $role );
		$interval   = max( 0.5, round( floatval( isset( $config['interval_minutes'] ) ? $config['interval_minutes'] : 60 ), 2 ) );
		$delivery   = isset( $config['delivery_channel'] ) ? sanitize_key( (string) $config['delivery_channel'] ) : 'telegram';
		$stop       = isset( $meta['stop_condition'] ) ? trim( (string) $meta['stop_condition'] ) : '';
		$end_at_utc = isset( $config['end_at_utc'] ) ? trim( (string) $config['end_at_utc'] ) : '';
		$source     = isset( $meta['source_channel'] ) ? sanitize_key( (string) $meta['source_channel'] ) : '';
		$created_at = function_exists( 'wp_date' ) ? wp_date( 'Y-m-d H:i:s' ) : gmdate( 'Y-m-d H:i:s' );
		$scheduled_hours  = $this->sanitize_operative_heartbeat_scheduled_hours( isset( $config['scheduled_hours'] ) ? $config['scheduled_hours'] : array() );
		$scheduled_minute = $this->sanitize_operative_heartbeat_scheduled_minute( isset( $config['scheduled_minute'] ) ? $config['scheduled_minute'] : 0 );
		$scheduled_slots  = empty( $scheduled_hours )
			? ''
			: implode(
				', ',
				array_map(
					function ( $hour ) use ( $scheduled_minute ) {
						return sprintf( '%02d:%02d', intval( $hour ), $scheduled_minute );
					},
					$scheduled_hours
				)
			);

		$lines = array(
			'# ' . $brief_file,
			'',
			'## Active Task',
			'Created at: ' . $created_at,
			'Role: ' . $role,
			'Delivery channel: ' . ( '' !== $delivery ? $delivery : 'telegram' ),
			'Source channel: ' . ( '' !== $source ? $source : 'unknown' ),
			'',
			'Objective:',
			$task_brief,
			'',
			'Cadence expectation:',
			'- Run on each due operative heartbeat tick.',
			'- Configured interval: ' . rtrim( rtrim( number_format( $interval, 2, '.', '' ), '0' ), '.' ) . ' minutes.',
			'' !== $scheduled_slots ? '- Structural scheduled slots: ' . $scheduled_slots . ' local time. PHP will skip non-slot ticks before the LLM starts.' : '- No structural scheduled slot gate is configured.',
			'',
			'Current checkpoint:',
			'- Task is active and waiting for the next due tick.',
			'',
			'Next step for the next tick:',
			'- If this tick is in an allowed structural slot, execute the objective exactly as written and send the requested user-facing message/report through the configured delivery channel.',
			'',
			'Stop condition:',
			'' !== $stop ? $stop : 'Stop when the founder stops this task, the objective is complete, or the task becomes blocked.',
			'Hard end time (UTC):',
			'' !== $end_at_utc ? $end_at_utc : 'none',
			'',
			'Duplicate/conflict policy:',
			'- Do not create additional recurring jobs for this same objective.',
			'- If the objective conflicts with a newer founder instruction, follow the newer instruction and report the conflict.',
			'',
			'Verification:',
			'- A successful tick must produce the requested delivery message or a clear blocked/error status.',
			'',
			'Notes for the next run:',
			'- Preserve this brief until the stop condition is met.',
		);

		return implode( "\n", $lines );
	}

	/**
	 * Execute the operative heartbeat via full agent loop.
	 *
	 * @param string $chat_id             Primary Telegram chat ID.
	 * @param array  $recipient_chat_ids  All recipient chat IDs.
	 * @param string $instructions        Role-specific heartbeat brief content.
	 * @param array  $state               Current operative heartbeat state.
	 * @param string $role                Runtime role.
	 * @return array Status array.
	 */
	private function execute_operative_heartbeat( $chat_id, $recipient_chat_ids, $instructions, $state, $role = 'admin', $config = array() ) {
		if ( ! $this->turn_engine || ! $this->auth_manager || ! $this->openai_client ) {
			return array(
				'success' => false,
				'sent'    => false,
				'reason'  => 'no_runtime',
				'error'   => 'Operative heartbeat runtime is not available.',
			);
		}

		$role       = $this->normalize_operative_heartbeat_role( $role );
		$config     = is_array( $config ) ? $config : array();
		$delivery_channel = $this->normalize_operative_heartbeat_delivery_channel( isset( $config['delivery_channel'] ) ? $config['delivery_channel'] : '' );
		$brief_file = $this->get_operative_heartbeat_brief_filename( $role );
		$session_id = $this->build_operative_heartbeat_session_id( $role, $chat_id );
		if ( 'admin_ui' === $delivery_channel && ! empty( $config['delivery_session_id'] ) ) {
			$session_id = (string) $config['delivery_session_id'];
		}
		$context    = $this->prepare_context( $session_id );
		$context['session_id']             = $session_id;
		$context['chat_id']                = 'admin_ui' === $delivery_channel ? '' : $chat_id;
		$context['channel']                = $delivery_channel;
		$context['is_heartbeat']           = true;
		$context['is_operative_heartbeat'] = true;
		$context['user_role']              = 'stock_manager' === $role ? 'stock_manager' : 'admin';
		if ( 'admin_ui' === $delivery_channel ) {
			$context['admin_user_id'] = isset( $config['delivery_admin_user_id'] ) ? intval( $config['delivery_admin_user_id'] ) : 0;
			$context['admin_user_login'] = isset( $config['delivery_admin_user_login'] ) ? (string) $config['delivery_admin_user_login'] : '';
		}
		if ( 'stock_manager' === $role && $this->telegram && method_exists( $this->telegram, 'find_stock_manager' ) ) {
			$manager = $this->telegram->find_stock_manager( $chat_id );
			$context['stock_manager_label'] = ( $manager && ! empty( $manager['label'] ) ) ? (string) $manager['label'] : '';
		}

		$identity_profile = $this->workspace && method_exists( $this->workspace, 'get_identity_profile' )
			? (array) $this->workspace->get_identity_profile()
			: array();
		$assistant_name = ! empty( $identity_profile['assistant_name'] ) ? (string) $identity_profile['assistant_name'] : 'Niwa';

		// Build execution log context so the agent knows what it already did.
		$log_context = '';
		$exec_log    = isset( $state['execution_log'] ) && is_array( $state['execution_log'] ) ? $state['execution_log'] : array();
		if ( ! empty( $exec_log ) ) {
			$log_lines = array();
			foreach ( array_slice( $exec_log, -10 ) as $entry ) {
				$at      = isset( $entry['at'] ) ? wp_date( 'Y-m-d H:i', intval( $entry['at'] ) ) : '?';
				$summary = isset( $entry['summary'] ) ? (string) $entry['summary'] : '';
				$log_lines[] = '- [' . $at . '] ' . $summary;
			}
			$log_context = "\n\n[POSLEDNJA IZVRSENJA]\n" . implode( "\n", $log_lines ) . "\n[/POSLEDNJA IZVRSENJA]";
		}

		$site_local_time = wp_date( 'Y-m-d H:i:s (l)' );

		$task_prompt = implode( "\n", array(
			'[OPERATIVE HEARTBEAT TICK]',
			'',
			'Trenutno vreme (lokalno): ' . $site_local_time,
			'',
			'Ti si ' . $assistant_name . ' i ovo je tvoj operativni heartbeat tick' . ( 'stock_manager' === $role ? ' za stock-manager workflow.' : '.' ),
			'Procitaj zadatke ispod i odluci koje od njih treba da izvrsis SADA na osnovu trenutnog vremena i datuma.',
			'Koristi dostupne alate da uradis zadatke. Odgovori na jeziku korisnika.',
			'',
			'PRAVILA:',
			'- Izvrsi samo zadatke koji su relevantni za TRENUTNI datum i vreme.',
			'- Proveri POSLEDNJA IZVRSENJA da ne ponovis zadatak koji si vec uradila nedavno.',
			'- Ako ' . $brief_file . ' opisuje multi-run continuation task, nastavi od poslednjeg checkpointa i sacuvaj sledeci korak za naredni tick.',
			'- Ne pretvaraj ovo u novi recurring job: radi samo scope koji je eksplicitno zapisan u ' . $brief_file . ' i samo ako je task i dalje aktivan.',
			'- Ako postoji fallback on conflict u instrukcijama, koristi ga pre bilo kakvog write-upisa i ne pogađaj oko konflikta ili duplikata.',
			'- Na kraju, sazmi sta si uradila u jednu poruku za trenutni founder kanal (max 320 reci).',
			'- Ne spominji da te je heartbeat pokrenuo — javi rezultat prirodno.',
			'- Ako nijedan zadatak nije relevantan za trenutni momenat, ili si vec sve odradila, vrati samo JSON {"send":false,"message":""}.',
			'- Ako founder treba da dobije izvestaj, vrati samo JSON {"send":true,"message":"..."} sa porukom za trenutni founder kanal.',
			'',
			'[OPERATIVE HEARTBEAT TASKS]',
			$instructions,
			'[/OPERATIVE HEARTBEAT TASKS]',
			$log_context,
		) );

		$turn_options = array(
			'mode'             => 'main',
			'allow_writes'     => true,
			'force_action'     => false,
			'internal'         => true,
			'visibility'       => 'internal',
		);

		$result = $this->turn_engine->run(
			$this,
			$task_prompt,
			$context,
			$turn_options
		);

		if ( ! empty( $result['empty_response_diagnostic'] ) ) {
			return array(
				'success' => false,
				'sent'    => false,
				'reason'  => 'no_reply',
				'error'   => isset( $result['reply'] ) ? (string) $result['reply'] : 'Operative heartbeat returned an empty LLM diagnostic.',
			);
		}

		if ( empty( $result['success'] ) || empty( $result['reply'] ) ) {
			return array(
				'success' => false,
				'sent'    => false,
				'reason'  => 'no_reply',
				'error'   => isset( $result['error'] ) ? (string) $result['error'] : 'Operative heartbeat did not return a reply.',
			);
		}

		$reply = trim( (string) $result['reply'] );
		$decision = $this->parse_heartbeat_send_decision( $reply );
		if ( empty( $decision['send'] ) ) {
			return array( 'success' => true, 'sent' => false, 'reason' => 'operative_heartbeat_ok' );
		}

		$message     = $this->prepare_founder_telegram_message( (string) $decision['message'] );
		if ( 'admin_ui' === $delivery_channel ) {
			$delivered = $this->deliver_operative_heartbeat_admin_ui_message( $session_id, $message, $config );
			return array(
				'success'         => ! empty( $delivered['success'] ),
				'sent'            => ! empty( $delivered['success'] ),
				'reason'          => ! empty( $delivered['success'] ) ? 'sent' : 'send_failed',
				'message'         => $message,
				'sent_count'      => ! empty( $delivered['success'] ) ? 1 : 0,
				'recipient_count' => 1,
				'errors'          => empty( $delivered['success'] ) && ! empty( $delivered['error'] ) ? array( (string) $delivered['error'] ) : array(),
			);
		}

		$sent_count  = 0;
		$send_errors = array();
		foreach ( array_values( array_unique( array_filter( array_map( 'strval', (array) $recipient_chat_ids ) ) ) ) as $recipient_chat_id ) {
			$sent = $this->send_operative_heartbeat_channel_message( $delivery_channel, $recipient_chat_id, $message, array( 'session_id' => $session_id ) );
			if ( ! empty( $sent['success'] ) ) {
				$sent_count++;
			} elseif ( ! empty( $sent['error'] ) ) {
				$send_errors[] = (string) $sent['error'];
			}
		}

		return array(
			'success'         => $sent_count > 0,
			'sent'            => $sent_count > 0,
			'reason'          => $sent_count > 0 ? ( $sent_count === count( $recipient_chat_ids ) ? 'sent' : 'partial_send' ) : 'send_failed',
			'message'         => $message,
			'sent_count'      => $sent_count,
			'recipient_count' => count( $recipient_chat_ids ),
			'errors'          => $send_errors,
		);
	}

	private function deliver_operative_heartbeat_admin_ui_message( $session_id, $message, $config = array() ) {
		$session_id = trim( (string) $session_id );
		$message    = trim( (string) $message );
		if ( '' === $session_id || '' === $message ) {
			return array(
				'success' => false,
				'error'   => 'Admin UI heartbeat delivery nema session ili poruku.',
			);
		}

		$config = is_array( $config ) ? $config : array();
		$meta   = array(
			'channel'                => 'admin_ui',
			'source'                 => 'operative_heartbeat',
			'is_heartbeat'           => true,
			'is_operative_heartbeat' => true,
		);
		if ( ! empty( $config['delivery_admin_user_id'] ) ) {
			$meta['admin_user_id'] = intval( $config['delivery_admin_user_id'] );
		}
		if ( ! empty( $config['delivery_admin_user_login'] ) ) {
			$meta['admin_user_login'] = (string) $config['delivery_admin_user_login'];
		}

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

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

	private function build_founder_heartbeat_session_id( $chat_id, $fallback = 'admin-telegram-heartbeat' ) {
		$chat_id = preg_replace( '/[^0-9\\-]/', '', (string) $chat_id );
		if ( '' === $chat_id ) {
			return (string) $fallback;
		}

		return 'admin-telegram-' . $chat_id;
	}

	private function normalize_operative_heartbeat_role( $role ) {
		return 'stock_manager' === sanitize_key( (string) $role ) ? 'stock_manager' : 'admin';
	}

	private function normalize_operative_heartbeat_delivery_channel( $channel ) {
		$channel = sanitize_key( (string) $channel );
		return in_array( $channel, array( 'telegram', 'whatsapp', 'admin_ui' ), true ) ? $channel : 'telegram';
	}

	private function get_operative_heartbeat_brief_filename( $role = 'admin' ) {
		return 'stock_manager' === $this->normalize_operative_heartbeat_role( $role ) ? 'STOCK_OPERATIVE_HEARTBEAT.md' : 'OPERATIVE_HEARTBEAT.md';
	}

	private function resolve_operative_heartbeat_recipient_chat_ids( $role, $settings, $config = array() ) {
		$role = $this->normalize_operative_heartbeat_role( $role );
		$config = is_array( $config ) ? $config : array();
		$delivery_channel = $this->normalize_operative_heartbeat_delivery_channel( isset( $config['delivery_channel'] ) ? $config['delivery_channel'] : '' );
		if ( 'admin' === $role && 'admin_ui' === $delivery_channel ) {
			$session_id = isset( $config['delivery_session_id'] ) ? trim( (string) $config['delivery_session_id'] ) : '';
			return '' !== $session_id ? array( $session_id ) : array();
		}
		$delivery_chat_id = isset( $config['delivery_chat_id'] ) ? trim( (string) $config['delivery_chat_id'] ) : '';
		if ( '' !== $delivery_chat_id ) {
			return array( $delivery_chat_id );
		}

		if ( 'stock_manager' !== $role ) {
			$chat_id = trim( (string) ( isset( $settings['owner_chat_id'] ) ? $settings['owner_chat_id'] : '' ) );
			return '' !== $chat_id ? array( $chat_id ) : array();
		}

		if ( ! $this->telegram || ! method_exists( $this->telegram, 'get_stock_managers' ) ) {
			return array();
		}

		$chat_ids = array();
		foreach ( (array) $this->telegram->get_stock_managers() as $manager ) {
			if ( ! is_array( $manager ) || empty( $manager['chat_id'] ) ) {
				continue;
			}
			$chat_ids[] = trim( (string) $manager['chat_id'] );
		}

		return array_values( array_unique( array_filter( $chat_ids ) ) );
	}

	private function build_operative_heartbeat_session_id( $role, $chat_id = '' ) {
		$role = $this->normalize_operative_heartbeat_role( $role );
		if ( 'stock_manager' === $role ) {
			$chat_id = preg_replace( '/[^0-9\\-]/', '', (string) $chat_id );
			return '' !== $chat_id ? 'admin-telegram-stock-operative-heartbeat-' . $chat_id : 'admin-telegram-stock-operative-heartbeat';
		}

		return $this->build_founder_heartbeat_session_id( $chat_id, 'admin-telegram-operative-heartbeat' );
	}

	/**
	 * Get operative heartbeat configuration.
	 *
	 * @return array Configuration with defaults.
	 */
	public function get_operative_heartbeat_config( $role = 'admin', $force_fresh = false ) {
		$option_name = $this->get_operative_heartbeat_config_option_name( $role );
		if ( $force_fresh && function_exists( 'wp_cache_delete' ) ) {
			wp_cache_delete( $option_name, 'options' );
		}
		$config = get_option( $option_name, array() );
		if ( ! is_array( $config ) ) {
			$config = array();
		}

		return wp_parse_args(
			$config,
			array(
				'enabled'          => false,
				'interval_minutes' => 60.0,
				'scheduled_hours'  => array(),
				'scheduled_minute' => 0,
				'delivery_channel' => 'telegram',
				'delivery_chat_id' => '',
				'delivery_session_id' => '',
				'delivery_admin_user_id' => 0,
				'delivery_admin_user_login' => '',
				'end_at_utc'       => '',
				'execution_mode'   => 'dynamic_agent',
				'static_message'   => '',
			)
		);
	}

	private function get_operative_heartbeat_config_option_name( $role = 'admin' ) {
		return 'stock_manager' === $this->normalize_operative_heartbeat_role( $role )
			? Niwa_AI_Constants::OPT_AUTONOMY_STOCK_OPERATIVE_HB_CONFIG
			: Niwa_AI_Constants::OPT_AUTONOMY_OPERATIVE_HB_CONFIG;
	}

	/**
	 * Save operative heartbeat configuration.
	 *
	 * @param array $config Configuration to save.
	 */
	public function save_operative_heartbeat_config( $config, $role = 'admin' ) {
		$role = $this->normalize_operative_heartbeat_role( $role );
		$option_name = $this->get_operative_heartbeat_config_option_name( $role );
		$previous_config = $this->get_operative_heartbeat_config( $role, true );
		$enabled         = ! empty( $config['enabled'] );
		$normalized_config = array(
			'enabled'          => $enabled,
			'interval_minutes' => max( 0.5, round( floatval( isset( $config['interval_minutes'] ) ? $config['interval_minutes'] : 60 ), 2 ) ),
			'scheduled_hours'  => $this->sanitize_operative_heartbeat_scheduled_hours( isset( $config['scheduled_hours'] ) ? $config['scheduled_hours'] : array() ),
			'scheduled_minute' => $this->sanitize_operative_heartbeat_scheduled_minute( isset( $config['scheduled_minute'] ) ? $config['scheduled_minute'] : 0 ),
			'delivery_channel' => $this->normalize_operative_heartbeat_delivery_channel( isset( $config['delivery_channel'] ) ? $config['delivery_channel'] : '' ),
			'delivery_chat_id' => isset( $config['delivery_chat_id'] ) ? sanitize_text_field( (string) $config['delivery_chat_id'] ) : '',
			'delivery_session_id' => isset( $config['delivery_session_id'] ) ? sanitize_text_field( (string) $config['delivery_session_id'] ) : '',
			'delivery_admin_user_id' => isset( $config['delivery_admin_user_id'] ) ? intval( $config['delivery_admin_user_id'] ) : 0,
			'delivery_admin_user_login' => isset( $config['delivery_admin_user_login'] ) ? sanitize_text_field( (string) $config['delivery_admin_user_login'] ) : '',
			'end_at_utc'       => $enabled && ! empty( $config['end_at_utc'] ) ? $this->sanitize_operative_heartbeat_end_at_utc( (string) $config['end_at_utc'] ) : '',
			'execution_mode'   => isset( $config['execution_mode'] ) && 'static_exact_message' === sanitize_key( (string) $config['execution_mode'] ) ? 'static_exact_message' : 'dynamic_agent',
			'static_message'   => isset( $config['static_message'] ) ? sanitize_textarea_field( (string) $config['static_message'] ) : '',
		);
		$previous_signature = $this->build_operative_heartbeat_config_signature( $previous_config );
		$new_signature      = $this->build_operative_heartbeat_config_signature( $normalized_config );

		update_option(
			$option_name,
			$normalized_config,
			false
		);

		if ( $enabled ) {
			$state_option_name = 'stock_manager' === $role
				? Niwa_AI_Constants::OPT_AUTONOMY_STOCK_OPERATIVE_HB_STATE
				: Niwa_AI_Constants::OPT_AUTONOMY_OPERATIVE_HB_STATE;
			$state = get_option( $state_option_name, array() );
			if ( ! is_array( $state ) ) {
				$state = array();
			}
			if ( empty( $state['last_run_at'] ) || $previous_signature !== $new_signature || ( isset( $state['config_signature'] ) && (string) $state['config_signature'] !== $new_signature ) ) {
				$state['last_run_at'] = time();
				$state['last_slot_key'] = '';
				$state['next_retry_at'] = 0;
			}
			if ( empty( $state['last_attempt_at'] ) ) {
				$state['last_attempt_at'] = 0;
			}
			$state['config_signature'] = $new_signature;
			$state['last_status']     = 'scheduled';
			update_option( $state_option_name, $state, false );

			if ( function_exists( 'wp_schedule_single_event' ) && class_exists( 'Niwa_AI_Admin_Autonomy' ) ) {
				wp_schedule_single_event( time() + 5, Niwa_AI_Admin_Autonomy::BACKGROUND_TICK_HOOK );
			}
			if ( function_exists( 'do_action' ) ) {
				do_action( 'niwa_ai_admin_autonomy_wake_background', 'operative-heartbeat' );
			}
		}

		$this->sync_operative_heartbeat_scheduler_interval();
	}

	private function build_operative_heartbeat_config_signature( $config ) {
		$config = is_array( $config ) ? $config : array();
		$payload = array(
			'enabled'          => ! empty( $config['enabled'] ),
			'interval_minutes' => max( 0.5, round( floatval( isset( $config['interval_minutes'] ) ? $config['interval_minutes'] : 60 ), 2 ) ),
			'scheduled_hours'  => $this->sanitize_operative_heartbeat_scheduled_hours( isset( $config['scheduled_hours'] ) ? $config['scheduled_hours'] : array() ),
			'scheduled_minute' => $this->sanitize_operative_heartbeat_scheduled_minute( isset( $config['scheduled_minute'] ) ? $config['scheduled_minute'] : 0 ),
			'delivery_channel' => $this->normalize_operative_heartbeat_delivery_channel( isset( $config['delivery_channel'] ) ? $config['delivery_channel'] : '' ),
			'delivery_chat_id' => isset( $config['delivery_chat_id'] ) ? sanitize_text_field( (string) $config['delivery_chat_id'] ) : '',
			'delivery_session_id' => isset( $config['delivery_session_id'] ) ? sanitize_text_field( (string) $config['delivery_session_id'] ) : '',
			'delivery_admin_user_id' => isset( $config['delivery_admin_user_id'] ) ? intval( $config['delivery_admin_user_id'] ) : 0,
			'end_at_utc'       => isset( $config['end_at_utc'] ) ? $this->sanitize_operative_heartbeat_end_at_utc( (string) $config['end_at_utc'] ) : '',
			'execution_mode'   => isset( $config['execution_mode'] ) && 'static_exact_message' === sanitize_key( (string) $config['execution_mode'] ) ? 'static_exact_message' : 'dynamic_agent',
			'static_message'   => isset( $config['static_message'] ) ? sanitize_textarea_field( (string) $config['static_message'] ) : '',
		);

		return md5( function_exists( 'wp_json_encode' ) ? wp_json_encode( $payload ) : json_encode( $payload ) );
	}

	public function normalize_operative_heartbeat_end_at_from_args( $args ) {
		$args = is_array( $args ) ? $args : array();

		$end_at_utc = isset( $args['end_at_utc'] ) ? trim( (string) $args['end_at_utc'] ) : '';
		if ( '' !== $end_at_utc ) {
			return $this->sanitize_operative_heartbeat_end_at_utc( $end_at_utc );
		}

		$end_at_local = '';
		foreach ( array( 'end_at_local', 'end_at' ) as $key ) {
			if ( '' === $end_at_local && isset( $args[ $key ] ) ) {
				$end_at_local = trim( (string) $args[ $key ] );
			}
		}
		if ( '' === $end_at_local ) {
			return '';
		}

		$timezone_string = isset( $args['timezone'] ) ? trim( (string) $args['timezone'] ) : '';
		if ( '' === $timezone_string && function_exists( 'wp_timezone_string' ) ) {
			$timezone_string = wp_timezone_string();
		}
		if ( '' === $timezone_string ) {
			$timezone_string = 'UTC';
		}

		try {
			$timezone = new DateTimeZone( $timezone_string );
		} catch ( Throwable $e ) {
			$timezone = new DateTimeZone( 'UTC' );
		}

		$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 ) {
			$datetime = DateTimeImmutable::createFromFormat( $format, $end_at_local, $timezone );
			if ( $datetime instanceof DateTimeImmutable ) {
				return $datetime->setTimezone( new DateTimeZone( 'UTC' ) )->format( 'Y-m-d H:i:s' );
			}
		}

		try {
			return ( new DateTimeImmutable( $end_at_local, $timezone ) )->setTimezone( new DateTimeZone( 'UTC' ) )->format( 'Y-m-d H:i:s' );
		} catch ( Throwable $e ) {
			return '';
		}
	}

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

		try {
			return ( new DateTimeImmutable( $value, new DateTimeZone( 'UTC' ) ) )->setTimezone( new DateTimeZone( 'UTC' ) )->format( 'Y-m-d H:i:s' );
		} catch ( Throwable $e ) {
			return '';
		}
	}

	private function is_operative_heartbeat_config_expired( $config ) {
		$end_at_utc = isset( $config['end_at_utc'] ) ? $this->sanitize_operative_heartbeat_end_at_utc( (string) $config['end_at_utc'] ) : '';
		if ( '' === $end_at_utc ) {
			return false;
		}

		try {
			$end = new DateTimeImmutable( $end_at_utc, new DateTimeZone( 'UTC' ) );
			return $end->getTimestamp() <= time();
		} catch ( Throwable $e ) {
			return false;
		}
	}

	public function sync_operative_heartbeat_scheduler_interval() {
		if ( ! class_exists( 'Niwa_AI_Admin_Autonomy' ) || ! function_exists( 'wp_schedule_event' ) ) {
			return array( 'success' => false, 'reason' => 'cron_unavailable' );
		}

		$user_seconds = 60;
		if ( class_exists( 'Niwa_AI_Performance_Settings' ) ) {
			$perf         = Niwa_AI_Performance_Settings::get();
			$user_seconds = isset( $perf['background_tick_seconds'] ) ? max( 30, intval( $perf['background_tick_seconds'] ) ) : 60;
		}

		$active_seconds = array();
		foreach ( array( 'admin', 'stock_manager' ) as $role ) {
			$config = $this->get_operative_heartbeat_config( $role );
			if ( empty( $config['enabled'] ) || $this->is_operative_heartbeat_config_expired( $config ) ) {
				continue;
			}

			$active_seconds[] = max( 30, intval( round( max( 0.5, floatval( isset( $config['interval_minutes'] ) ? $config['interval_minutes'] : 60 ) ) * MINUTE_IN_SECONDS ) ) );
		}

		$target_seconds  = empty( $active_seconds ) ? $user_seconds : min( $user_seconds, min( $active_seconds ) );
		$target_slug     = class_exists( 'Niwa_AI_Performance_Settings' )
			? Niwa_AI_Performance_Settings::schedule_slug_for_max_interval( $target_seconds )
			: ( $target_seconds <= 30 ? 'niwa_ai_thirty_seconds' : 'niwa_ai_one_minute' );
		$target_interval = class_exists( 'Niwa_AI_Performance_Settings' )
			? Niwa_AI_Performance_Settings::interval_for_schedule_slug( $target_slug )
			: $target_seconds;

		$hook         = Niwa_AI_Admin_Autonomy::BACKGROUND_TICK_HOOK;
		$current_slug = $this->get_recurring_cron_schedule_slug( $hook );
		if ( $current_slug !== $target_slug ) {
			$this->unschedule_recurring_cron_events( $hook );
			wp_schedule_event( time() + min( 60, max( 30, intval( $target_interval ) ) ), $target_slug, $hook );
		}

		$override = array(
			'active'         => ! empty( $active_seconds ) && $target_seconds < $user_seconds,
			'target_seconds' => intval( $target_interval ),
			'schedule_slug'  => $target_slug,
			'user_seconds'   => $user_seconds,
			'active_seconds' => $active_seconds,
			'updated_at'     => time(),
		);
		return array_merge( array( 'success' => true ), $override );
	}

	private function get_recurring_cron_schedule_slug( $hook ) {
		if ( ! function_exists( '_get_cron_array' ) ) {
			return '';
		}

		$cron = _get_cron_array();
		foreach ( (array) $cron as $timestamp => $hooks ) {
			if ( empty( $hooks[ $hook ] ) || ! is_array( $hooks[ $hook ] ) ) {
				continue;
			}

			foreach ( $hooks[ $hook ] as $event ) {
				if ( ! empty( $event['schedule'] ) ) {
					return (string) $event['schedule'];
				}
			}
		}

		return '';
	}

	private function unschedule_recurring_cron_events( $hook ) {
		if ( ! function_exists( '_get_cron_array' ) || ! function_exists( 'wp_unschedule_event' ) ) {
			return;
		}

		$cron = _get_cron_array();
		foreach ( (array) $cron as $timestamp => $hooks ) {
			if ( empty( $hooks[ $hook ] ) || ! is_array( $hooks[ $hook ] ) ) {
				continue;
			}

			foreach ( $hooks[ $hook ] as $event ) {
				if ( empty( $event['schedule'] ) ) {
					continue;
				}

				$args = isset( $event['args'] ) && is_array( $event['args'] ) ? $event['args'] : array();
				wp_unschedule_event( intval( $timestamp ), $hook, $args );
			}
		}
	}

	/**
	 * Get operative heartbeat state.
	 *
	 * @return array State with defaults.
	 */
	public function get_operative_heartbeat_state( $role = 'admin' ) {
		$option_name = 'stock_manager' === $this->normalize_operative_heartbeat_role( $role )
			? Niwa_AI_Constants::OPT_AUTONOMY_STOCK_OPERATIVE_HB_STATE
			: Niwa_AI_Constants::OPT_AUTONOMY_OPERATIVE_HB_STATE;
		$state = get_option( $option_name, array() );
		if ( ! is_array( $state ) ) {
			$state = array();
		}

		return wp_parse_args(
			$state,
			array(
				'last_run_at'     => 0,
				'last_sent_at'    => 0,
				'last_status'     => '',
				'last_summary'    => '',
				'run_count'       => 0,
				'last_attempt_at' => 0,
				'next_retry_at'  => 0,
				'last_slot_key'   => '',
				'config_signature' => '',
				'execution_log'   => array(),
			)
		);
	}

	/**
	 * Save operative heartbeat state.
	 *
	 * @param array $state State to save.
	 */
	private function save_operative_heartbeat_state( $state, $role = 'admin' ) {
		$option_name = 'stock_manager' === $this->normalize_operative_heartbeat_role( $role )
			? Niwa_AI_Constants::OPT_AUTONOMY_STOCK_OPERATIVE_HB_STATE
			: Niwa_AI_Constants::OPT_AUTONOMY_OPERATIVE_HB_STATE;
		$current = get_option( $option_name, array() );
		$current = is_array( $current ) ? $current : array();
		$incoming_signature = isset( $state['config_signature'] ) ? (string) $state['config_signature'] : '';
		$current_signature  = isset( $current['config_signature'] ) ? (string) $current['config_signature'] : '';
		if ( '' !== $incoming_signature && '' !== $current_signature && ! hash_equals( $current_signature, $incoming_signature ) ) {
			$state['config_signature'] = $current_signature;
			$state['last_run_at']      = intval( isset( $current['last_run_at'] ) ? $current['last_run_at'] : 0 );
			$state['last_slot_key']    = isset( $current['last_slot_key'] ) ? (string) $current['last_slot_key'] : '';
		}

		update_option(
			$option_name,
			array(
				'last_run_at'   => intval( isset( $state['last_run_at'] ) ? $state['last_run_at'] : 0 ),
				'last_sent_at'  => intval( isset( $state['last_sent_at'] ) ? $state['last_sent_at'] : 0 ),
				'last_status'   => isset( $state['last_status'] ) ? (string) $state['last_status'] : '',
				'last_summary'  => isset( $state['last_summary'] ) ? (string) $state['last_summary'] : '',
				'run_count'     => intval( isset( $state['run_count'] ) ? $state['run_count'] : 0 ),
				'last_attempt_at' => intval( isset( $state['last_attempt_at'] ) ? $state['last_attempt_at'] : 0 ),
				'next_retry_at' => intval( isset( $state['next_retry_at'] ) ? $state['next_retry_at'] : 0 ),
				'last_slot_key' => isset( $state['last_slot_key'] ) ? (string) $state['last_slot_key'] : '',
				'config_signature' => isset( $state['config_signature'] ) ? (string) $state['config_signature'] : '',
				'execution_log' => isset( $state['execution_log'] ) && is_array( $state['execution_log'] ) ? array_slice( $state['execution_log'], -20 ) : array(),
			),
			false
		);
	}

}
