<?php
/**
 * Approval Queue
 *
 * Ledger of all non-readonly tool executions. High-risk writes pause for
 * admin approval (status = pending); everything else is recorded immediately
 * (status = executed). Records retained for 30 days.
 *
 * @package    Agent_Builder
 * @subpackage Includes
 * @author     Agent Builder Team <support@agentic-plugin.com>
 * @license    GPL-2.0-or-later https://www.gnu.org/licenses/gpl-2.0.html
 * @link       https://agentic-plugin.com
 * @since      0.1.0
 *
 * php version 8.1
 */

declare(strict_types=1);

namespace Agentic;

// Prevent direct access.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Manages the approval queue and write-operations ledger.
 */
class Approval_Queue {

	/**
	 * Number of days to retain records before cleanup.
	 */
	private const RETENTION_DAYS = 30;

	/**
	 * Constructor
	 */
	public function __construct() {
		// Add admin notices for pending approvals.
		add_action( 'admin_notices', array( $this, 'pending_approval_notice' ) );
	}

	/**
	 * Show notice for pending approvals
	 *
	 * @return void
	 */
	public function pending_approval_notice(): void {
		if ( ! current_user_can( 'manage_options' ) ) {
			return;
		}

		$count = $this->get_pending_count();

		if ( $count > 0 ) {
			$url = admin_url( 'admin.php?page=agentic-approvals' );
			printf(
				'<div class="notice notice-warning"><p><strong>Agentic:</strong> %s pending approval(s). <a href="%s">Review now</a></p></div>',
				esc_html( $count ),
				esc_url( $url )
			);
		}
	}

	/**
	 * Get count of pending approvals
	 *
	 * @return int
	 */
	public function get_pending_count(): int {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table query.
		return (int) $wpdb->get_var(
			"SELECT COUNT(*) FROM {$wpdb->prefix}agentic_approval_queue WHERE status = 'pending'"
		);
	}

	/**
	 * Get all pending approvals
	 *
	 * @return array
	 */
	public function get_pending(): array {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table query.
		$results = $wpdb->get_results(
			"SELECT * FROM {$wpdb->prefix}agentic_approval_queue WHERE status = 'pending' ORDER BY created_at DESC",
			ARRAY_A
		);

		foreach ( $results as &$row ) {
			$row['params'] = json_decode( $row['params'], true );
		}

		return $results;
	}

	/**
	 * Add a high-risk item to the approval queue (status = pending).
	 *
	 * @param string $agent_id   Agent identifier.
	 * @param string $action     Tool/action name.
	 * @param array  $params     Tool arguments.
	 * @param string $reasoning  Reason for queuing.
	 * @param int    $expires    Days until pending item expires.
	 * @param string $risk_level Risk level.
	 * @param string $mode       Agent operating mode.
	 * @param string $invocation Invocation context (chat, cron, hook, cli).
	 * @return int|false Queue item ID or false.
	 */
	public function add( string $agent_id, string $action, array $params, string $reasoning = '', int $expires = 7, string $risk_level = 'high', string $mode = '', string $invocation = '' ): int|false {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table insert.
		$result = $wpdb->insert(
			$wpdb->prefix . 'agentic_approval_queue',
			array(
				'agent_id'   => $agent_id,
				'action'     => $action,
				'params'     => wp_json_encode( $params ),
				'reasoning'  => $reasoning,
				'risk_level' => $risk_level,
				'status'     => 'pending',
				'mode'       => $mode,
				'invocation' => $invocation,
				'created_at' => gmdate( 'Y-m-d H:i:s' ),
				'expires_at' => gmdate( 'Y-m-d H:i:s', strtotime( "+{$expires} days" ) ),
			)
		);

		return $result ? $wpdb->insert_id : false;
	}

