<?php
/**
 * Agent lifecycle hooks — cron scheduling and event listeners.
 *
 * Handles binding, executing, and cleaning up scheduled tasks and
 * event listeners for all active agents. Extracted from the Plugin
 * class to keep agent-builder.php focused on bootstrapping.
 *
 * @package Agent_Builder
 */

declare(strict_types=1);

namespace Agentic;

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

/**
 * Agent_Lifecycle
 *
 * All methods are static so they can be registered directly with
 * add_action() using array( Agent_Lifecycle::class, 'method' ).
 */
class Agent_Lifecycle {

	/**
	 * Bind cron hooks for all active agents' scheduled tasks.
	 *
	 * Called on 'agentic_agents_loaded' action.
	 *
	 * @return void
	 */
	public static function bind_cron_hooks(): void {
		$registry  = \Agentic_Agent_Registry::get_instance();
		$instances = $registry->get_all_instances();

		foreach ( $instances as $agent ) {
			$tasks = $agent->get_scheduled_tasks();

			foreach ( $tasks as $task ) {
				$hook = $agent->get_cron_hook( $task['id'] );

				add_action(
					$hook,
					static function () use ( $agent, $task ) {
						self::execute_scheduled_task( $agent, $task );
					}
				);
			}
		}
	}

	/**
	 * Execute a scheduled task with outcome logging and optional LLM routing.
	 *
	 * If the task defines a 'prompt' field and the LLM is configured, the task
	 * runs through Agent_Controller::run_autonomous_task() (full AI reasoning
	 * with tool calls). Otherwise it falls back to calling the agent's callback
	 * method directly.
	 *
	 * Every execution is wrapped with start/complete/error audit logging including
	 * duration timing, so admins can see exactly what happened and how long it took.
	 *
	 * @param Agent_Base $agent Agent instance.
	 * @param array      $task  Task definition from get_scheduled_tasks().
	 * @return void
	 */
	public static function execute_scheduled_task( Agent_Base $agent, array $task ): void {
		\Plugin::get_instance()->load_chat_components();

		$audit    = new Audit_Log();
		$start    = microtime( true );
		$agent_id = $agent->get_id();
		$mode     = ! empty( $task['prompt'] ) ? 'autonomous' : ( ! empty( $task['tool'] ) ? 'tool' : 'direct' );

		// Log task start.
		$audit->log(
			$agent_id,
			'scheduled_task_start',
			$task['id'],
			array(
				'task_name' => $task['name'],
				'schedule'  => $task['schedule'],
				'mode'      => $mode,
			)
		);

		try {
			$result = null;

			// If task has a prompt, route through LLM for autonomous execution.
			if ( ! empty( $task['prompt'] ) ) {
				$controller = new Agent_Controller();
				$controller->set_invocation_context( 'cron' );
				$result = $controller->run_autonomous_task( $agent, $task['prompt'], $task['id'] );
			}

			// Fallback 1: declarative tool mode — run one reviewed tool directly
			// (no LLM), through the same risk/permission gate as a chat turn.
			if ( null === $result && ! empty( $task['tool'] ) ) {
				$result = self::run_automation_tool( $agent, $task, array(), 'cron' );
			}

			// Fallback 2: bundled-PHP-agent callback method. Manifests never carry
			// a callback (the validator strips it), so this only fires for reviewed
			// PHP agents.
			if ( null === $result && ! empty( $task['callback'] ) && method_exists( $agent, $task['callback'] ) ) {
				call_user_func( array( $agent, $task['callback'] ) );
				$result = array(
					'mode'   => 'direct',
					'status' => 'completed',
				);
			}

			$duration = round( microtime( true ) - $start, 3 );

			// Log task completion.
			$audit->log(
				$agent_id,
				'scheduled_task_complete',
				$task['id'],
				array(
					'task_name'  => $task['name'],
					'duration_s' => $duration,
					'mode'       => $mode,
					'result'     => is_array( $result ) ? substr( wp_json_encode( $result ), 0, 1000 ) : null,
				)
			);
		} catch ( \Throwable $e ) {
			$duration = round( microtime( true ) - $start, 3 );

			// Log task error.
			$audit->log(
				$agent_id,
				'scheduled_task_error',
				$task['id'],
				array(
					'task_name'  => $task['name'],
					'duration_s' => $duration,
					'error'      => $e->getMessage(),
					'file'       => $e->getFile() . ':' . $e->getLine(),
				)
			);

			if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
				// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Debug logging only when WP_DEBUG is enabled.
				error_log(
					sprintf(
						'Agentic scheduled task error (%s/%s): %s',
						$agent_id,
						$task['id'],
						$e->getMessage()
					)
				);
			}
		}
	}

	/**
	 * Bind WordPress action hooks for all active agents' event listeners.
	 *
	 * Called on 'agentic_agents_loaded' action.
	 *
	 * @return void
	 */
	public static function bind_event_listeners(): void {
		$registry  = \Agentic_Agent_Registry::get_instance();
		$instances = $registry->get_all_instances();

		foreach ( $instances as $agent ) {
			$listeners = $agent->get_event_listeners();

			foreach ( $listeners as $listener ) {
				$priority      = $listener['priority'] ?? 10;
				$accepted_args = $listener['accepted_args'] ?? 1;

				add_action(
					$listener['hook'],
					static function () use ( $agent, $listener ) {
						$args = func_get_args();
						self::execute_event_listener( $agent, $listener, $args );
					},
					$priority,
					$accepted_args
				);
			}
		}

		// Bind user-defined triggers created via the Deployment → Event Listeners form.
		$user_triggers = get_option( 'agentic_user_event_triggers', array() );
		if ( ! is_array( $user_triggers ) ) {
			$user_triggers = array();
		}

		foreach ( $user_triggers as $trigger ) {
			$agent = $registry->get_agent_instance( $trigger['agent_slug'] ?? '' );
			if ( ! $agent ) {
				continue;
			}

			$listener = array(
				'id'       => $trigger['id'],
				'name'     => $trigger['name'],
				'hook'     => $trigger['hook'],
				'prompt'   => $trigger['prompt'],
				'priority' => $trigger['priority'] ?? 10,
			);

			add_action(
				$trigger['hook'],
				static function () use ( $agent, $listener ) {
					$args = func_get_args();
					self::execute_event_listener( $agent, $listener, $args );
				},
				$trigger['priority'] ?? 10,
				1
			);
		}
	}

	/**
	 * Execute an event listener with outcome logging.
	 *
	 * For direct mode: calls the agent callback synchronously.
	 * For autonomous mode (prompt defined): queues an async LLM task via
	 * wp_schedule_single_event so it doesn't block the current request.
	 *
	 * @param Agent_Base $agent    Agent instance.
	 * @param array      $listener Listener definition.
	 * @param array      $args     WordPress hook arguments.
	 * @return void
	 */
	public static function execute_event_listener( Agent_Base $agent, array $listener, array $args ): void {
		$audit    = new Audit_Log();
		$start    = microtime( true );
		$agent_id = $agent->get_id();

		try {
			if ( ! empty( $listener['prompt'] ) ) {
				// Queue async LLM execution so we don't block the current request.
				wp_schedule_single_event(
					time(),
					'agentic_async_event',
					array(
						$agent_id,
						$listener['id'],
						$listener['prompt'],
						self::sanitize_hook_args( $args ),
					)
				);

				$audit->log(
					$agent_id,
					'event_listener_triggered',
					$listener['id'],
					array(
						'listener_name' => $listener['name'],
						'hook'          => $listener['hook'],
						'mode'          => 'autonomous',
					)
				);
				return; // Actual execution happens in handle_async_event.
			}

			// Declarative tool mode: run one reviewed tool synchronously, through
			// the same risk/permission gate as chat. The tool receives the hook
			// arguments mapped to named parameters (see the listener's "args").
			if ( ! empty( $listener['tool'] ) ) {
				$result   = self::run_automation_tool( $agent, $listener, $args, 'hook' );
				$duration = round( microtime( true ) - $start, 3 );

				$audit->log(
					$agent_id,
					'event_listener_complete',
					$listener['id'],
					array(
						'listener_name' => $listener['name'],
						'hook'          => $listener['hook'],
						'duration_s'    => $duration,
						'mode'          => 'tool',
						'result'        => is_array( $result ) ? substr( wp_json_encode( $result ), 0, 1000 ) : null,
					)
				);
				return;
			}

			// Direct mode: call the agent callback synchronously. Manifests never
			// carry a callback (the validator strips it), so this only fires for
			// reviewed PHP agents. If the callback returns false the event was not
			// relevant — skip logging.
			$result = false;
			if ( ! empty( $listener['callback'] ) && method_exists( $agent, $listener['callback'] ) ) {
				$result = call_user_func_array( array( $agent, $listener['callback'] ), $args );
			}

			if ( false === $result ) {
				return; // Callback determined the event was not relevant.
			}

			$duration = round( microtime( true ) - $start, 3 );

			$audit->log(
				$agent_id,
				'event_listener_complete',
				$listener['id'],
				array(
					'listener_name' => $listener['name'],
					'hook'          => $listener['hook'],
					'duration_s'    => $duration,
					'mode'          => 'direct',
				)
			);
		} catch ( \Throwable $e ) {
			$duration = round( microtime( true ) - $start, 3 );

			$audit->log(
				$agent_id,
				'event_listener_error',
				$listener['id'],
				array(
					'listener_name' => $listener['name'],
					'hook'          => $listener['hook'],
					'duration_s'    => $duration,
					'error'         => $e->getMessage(),
					'file'          => $e->getFile() . ':' . $e->getLine(),
				)
			);
		}
	}

	/**
	 * Handle async event processing via WP-Cron single event.
	 *
	 * Runs the LLM with the event prompt and serialized hook arguments.
	 *
	 * @param string $agent_id    Agent ID.
	 * @param string $listener_id Listener ID.
	 * @param string $prompt      Base prompt.
	 * @param array  $hook_args   Sanitized hook arguments.
	 * @return void
	 */
	public static function handle_async_event( string $agent_id, string $listener_id, string $prompt, array $hook_args ): void {
		\Plugin::get_instance()->load_chat_components();

		$registry = \Agentic_Agent_Registry::get_instance();
		$agent    = $registry->get_agent_instance( $agent_id );
		$audit    = new Audit_Log();

		if ( ! $agent ) {
			$audit->log( $agent_id, 'event_listener_error', $listener_id, array( 'error' => 'Agent not found for async event' ) );
			return;
		}

		// Build context-enriched prompt.
		$context_json = wp_json_encode( $hook_args, JSON_PRETTY_PRINT );
		$full_prompt  = $prompt . "\n\n[EVENT CONTEXT]\n" . $context_json;

		$start = microtime( true );

		try {
			$controller = new Agent_Controller();
			$controller->set_invocation_context( 'hook' );
			$result = $controller->run_autonomous_task( $agent, $full_prompt, 'event_' . $listener_id );

			// If LLM not configured, try direct fallback.
			if ( null === $result ) {
				$listeners = $agent->get_event_listeners();
				foreach ( $listeners as $listener ) {
					if ( $listener['id'] === $listener_id && method_exists( $agent, $listener['callback'] ) ) {
						call_user_func( array( $agent, $listener['callback'] ), ...$hook_args );
						$result = array(
							'mode'   => 'direct_fallback',
							'status' => 'completed',
						);
						break;
					}
				}
			}

			$duration = round( microtime( true ) - $start, 3 );

			$audit->log(
				$agent_id,
				'event_listener_complete',
				$listener_id,
				array(
					'duration_s' => $duration,
					'mode'       => 'autonomous',
					'result'     => is_array( $result ) ? substr( wp_json_encode( $result ), 0, 1000 ) : null,
				)
			);
		} catch ( \Throwable $e ) {
			$duration = round( microtime( true ) - $start, 3 );

			$audit->log(
				$agent_id,
				'event_listener_error',
				$listener_id,
				array(
					'duration_s' => $duration,
					'error'      => $e->getMessage(),
					'file'       => $e->getFile() . ':' . $e->getLine(),
				)
			);
		}
	}

	/**
	 * Register cron events when an agent is activated.
	 *
	 * @param string     $slug  Agent slug.
	 * @param array|null $agent Agent data (unused, required by hook signature).
	 * @return void
	 */
	public static function on_agent_activated( string $slug, $agent ): void {
		unset( $agent ); // Unused parameter required by hook signature.
		$registry = \Agentic_Agent_Registry::get_instance();
		$instance = $registry->get_agent_instance( $slug );

		if ( $instance ) {
			$instance->register_scheduled_tasks();
			// Seed default settings once — does not overwrite existing admin customisations.
			\Agentic\Agent_Settings::seed_defaults( $slug, $instance->get_default_settings() );
		}

		\Agentic\Security_Log::log_system(
			'agent_activated',
			'agents',
			array( 'slug' => $slug )
		);
	}

	/**
	 * Unregister cron events when an agent is deactivated.
	 *
	 * @param string     $slug  Agent slug.
	 * @param array|null $agent Agent data (unused, required by hook signature).
	 * @return void
	 */
	public static function on_agent_deactivated( string $slug, $agent ): void {
		unset( $agent ); // Unused parameter required by hook signature.
		$registry = \Agentic_Agent_Registry::get_instance();
		$instance = $registry->get_agent_instance( $slug );

		if ( $instance ) {
			$instance->unregister_scheduled_tasks();
		}

		\Agentic\Security_Log::log_system(
			'agent_deactivated',
			'agents',
			array( 'slug' => $slug )
		);
	}

	/**
	 * Log when an agent is installed (uploaded).
	 *
	 * @param string     $slug  Agent slug.
	 * @param array|null $agent Agent data.
	 * @return void
	 */
	public static function on_agent_installed( string $slug, $agent ): void {
		unset( $agent );
		\Agentic\Security_Log::log_system(
			'agent_installed',
			'agents',
			array( 'slug' => $slug )
		);
	}

	/**
	 * Log when an agent is deleted.
	 *
	 * @param string $slug Agent slug.
	 * @return void
	 */
	public static function on_agent_deleted( string $slug ): void {
		\Agentic\Security_Log::log_system(
			'agent_deleted',
			'agents',
			array( 'slug' => $slug )
		);
	}

	/**
	 * Run a declarative automation "tool" action through the standard tool gate.
	 *
	 * Used by both scheduled tasks and event listeners when they declare a
	 * `tool` instead of (or as a fallback to) a `prompt`. Execution goes through
	 * Tool_Executor exactly like a chat tool call, so tool enable/disable state
	 * and risk-level enforcement apply identically: a disabled or extreme-risk
	 * tool is refused, a high-risk tool is queued for admin approval, and only
	 * permitted tools run unattended. Which agent scheduled the action is
	 * irrelevant — the tool's own permission is the boundary.
	 *
	 * @param Agent_Base $agent     Agent instance.
	 * @param array      $spec      Task/listener spec (must include 'tool'; may include 'args').
	 * @param array      $hook_args Positional hook arguments (empty for cron tasks).
	 * @param string     $context   Invocation context ('cron' or 'hook').
	 * @return array Tool result (or a status/error array from the gate).
	 */
	private static function run_automation_tool( Agent_Base $agent, array $spec, array $hook_args, string $context ): array {
		$tool = (string) ( $spec['tool'] ?? '' );
		if ( '' === $tool ) {
			return array( 'error' => 'No tool specified for automation action.' );
		}

		// Map positional hook arguments onto named tool parameters when the spec
		// provides a name list (e.g. ['option','old_value','new_value']).
		$arguments = array();
		if ( ! empty( $spec['args'] ) && is_array( $spec['args'] ) ) {
			$values = array_values( $hook_args );
			foreach ( $spec['args'] as $index => $name ) {
				$arguments[ (string) $name ] = $values[ $index ] ?? null;
			}
		}

		$mode = self::resolve_agent_mode( $agent );
		if ( 'disabled' === $mode ) {
			return array( 'error' => 'Agent is disabled; automation tool not run.' );
		}

		$executor = new Tool_Executor( Tool_Loader::get_instance(), new Audit_Log() );
		return $executor->execute( $tool, $arguments, $agent->get_id(), $mode, $context, $agent );
	}

	/**
	 * Resolve an agent's effective operating mode for unattended execution.
	 *
	 * Mirrors the chat resolver: per-agent override → agent default → global
	 * setting. Determines how Tool_Executor gates medium/high-risk tools.
	 *
	 * @param Agent_Base $agent Agent instance.
	 * @return string 'disabled'|'supervised'|'autonomous'.
	 */
	private static function resolve_agent_mode( Agent_Base $agent ): string {
		$slug = $agent->get_id();
		$mode = (string) Agent_Settings::get( $slug, 'override_mode' );
		if ( '' === $mode ) {
			$mode = (string) $agent->get_default_mode();
		}
		if ( '' === $mode ) {
			$mode = (string) get_option( 'agentic_agent_mode', 'supervised' );
		}
		return in_array( $mode, array( 'disabled', 'supervised', 'autonomous' ), true ) ? $mode : 'supervised';
	}

	/**
	 * Sanitize hook arguments for safe serialization.
	 *
	 * Converts WP objects to arrays, truncates large values, removes non-serializable data.
	 *
	 * @param array $args Raw hook arguments.
	 * @return array Sanitized arguments.
	 */
	private static function sanitize_hook_args( array $args ): array {
		$sanitized = array();

		foreach ( $args as $key => $value ) {
			if ( $value instanceof \WP_Post ) {
				$sanitized[ $key ] = array(
					'_type'       => 'WP_Post',
					'ID'          => $value->ID,
					'post_title'  => $value->post_title,
					'post_type'   => $value->post_type,
					'post_status' => $value->post_status,
					'post_author' => $value->post_author,
				);
			} elseif ( $value instanceof \WP_Comment ) {
				$sanitized[ $key ] = array(
					'_type'           => 'WP_Comment',
					'comment_ID'      => $value->comment_ID,
					'comment_post_ID' => $value->comment_post_ID,
					'comment_author'  => $value->comment_author,
					'comment_content' => substr( $value->comment_content, 0, 500 ),
				);
			} elseif ( $value instanceof \WP_User ) {
				$sanitized[ $key ] = array(
					'_type'        => 'WP_User',
					'ID'           => $value->ID,
					'user_login'   => $value->user_login,
					'display_name' => $value->display_name,
					'roles'        => $value->roles,
				);
			} elseif ( is_object( $value ) ) {
				$sanitized[ $key ] = array(
					'_type' => get_class( $value ),
					'_note' => 'Object serialized to class name only',
				);
			} elseif ( is_string( $value ) && strlen( $value ) > 1000 ) {
				$sanitized[ $key ] = substr( $value, 0, 1000 ) . '... [truncated]';
			} else {
				$sanitized[ $key ] = $value;
			}
		}

		return $sanitized;
	}
}
