<?php
/**
 * Web loopback/local CLI runner execution.
 *
 * @package Niwa_AI
 */

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

class Niwa_AI_Admin_Autonomy_Loopback_Runner_Executor {
	/** @var object|null */
	private $telegram;

	/** @var array<string,callable> */
	private $callbacks;

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

	public function execute( $reason, $runner_id, $transport, $active_ttl ) {
		$reason    = sanitize_key( (string) $reason );
		$runner_id = sanitize_text_field( (string) $runner_id );
		$transport = 'local_cli' === (string) $transport ? 'local_cli' : 'web_loopback';
		$previous_runner_id = (string) $this->call( 'get_active_loopback_runner_id', array(), '' );
		$this->call( 'set_active_loopback_runner_id', array( $runner_id ) );

		$last_error             = '';
		$actor_messages_handled = 0;
		$poll_result            = array( 'success' => true );
		$checkpoint             = 'received';

		$this->debug(
			'loopback_runner_received',
			array(
				'reason'        => $reason,
				'lock_acquired' => true,
				'transport'     => $transport,
			)
		);

		register_shutdown_function(
			function () use ( &$checkpoint, $reason, $runner_id, $transport ) {
				$this->recover_shutdown_if_needed( $checkpoint, $reason, $runner_id, $transport );
			}
		);

		try {
			$state                     = $this->telegram->get_runner_state();
			$state['last_seen_at']     = time();
			$state['active_until']     = time() + max( 10, intval( $active_ttl ) );
			$state['active_runner_id'] = $runner_id;
			$state['active_transport'] = $transport;
			$state['last_transport']   = $transport;
			$state['active_pid']       = 'local_cli' === $transport ? getmypid() : 0;
			$state['last_error']       = '';
			$this->telegram->update_runner_state( $state );

			$checkpoint  = 'about_to_poll';
			$poll_result = $this->call( 'poll_telegram_updates', array(), array( 'success' => true ) );
			$checkpoint  = 'poll_returned';
			$actor_messages_handled = isset( $poll_result['actor_messages'] ) ? intval( $poll_result['actor_messages'] ) : 0;
			$this->debug(
				'loopback_runner_poll_result',
				array(
					'reason'         => $reason,
					'success'        => ! empty( $poll_result['success'] ),
					'note'           => isset( $poll_result['note'] ) ? (string) $poll_result['note'] : '',
					'handled'        => isset( $poll_result['handled'] ) ? intval( $poll_result['handled'] ) : 0,
					'ignored'        => isset( $poll_result['ignored'] ) ? intval( $poll_result['ignored'] ) : 0,
					'actor_messages' => $actor_messages_handled,
					'transport'      => $transport,
					'errors'         => isset( $poll_result['errors'] ) ? array_values( array_filter( (array) $poll_result['errors'] ) ) : array(),
				)
			);
			if ( empty( $poll_result['success'] ) ) {
				$last_error = isset( $poll_result['error'] ) ? (string) $poll_result['error'] : 'poll_failed';
			} elseif ( ! empty( $poll_result['errors'] ) ) {
				$last_error = implode( ' | ', array_slice( array_values( array_filter( (array) $poll_result['errors'] ) ), 0, 3 ) );
			}

			if ( 'background-wake' === $reason ) {
				$checkpoint = 'automation_loop';
				$automation_errors = $this->call( 'run_background_wake_loop', array(), array() );
				if ( ! empty( $automation_errors ) ) {
					$last_error = '' !== $last_error
						? $last_error . ' | ' . implode( ' | ', array_slice( $automation_errors, 0, 3 ) )
						: implode( ' | ', array_slice( $automation_errors, 0, 3 ) );
				}
			}
		} catch ( Throwable $e ) {
			$last_error  = $this->call( 'format_runtime_exception_message', array( $e ), $e->getMessage() );
			$poll_result = array(
				'success' => false,
				'error'   => $last_error,
			);
			$this->debug(
				'loopback_runner_exception',
				array(
					'reason'    => $reason,
					'transport' => $transport,
					'error'     => $last_error,
				)
			);
		} finally {
			$state                 = $this->telegram->get_runner_state();
			$state['last_seen_at'] = time();
			$state['last_error']   = $last_error;
			$wake_pending          = ! empty( $state['wake_pending'] );
			$wake_reason           = isset( $state['wake_reason'] ) ? sanitize_key( (string) $state['wake_reason'] ) : '';
			if ( ! empty( $this->call( 'runner_state_is_owned_by_current_request', array( $state, $runner_id ), false ) ) ) {
				$state['active_until']     = 0;
				$state['active_runner_id'] = '';
				$state['active_transport'] = '';
				$state['active_pid']       = 0;
				$state['wake_pending']     = 0;
				$state['wake_reason']      = '';
			} else {
				$wake_pending = false;
				$wake_reason  = '';
			}
			$this->telegram->update_runner_state( $state );
			$this->call( 'set_active_loopback_runner_id', array( $previous_runner_id ) );
		}

		$has_pending_actors  = ! empty( $this->call( 'has_pending_actor_messages', array(), false ) );
		$has_pending_buffers = ! empty( $this->call( 'has_pending_transport_buffers', array(), false ) );
		$webhook_active      = ! empty( $this->call( 'is_telegram_webhook_active', array(), false ) );
		$should_chain        = $wake_pending
			|| $has_pending_buffers
			|| ( $has_pending_actors && $actor_messages_handled > 0 );
		if ( $has_pending_actors && $actor_messages_handled < 1 ) {
			$this->call( 'schedule_runner_retry' );
		}

		if ( 'background-wake' === $reason && ! empty( $this->call( 'has_due_scheduled_jobs_soon', array( 20 ), false ) ) ) {
			$should_chain = true;
		}

		$this->debug(
			'loopback_runner_finished',
			array(
				'reason'              => $reason,
				'has_pending_actors'  => (bool) $has_pending_actors,
				'has_pending_buffers' => (bool) $has_pending_buffers,
				'webhook_active'      => (bool) $webhook_active,
				'wake_pending'        => (bool) $wake_pending,
				'actor_messages'      => $actor_messages_handled,
				'will_chain'          => (bool) $should_chain,
				'last_error'          => $last_error,
				'transport'           => $transport,
			)
		);

		$chain_result = array();
		if ( $should_chain ) {
			$next_reason = 'admin-ui' === $wake_reason
				? 'admin-ui'
				: ( ( 'background-wake' === $reason || 'background-wake' === $wake_reason )
					? 'background-wake'
					: ( $has_pending_buffers ? 'webhook-buffer' : 'chain' ) );
			$chain_result = $this->call(
				'dispatch_loopback_runner',
				array(
					$next_reason,
					'background-wake' === $next_reason ? 75 : ( 'webhook-buffer' === $next_reason ? 30 : $active_ttl ),
				),
				array()
			);
		}

		$checkpoint = 'finished';

		return array(
			'success'        => '' === $last_error,
			'transport'      => $transport,
			'poll_result'    => $poll_result,
			'actor_messages' => $actor_messages_handled,
			'will_chain'     => (bool) $should_chain,
			'chain_result'   => $chain_result,
			'last_error'     => $last_error,
		);
	}