	/**
	 * Record a write tool that executed immediately (no approval needed).
	 *
	 * Used for tools that pass risk gating (allow/confirm-then-execute) but
	 * are non-readonly, so they need to appear in the operations ledger.
	 *
	 * @param string $agent_id   Agent identifier.
	 * @param string $action     Tool/action name.
	 * @param array  $params     Tool arguments.
	 * @param string $risk_level Effective risk level.
	 * @param string $mode       Agent operating mode.
	 * @param string $invocation Invocation context (chat, cron, hook, cli).
	 * @return int|false Record ID or false.
	 */
	public function log_executed( string $agent_id, string $action, array $params, string $risk_level = 'none', string $mode = '', string $invocation = '' ): int|false {
		global $wpdb;

		$now = gmdate( 'Y-m-d H:i:s' );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table insert.
		$result = $wpdb->insert(
			$wpdb->prefix . 'agentic_approval_queue',
			array(
				'agent_id'    => $agent_id,
				'action'      => $action,
				'params'      => wp_json_encode( $params ),
				'reasoning'   => '',
				'risk_level'  => $risk_level,
				'status'      => 'executed',
				'mode'        => $mode,
				'invocation'  => $invocation,
				'created_at'  => $now,
				'executed_at' => $now,
			)
		);

		return $result ? $wpdb->insert_id : false;
	}

	/**
	 * Approve an item
	 *
	 * @param int $id Queue item ID.
	 * @return bool Success.
	 */
	public function approve( int $id ): bool {
		global $wpdb;

		// Fetch the row before updating so we can log meaningful context.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Single-row lookup before update.
		$row = $wpdb->get_row( $wpdb->prepare( "SELECT agent_id, action, risk_level FROM {$wpdb->prefix}agentic_approval_queue WHERE id = %d", $id ), ARRAY_A );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table update.
		$result = (bool) $wpdb->update(
			$wpdb->prefix . 'agentic_approval_queue',
			array(
				'status'      => 'approved',
				'approved_by' => get_current_user_id(),
				'approved_at' => gmdate( 'Y-m-d H:i:s' ),
			),
			array( 'id' => $id )
		);

		if ( $result ) {
			\Agentic\Security_Log::log_system(
				'action_approved',
				'approvals',
				array(
					'queue_id'   => $id,
					'agent_id'   => $row['agent_id'] ?? '',
					'action'     => $row['action'] ?? '',
					'risk_level' => $row['risk_level'] ?? '',
				)
			);
		} else {
			\Agentic\Security_Log::log_system(
				'action_approve_failed',
				'approvals',
				array(
					'queue_id' => $id,
					'agent_id' => $row['agent_id'] ?? '',
					'action'   => $row['action'] ?? '',
					'reason'   => 'db_update_failed',
				)
			);
		}

		return $result;
	}

	/**
	 * Reject an item
	 *
	 * @param int $id Queue item ID.
	 * @return bool Success.
	 */
	public function reject( int $id ): bool {
		global $wpdb;

		// Fetch the row before updating so we can log meaningful context.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Single-row lookup before update.
		$row = $wpdb->get_row( $wpdb->prepare( "SELECT agent_id, action, risk_level FROM {$wpdb->prefix}agentic_approval_queue WHERE id = %d", $id ), ARRAY_A );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table update.
		$result = (bool) $wpdb->update(
			$wpdb->prefix . 'agentic_approval_queue',
			array(
				'status'      => 'rejected',
				'approved_by' => get_current_user_id(),
				'approved_at' => gmdate( 'Y-m-d H:i:s' ),
			),
			array( 'id' => $id )
		);

		if ( $result ) {
			\Agentic\Security_Log::log_system(
				'action_rejected',
				'approvals',
				array(
					'queue_id'   => $id,
					'agent_id'   => $row['agent_id'] ?? '',
					'action'     => $row['action'] ?? '',
					'risk_level' => $row['risk_level'] ?? '',
				)
			);
		} else {
			\Agentic\Security_Log::log_system(
				'action_reject_failed',
				'approvals',
				array(
					'queue_id' => $id,
					'agent_id' => $row['agent_id'] ?? '',
					'action'   => $row['action'] ?? '',
					'reason'   => 'db_update_failed',
				)
			);
		}

		return $result;
	}