	private function recover_shutdown_if_needed( $checkpoint, $reason, $runner_id, $transport ) {
		$error    = error_get_last();
		$is_fatal = $error && in_array( $error['type'], array( E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR ), true );
		if ( ! $is_fatal && 'finished' === $checkpoint ) {
			return;
		}

		$this->debug(
			'loopback_runner_shutdown',
			array(
				'reason'     => $reason,
				'checkpoint' => $checkpoint,
				'fatal'      => (bool) $is_fatal,
				'transport'  => $transport,
				'error_type' => $is_fatal && $error ? intval( $error['type'] ) : 0,
				'error_msg'  => $is_fatal && $error ? (string) $error['message'] : '',
				'error_file' => $is_fatal && $error ? basename( (string) $error['file'] ) : '',
				'error_line' => $is_fatal && $error ? intval( $error['line'] ) : 0,
			)
		);

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

		try {
			$state = $this->telegram->get_runner_state();
			if ( ! empty( $this->call( 'runner_state_is_owned_by_current_request', array( $state, $runner_id ), false ) ) ) {
				$state['active_until']     = 0;
				$state['active_runner_id'] = '';
				$state['active_transport'] = '';
				$state['active_pid']       = 0;
				$state['last_error']       = $is_fatal && $error
					? 'fatal:' . basename( (string) $error['file'] ) . ':' . intval( $error['line'] ) . ' ' . substr( (string) $error['message'], 0, 200 )
					: 'died_at:' . $checkpoint;
				$this->telegram->update_runner_state( $state );
			}
		} catch ( Throwable $e ) {
			// Shutdown phase, best effort only.
		}
	}

	private function debug( $event, $context = array() ) {
		$this->call( 'debug_logger', array( $event, $context ) );
	}

	private function call( $name, $args = array(), $default = null ) {
		return Niwa_AI_Admin_Autonomy_Callbacks::call( $this->callbacks, $name, $args, $default );
	}
}