	/**
	 * Find an approved (but not yet executed) entry for a given agent + tool.
	 *
	 * @param string $agent_id Agent identifier.
	 * @param string $tool_name Tool/action name.
	 * @return array|null The queue row, or null if none found.
	 */
	public function find_approved( string $agent_id, string $tool_name ): ?array {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table query.
		$row = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT * FROM {$wpdb->prefix}agentic_approval_queue
				 WHERE agent_id = %s AND action = %s AND status = 'approved'
				 AND expires_at > NOW()
				 ORDER BY approved_at DESC LIMIT 1",
				$agent_id,
				$tool_name
			),
			ARRAY_A
		);

		return is_array( $row ) ? $row : null;
	}

	/**
	 * Mark an approved entry as executed.
	 *
	 * @param int $id Queue item ID.
	 * @return bool Success.
	 */
	public function mark_executed( int $id ): bool {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table update.
		return (bool) $wpdb->update(
			$wpdb->prefix . 'agentic_approval_queue',
			array(
				'status'      => 'executed',
				'executed_at' => gmdate( 'Y-m-d H:i:s' ),
			),
			array( 'id' => $id )
		);
	}

	/**
	 * Get recent operations with optional filtering.
	 *
	 * @param array $args {
	 *     Optional. Query arguments.
	 *     @type string $status   Filter by status (pending, executed, approved, rejected, expired).
	 *     @type string $agent_id Filter by agent.
	 *     @type int    $limit    Number of records (default 50).
	 *     @type int    $offset   Pagination offset (default 0).
	 * }
	 * @return array Records.
	 */
	public function get_recent( array $args = array() ): array {
		global $wpdb;

		$where  = array( '1=1' );
		$values = array();

		if ( ! empty( $args['status'] ) ) {
			$where[]  = 'status = %s';
			$values[] = $args['status'];
		}

		if ( ! empty( $args['agent_id'] ) ) {
			$where[]  = 'agent_id = %s';
			$values[] = $args['agent_id'];
		}

		$limit  = (int) ( $args['limit'] ?? 50 );
		$offset = (int) ( $args['offset'] ?? 0 );

		$where_sql = implode( ' AND ', $where );
		$query     = "SELECT * FROM {$wpdb->prefix}agentic_approval_queue WHERE {$where_sql} ORDER BY created_at DESC LIMIT %d OFFSET %d";
		$values[]  = $limit;
		$values[]  = $offset;

		// phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared -- Custom table query with dynamic where clause.
		$results = $wpdb->get_results(
			// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Custom plugin table.
			$wpdb->prepare( $query, ...$values ),
			ARRAY_A
		);

		foreach ( $results as &$row ) {
			$row['params'] = json_decode( $row['params'] ?? '[]', true );
		}

		return $results;
	}

	/**
	 * Expire stale pending items and delete records older than retention period.
	 *
	 * @return int Number of rows deleted.
	 */
	public function cleanup_expired(): int {
		global $wpdb;

		$table = $wpdb->prefix . 'agentic_approval_queue';

		// Mark pending items past their expiry as expired (preserve the record).
		// phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Custom table update.
		$wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
			"UPDATE {$table} SET status = 'expired' WHERE status = 'pending' AND expires_at IS NOT NULL AND expires_at < NOW()" // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
		);

		// Delete all records older than retention period.
		// phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Retention cleanup on custom table.
		return (int) $wpdb->query(
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Custom plugin table.
			$wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
				"DELETE FROM {$table} WHERE created_at < DATE_SUB(UTC_TIMESTAMP(), INTERVAL %d DAY)", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
				self::RETENTION_DAYS
			)
		);
	}
}
