<?php
/**
 * WordPress admin tools — plugin + theme management, taxonomies, permalinks.
 *
 * Each handler runs with the linked user's WordPress capabilities, so a
 * subscriber-level token can't install plugins even if the AI asks.
 */

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

class PCD_PC_Tools_Admin {

	public static function register(): void {
		// ---- PLUGINS ----

		PCD_PC_Tools::register(
			'wp_plugin_list',
			'List all installed plugins with status (active/inactive), version, and update availability.',
			[
				'type' => 'object',
				'properties' => [
					'status' => [ 'type' => 'string', 'enum' => [ 'all', 'active', 'inactive' ], 'default' => 'all' ],
				],
			],
			[ __CLASS__, 'plugin_list' ],
			[ 'cap' => 'activate_plugins', 'category' => 'admin' ]
		);

		PCD_PC_Tools::register(
			'wp_plugin_activate',
			'Activate a plugin by slug (folder name) or full plugin file path.',
			[
				'type' => 'object',
				'required' => [ 'plugin' ],
				'properties' => [ 'plugin' => [ 'type' => 'string' ] ],
			],
			[ __CLASS__, 'plugin_activate' ],
			[ 'cap' => 'activate_plugins', 'category' => 'admin' ]
		);

		PCD_PC_Tools::register(
			'wp_plugin_deactivate',
			'Deactivate a plugin by slug or plugin file path.',
			[
				'type' => 'object',
				'required' => [ 'plugin' ],
				'properties' => [ 'plugin' => [ 'type' => 'string' ] ],
			],
			[ __CLASS__, 'plugin_deactivate' ],
			[ 'cap' => 'activate_plugins', 'category' => 'admin' ]
		);

		PCD_PC_Tools::register(
			'wp_plugin_install',
			'Install a plugin from the WordPress.org repo by slug. Two-phase: first call returns plugin info + confirmation prompt; pass confirm=true to actually install. Pass activate=true to also activate immediately.',
			[
				'type' => 'object',
				'required' => [ 'slug' ],
				'properties' => [
					'slug'     => [ 'type' => 'string', 'description' => 'wp.org plugin slug, e.g. "duplicate-page".' ],
					'version'  => [ 'type' => 'string', 'description' => 'Specific version. Omit for latest.' ],
					'activate' => [ 'type' => 'boolean', 'default' => false ],
					'confirm'  => [ 'type' => 'boolean', 'default' => false ],
				],
			],
			[ __CLASS__, 'plugin_install' ],
			[ 'cap' => 'install_plugins', 'category' => 'admin' ]
		);

		PCD_PC_Tools::register(
			'wp_plugin_install_zip',
			'Install a plugin from a zip file — use this for any plugin NOT on wordpress.org (premium plugins, custom builds, GitHub releases, plugin zips the user uploaded in chat). Pass EITHER zip_url (any public URL) OR zip_data (base64-encoded zip bytes, max 50MB). Two-phase: first call inspects the zip and returns the detected plugin header; pass confirm:true to install. Set overwrite=true to replace an existing copy of the same plugin. Set activate=true to activate after install.',
			[
				'type' => 'object',
				'properties' => [
					'zip_url'   => [ 'type' => 'string', 'description' => 'Public URL to a plugin .zip file.' ],
					'zip_data'  => [ 'type' => 'string', 'description' => 'Base64-encoded contents of a plugin .zip file. Max 50MB decoded.' ],
					'activate'  => [ 'type' => 'boolean', 'default' => false ],
					'overwrite' => [ 'type' => 'boolean', 'default' => false, 'description' => 'Replace an existing plugin of the same folder name.' ],
					'confirm'   => [ 'type' => 'boolean', 'default' => false ],
				],
			],
			[ __CLASS__, 'plugin_install_zip' ],
			[ 'cap' => 'install_plugins', 'category' => 'admin' ]
		);

		PCD_PC_Tools::register(
			'wp_plugin_update',
			'Update an installed plugin to its latest version. Two-phase: first call returns current vs new version; pass confirm=true to upgrade.',
			[
				'type' => 'object',
				'required' => [ 'plugin' ],
				'properties' => [
					'plugin'  => [ 'type' => 'string' ],
					'confirm' => [ 'type' => 'boolean', 'default' => false ],
				],
			],
			[ __CLASS__, 'plugin_update' ],
			[ 'cap' => 'update_plugins', 'category' => 'admin' ]
		);

		PCD_PC_Tools::register(
			'wp_plugin_delete',
			'Permanently delete a plugin (removes its files). Two-phase: first call returns warning + active status; pass confirm=true to delete. Will auto-deactivate the plugin if it\'s active. Cannot be undone — plugin uninstall scripts also fire, dropping plugin data.',
			[
				'type' => 'object',
				'required' => [ 'plugin' ],
				'properties' => [
					'plugin'  => [ 'type' => 'string' ],
					'confirm' => [ 'type' => 'boolean', 'default' => false ],
				],
			],
			[ __CLASS__, 'plugin_delete' ],
			[ 'cap' => 'delete_plugins', 'category' => 'admin' ]
		);

		// ---- THEMES ----

		PCD_PC_Tools::register(
			'wp_theme_list',
			'List installed themes with status (active/inactive), version, and parent theme.',
			[ 'type' => 'object', 'properties' => new stdClass() ],
			[ __CLASS__, 'theme_list' ],
			[ 'cap' => 'switch_themes', 'category' => 'admin' ]
		);

		PCD_PC_Tools::register(
			'wp_theme_delete',
			'Permanently delete an installed theme (removes its files). Two-phase: pass confirm=true to delete. Cannot delete the active theme — activate a different one first.',
			[
				'type' => 'object',
				'required' => [ 'stylesheet' ],
				'properties' => [
					'stylesheet' => [ 'type' => 'string' ],
					'confirm'    => [ 'type' => 'boolean', 'default' => false ],
				],
			],
			[ __CLASS__, 'theme_delete' ],
			[ 'cap' => 'delete_themes', 'category' => 'admin' ]
		);

		PCD_PC_Tools::register(
			'wp_theme_activate',
			'Switch the active theme. Pass the theme stylesheet slug (folder name).',
			[
				'type' => 'object',
				'required' => [ 'stylesheet' ],
				'properties' => [
					'stylesheet' => [ 'type' => 'string' ],
					'confirm'    => [ 'type' => 'boolean' ],
				],
			],
			[ __CLASS__, 'theme_activate' ],
			[ 'cap' => 'switch_themes', 'category' => 'admin', 'destructive' => true ]
		);

		// ---- TAXONOMIES / TERMS ----

		PCD_PC_Tools::register(
			'wp_taxonomy_list',
			'List taxonomies available on this site (category, post_tag, custom taxonomies registered by plugins/CPTs).',
			[
				'type' => 'object',
				'properties' => [
					'object_type' => [ 'type' => 'string', 'description' => 'Filter to taxonomies attached to this post type, e.g. post, product.' ],
				],
			],
			[ __CLASS__, 'taxonomy_list' ],
			[ 'cap' => 'edit_posts', 'category' => 'admin' ]
		);

		PCD_PC_Tools::register(
			'wp_term_list',
			'List terms in a taxonomy (e.g. all categories or tags).',
			[
				'type' => 'object',
				'required' => [ 'taxonomy' ],
				'properties' => [
					'taxonomy' => [ 'type' => 'string' ],
					'search'   => [ 'type' => 'string' ],
					'per_page' => [ 'type' => 'integer', 'default' => 100 ],
				],
			],
			[ __CLASS__, 'term_list' ],
			[ 'cap' => 'edit_posts', 'category' => 'admin' ]
		);

		PCD_PC_Tools::register(
			'wp_term_create',
			'Create a new taxonomy term (e.g. a new category or tag).',
			[
				'type' => 'object',
				'required' => [ 'taxonomy', 'name' ],
				'properties' => [
					'taxonomy'    => [ 'type' => 'string' ],
					'name'        => [ 'type' => 'string' ],
					'slug'        => [ 'type' => 'string' ],
					'description' => [ 'type' => 'string' ],
					'parent_id'   => [ 'type' => 'integer' ],
				],
			],
			[ __CLASS__, 'term_create' ],
			[ 'cap' => 'manage_categories', 'category' => 'admin' ]
		);

		PCD_PC_Tools::register(
			'wp_term_assign',
			'Assign terms to a post. Pass term IDs (preferred) or slugs.',
			[
				'type' => 'object',
				'required' => [ 'post_id', 'taxonomy', 'terms' ],
				'properties' => [
					'post_id'  => [ 'type' => 'integer' ],
					'taxonomy' => [ 'type' => 'string' ],
					'terms'    => [ 'type' => 'array', 'items' => [ 'type' => 'string' ], 'description' => 'Term IDs as strings, or slugs.' ],
					'append'   => [ 'type' => 'boolean', 'default' => false, 'description' => 'If true, add to existing terms instead of replacing.' ],
				],
			],
			[ __CLASS__, 'term_assign' ],
			[ 'cap' => 'edit_posts', 'category' => 'admin' ]
		);

		// ---- PERMALINKS ----

		PCD_PC_Tools::register(
			'wp_flush_rewrite',
			'Flush WordPress rewrite rules. Run this after changing URL structure, adding CPTs via mu-plugin, etc.',
			[ 'type' => 'object', 'properties' => new stdClass() ],
			[ __CLASS__, 'flush_rewrite' ],
			[ 'cap' => 'manage_options', 'category' => 'admin' ]
		);

		// ---- USERS ----

		PCD_PC_Tools::register(
			'wp_user_list',
			'List WordPress users with role filter and search.',
			[
				'type' => 'object',
				'properties' => [
					'role'     => [ 'type' => 'string', 'description' => 'Filter to a single role: administrator, editor, author, etc.' ],
					'search'   => [ 'type' => 'string' ],
					'per_page' => [ 'type' => 'integer', 'default' => 50 ],
					'page'     => [ 'type' => 'integer', 'default' => 1 ],
				],
			],
			[ __CLASS__, 'user_list' ],
			[ 'cap' => 'list_users', 'category' => 'users' ]
		);

		PCD_PC_Tools::register(
			'wp_user_get',
			'Get a single user by ID — login, email, display name, roles, registration date.',
			[
				'type' => 'object',
				'required' => [ 'id' ],
				'properties' => [ 'id' => [ 'type' => 'integer' ] ],
			],
			[ __CLASS__, 'user_get' ],
			[ 'cap' => 'list_users', 'category' => 'users' ]
		);

		PCD_PC_Tools::register(
			'wp_user_create',
			'Create a new WordPress user. Returns the new user ID. WordPress sends the new user a notification email by default unless skip_notification=true.',
			[
				'type' => 'object',
				'required' => [ 'user_login', 'user_email' ],
				'properties' => [
					'user_login'        => [ 'type' => 'string' ],
					'user_email'        => [ 'type' => 'string' ],
					'user_pass'         => [ 'type' => 'string', 'description' => 'Plaintext password. Omit to auto-generate.' ],
					'first_name'        => [ 'type' => 'string' ],
					'last_name'         => [ 'type' => 'string' ],
					'display_name'      => [ 'type' => 'string' ],
					'role'              => [ 'type' => 'string', 'default' => 'subscriber' ],
					'skip_notification' => [ 'type' => 'boolean', 'default' => false ],
				],
			],
			[ __CLASS__, 'user_create' ],
			[ 'cap' => 'create_users', 'category' => 'users' ]
		);

		PCD_PC_Tools::register(
			'wp_user_update',
			'Update user fields. Snapshot of prior values created for undo.',
			[
				'type' => 'object',
				'required' => [ 'id' ],
				'properties' => [
					'id'           => [ 'type' => 'integer' ],
					'user_email'   => [ 'type' => 'string' ],
					'first_name'   => [ 'type' => 'string' ],
					'last_name'    => [ 'type' => 'string' ],
					'display_name' => [ 'type' => 'string' ],
					'role'         => [ 'type' => 'string' ],
				],
			],
			[ __CLASS__, 'user_update' ],
			[ 'cap' => 'edit_users', 'category' => 'users' ]
		);

		PCD_PC_Tools::register(
			'wp_user_delete',
			'Delete a user. Pass reassign_to to give their posts to another user; otherwise their posts are deleted.',
			[
				'type' => 'object',
				'required' => [ 'id' ],
				'properties' => [
					'id'           => [ 'type' => 'integer' ],
					'reassign_to'  => [ 'type' => 'integer' ],
					'confirm'      => [ 'type' => 'boolean' ],
				],
			],
			[ __CLASS__, 'user_delete' ],
			[ 'cap' => 'delete_users', 'category' => 'users', 'destructive' => true ]
		);

		// ---- MENUS ----

		PCD_PC_Tools::register(
			'wp_menu_list',
			'List all WordPress nav menus and their assigned theme locations.',
			[ 'type' => 'object', 'properties' => new stdClass() ],
			[ __CLASS__, 'menu_list' ],
			[ 'cap' => 'edit_theme_options', 'category' => 'menus' ]
		);

		PCD_PC_Tools::register(
			'wp_menu_get',
			'Get a single menu with all its items in tree order.',
			[
				'type' => 'object',
				'required' => [ 'menu' ],
				'properties' => [ 'menu' => [ 'type' => 'string', 'description' => 'Menu ID, slug, or name.' ] ],
			],
			[ __CLASS__, 'menu_get' ],
			[ 'cap' => 'edit_theme_options', 'category' => 'menus' ]
		);

		PCD_PC_Tools::register(
			'wp_menu_create',
			'Create a new WordPress nav menu.',
			[
				'type' => 'object',
				'required' => [ 'name' ],
				'properties' => [ 'name' => [ 'type' => 'string' ] ],
			],
			[ __CLASS__, 'menu_create' ],
			[ 'cap' => 'edit_theme_options', 'category' => 'menus' ]
		);

		PCD_PC_Tools::register(
			'wp_menu_assign_location',
			'Assign a menu to a theme location (e.g. primary, footer). Pass null/empty menu_id to unassign.',
			[
				'type' => 'object',
				'required' => [ 'location' ],
				'properties' => [
					'location' => [ 'type' => 'string', 'description' => 'Theme location slug. Use bricks_status / wp_menu_list to see what locations the theme registers.' ],
					'menu_id'  => [ 'type' => 'integer' ],
				],
			],
			[ __CLASS__, 'menu_assign_location' ],
			[ 'cap' => 'edit_theme_options', 'category' => 'menus' ]
		);

		PCD_PC_Tools::register(
			'wp_menu_item_add',
			'Add an item to a nav menu. Type: post (link to post/page), taxonomy (link to category/tag), custom (raw URL).',
			[
				'type' => 'object',
				'required' => [ 'menu_id', 'type' ],
				'properties' => [
					'menu_id'   => [ 'type' => 'integer' ],
					'type'      => [ 'type' => 'string', 'enum' => [ 'post_type', 'taxonomy', 'custom' ] ],
					'object'    => [ 'type' => 'string', 'description' => 'Post type slug (e.g. "page") or taxonomy slug (e.g. "category"). Required for post_type / taxonomy.' ],
					'object_id' => [ 'type' => 'integer', 'description' => 'Post ID or term ID. Required for post_type / taxonomy.' ],
					'title'     => [ 'type' => 'string', 'description' => 'Override the displayed label. Required for custom; optional for post_type/taxonomy.' ],
					'url'       => [ 'type' => 'string', 'description' => 'URL. Required for custom.' ],
					'parent_id' => [ 'type' => 'integer', 'description' => 'Parent menu item ID to nest under.' ],
				],
			],
			[ __CLASS__, 'menu_item_add' ],
			[ 'cap' => 'edit_theme_options', 'category' => 'menus' ]
		);

		PCD_PC_Tools::register(
			'wp_menu_item_delete',
			'Remove an item from a nav menu.',
			[
				'type' => 'object',
				'required' => [ 'item_id' ],
				'properties' => [ 'item_id' => [ 'type' => 'integer' ] ],
			],
			[ __CLASS__, 'menu_item_delete' ],
			[ 'cap' => 'edit_theme_options', 'category' => 'menus' ]
		);

		// ---- COMMENTS ----

		PCD_PC_Tools::register(
			'wp_comment_list',
			'List comments with status filter (approve, hold, spam, trash, all).',
			[
				'type' => 'object',
				'properties' => [
					'status'   => [ 'type' => 'string', 'enum' => [ 'approve', 'hold', 'spam', 'trash', 'all' ], 'default' => 'all' ],
					'post_id'  => [ 'type' => 'integer' ],
					'search'   => [ 'type' => 'string' ],
					'per_page' => [ 'type' => 'integer', 'default' => 50 ],
				],
			],
			[ __CLASS__, 'comment_list' ],
			[ 'cap' => 'moderate_comments', 'category' => 'comments' ]
		);

		PCD_PC_Tools::register(
			'wp_comment_moderate',
			'Set a comment\'s status — approve, spam, hold (unapprove), trash.',
			[
				'type' => 'object',
				'required' => [ 'id', 'status' ],
				'properties' => [
					'id'     => [ 'type' => 'integer' ],
					'status' => [ 'type' => 'string', 'enum' => [ 'approve', 'hold', 'spam', 'trash' ] ],
				],
			],
			[ __CLASS__, 'comment_moderate' ],
			[ 'cap' => 'moderate_comments', 'category' => 'comments' ]
		);

		// ---- BULK SEARCH-REPLACE ----

		PCD_PC_Tools::register(
			'wp_search_replace',
			'Find-and-replace across post content. Defaults to dry-run — pass confirm=true to apply. Each affected post is snapshotted for undo.',
			[
				'type' => 'object',
				'required' => [ 'search', 'replace' ],
				'properties' => [
					'search'         => [ 'type' => 'string' ],
					'replace'        => [ 'type' => 'string' ],
					'post_types'     => [ 'type' => 'array', 'items' => [ 'type' => 'string' ], 'description' => 'Default: post, page.' ],
					'case_sensitive' => [ 'type' => 'boolean', 'default' => false ],
					'confirm'        => [ 'type' => 'boolean', 'default' => false ],
				],
			],
			[ __CLASS__, 'search_replace' ],
			[ 'cap' => 'edit_others_posts', 'category' => 'bulk', 'destructive' => false ]
		);
	}

	// ------------------------------------------------------------------
	// PLUGINS
	// ------------------------------------------------------------------

	private static function load_plugin_includes(): void {
		if ( ! function_exists( 'get_plugins' ) ) {
			require_once ABSPATH . 'wp-admin/includes/plugin.php';
		}
	}

	public static function plugin_list( array $args ): array {
		self::load_plugin_includes();
		$plugins = get_plugins();
		$updates = get_site_transient( 'update_plugins' );
		$update_data = is_object( $updates ) && isset( $updates->response ) ? $updates->response : [];

		$filter = $args['status'] ?? 'all';
		$rows = [];
		foreach ( $plugins as $file => $data ) {
			$is_active = is_plugin_active( $file );
			$status    = $is_active ? 'active' : 'inactive';
			if ( 'all' !== $filter && $filter !== $status ) {
				continue;
			}
			$rows[] = [
				'file'        => $file,
				'slug'        => dirname( $file ) === '.' ? pathinfo( $file, PATHINFO_FILENAME ) : dirname( $file ),
				'name'        => $data['Name'],
				'version'     => $data['Version'],
				'status'      => $status,
				'author'      => wp_strip_all_tags( $data['Author'] ?? '' ),
				'description' => $data['Description'] ?? '',
				'update_available' => isset( $update_data[ $file ] ) ? $update_data[ $file ]->new_version : null,
			];
		}
		return [ 'count' => count( $rows ), 'plugins' => $rows ];
	}

	private static function resolve_plugin_file( string $input ): ?string {
		self::load_plugin_includes();
		$plugins = get_plugins();
		if ( isset( $plugins[ $input ] ) ) {
			return $input;
		}
		foreach ( $plugins as $file => $_data ) {
			$dir = dirname( $file );
			if ( $dir === $input ) {
				return $file;
			}
			if ( '.' === $dir && pathinfo( $file, PATHINFO_FILENAME ) === $input ) {
				return $file;
			}
		}
		return null;
	}

	public static function plugin_activate( array $args ) {
		$file = self::resolve_plugin_file( (string) ( $args['plugin'] ?? '' ) );
		if ( ! $file ) {
			return new WP_Error( 'not_found', 'Plugin not found: ' . ( $args['plugin'] ?? '' ) );
		}
		if ( is_plugin_active( $file ) ) {
			return [ 'plugin' => $file, 'status' => 'already_active' ];
		}
		PCD_PC_Snapshot::capture( 'option', 'active_plugins', get_option( 'active_plugins' ), "active_plugins before activate {$file}" );
		$result = activate_plugin( $file );
		if ( is_wp_error( $result ) ) {
			return $result;
		}
		return [ 'plugin' => $file, 'status' => 'activated' ];
	}

	public static function plugin_deactivate( array $args ) {
		$file = self::resolve_plugin_file( (string) ( $args['plugin'] ?? '' ) );
		if ( ! $file ) {
			return new WP_Error( 'not_found', 'Plugin not found: ' . ( $args['plugin'] ?? '' ) );
		}
		if ( ! is_plugin_active( $file ) ) {
			return [ 'plugin' => $file, 'status' => 'already_inactive' ];
		}
		PCD_PC_Snapshot::capture( 'option', 'active_plugins', get_option( 'active_plugins' ), "active_plugins before deactivate {$file}" );
		deactivate_plugins( $file );
		return [ 'plugin' => $file, 'status' => 'deactivated' ];
	}

	private static function file_mods_blocked(): ?WP_Error {
		if ( defined( 'DISALLOW_FILE_MODS' ) && DISALLOW_FILE_MODS ) {
			return new WP_Error( 'file_mods_disabled', 'File modifications are disabled on this site (DISALLOW_FILE_MODS).' );
		}
		return null;
	}

	public static function plugin_install( array $args ) {
		if ( $err = self::file_mods_blocked() ) return $err;
		$slug = sanitize_key( (string) ( $args['slug'] ?? '' ) );
		if ( ! $slug ) return new WP_Error( 'no_slug', 'slug is required.' );

		require_once ABSPATH . 'wp-admin/includes/plugin-install.php';

		$api_args = [
			'slug'   => $slug,
			'fields' => [
				'short_description' => true,
				'sections'          => false,
				'icons'             => false,
				'banners'           => false,
				'compatibility'     => false,
			],
		];
		if ( ! empty( $args['version'] ) ) {
			$api_args['version'] = sanitize_text_field( $args['version'] );
		}

		$api = plugins_api( 'plugin_information', $api_args );
		if ( is_wp_error( $api ) ) return $api;

		// Two-phase confirmation.
		if ( empty( $args['confirm'] ) ) {
			return [
				'requires_confirmation' => true,
				'slug'             => $api->slug,
				'name'             => $api->name,
				'version'          => $api->version,
				'author'           => wp_strip_all_tags( $api->author ),
				'rating'           => $api->rating,
				'active_installs'  => $api->active_installs,
				'short_description'=> $api->short_description ?? '',
				'will_activate'    => ! empty( $args['activate'] ),
				'message'          => sprintf(
					'Ready to install %s v%s. Re-call with confirm:true to proceed.',
					$api->name,
					$api->version
				),
			];
		}

		require_once ABSPATH . 'wp-admin/includes/file.php';
		require_once ABSPATH . 'wp-admin/includes/misc.php';
		require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';

		$skin     = new Automatic_Upgrader_Skin();
		$upgrader = new Plugin_Upgrader( $skin );
		$result   = $upgrader->install( $api->download_link );

		if ( is_wp_error( $result ) ) return $result;
		if ( ! $result ) {
			return new WP_Error( 'install_failed', 'Install failed. ' . implode( '; ', $skin->get_upgrade_messages() ) );
		}

		$activated = false;
		if ( ! empty( $args['activate'] ) ) {
			$plugin_file = $upgrader->plugin_info();
			if ( $plugin_file ) {
				PCD_PC_Snapshot::capture( 'option', 'active_plugins', get_option( 'active_plugins' ), "active_plugins before activate {$plugin_file}" );
				$activate_result = activate_plugin( $plugin_file );
				$activated = ! is_wp_error( $activate_result );
			}
		}

		return [
			'slug'      => $api->slug,
			'name'      => $api->name,
			'version'   => $api->version,
			'installed' => true,
			'activated' => $activated,
		];
	}

	public static function plugin_install_zip( array $args ) {
		if ( $err = self::file_mods_blocked() ) return $err;

		$zip_url  = trim( (string) ( $args['zip_url'] ?? '' ) );
		$zip_data = (string) ( $args['zip_data'] ?? '' );
		if ( ! $zip_url && ! $zip_data ) {
			return new WP_Error( 'no_source', 'Either zip_url or zip_data is required.' );
		}
		if ( $zip_url && $zip_data ) {
			return new WP_Error( 'ambiguous_source', 'Pass either zip_url or zip_data, not both.' );
		}

		require_once ABSPATH . 'wp-admin/includes/file.php';
		require_once ABSPATH . 'wp-admin/includes/misc.php';
		require_once ABSPATH . 'wp-admin/includes/plugin.php';
		require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';

		// Materialize a local file path. URLs go through WP's safe downloader;
		// base64 payloads are decoded into a temp file with magic-byte + size
		// guards so we don't hand a wrong-shaped blob to Plugin_Upgrader.
		$local_path = '';
		$cleanup    = null;

		if ( $zip_url ) {
			$tmp = download_url( $zip_url, 60 );
			if ( is_wp_error( $tmp ) ) return $tmp;
			$local_path = $tmp;
			$cleanup    = function () use ( $tmp ) { if ( file_exists( $tmp ) ) wp_delete_file( $tmp ); };
		} else {
			$bytes = base64_decode( $zip_data, true );
			if ( $bytes === false ) {
				return new WP_Error( 'bad_base64', 'zip_data is not valid base64.' );
			}
			$max = 50 * 1024 * 1024;
			if ( strlen( $bytes ) > $max ) {
				return new WP_Error( 'too_large', 'Decoded zip exceeds 50MB.' );
			}
			if ( substr( $bytes, 0, 4 ) !== "PK\x03\x04" ) {
				return new WP_Error( 'not_zip', 'Decoded bytes are not a zip archive (missing PK magic).' );
			}
			$tmp = wp_tempnam( 'pcd-plugin-zip.zip' );
			if ( ! $tmp ) {
				return new WP_Error( 'no_tempfile', 'Could not create a temp file for the upload.' );
			}
			if ( file_put_contents( $tmp, $bytes ) === false ) {
				wp_delete_file( $tmp );
				return new WP_Error( 'write_failed', 'Could not write zip data to temp file.' );
			}
			$local_path = $tmp;
			$cleanup    = function () use ( $tmp ) { if ( file_exists( $tmp ) ) wp_delete_file( $tmp ); };
		}

		$meta = self::peek_plugin_zip( $local_path );

		if ( empty( $args['confirm'] ) ) {
			if ( $cleanup ) $cleanup();
			return [
				'requires_confirmation' => true,
				'source'                => $zip_url ? 'url' : 'upload',
				'zip_url'               => $zip_url ?: null,
				'decoded_bytes'         => $zip_url ? null : strlen( $bytes ),
				'detected'              => $meta,
				'will_activate'         => ! empty( $args['activate'] ),
				'will_overwrite'        => ! empty( $args['overwrite'] ),
				'message'               => 'Ready to install zip. Re-call with confirm:true to proceed.',
			];
		}

		WP_Filesystem();
		$skin     = new Automatic_Upgrader_Skin();
		$upgrader = new Plugin_Upgrader( $skin );
		$options  = [];
		if ( ! empty( $args['overwrite'] ) ) {
			$options['overwrite_package'] = true;
		}

		PCD_PC_Snapshot::capture( 'option', 'active_plugins', get_option( 'active_plugins' ), 'active_plugins before install_zip' );

		$result = $upgrader->install( $local_path, $options );
		if ( $cleanup ) $cleanup();

		if ( is_wp_error( $result ) ) return $result;
		if ( ! $result ) {
			return new WP_Error( 'install_failed', 'Install failed. ' . implode( '; ', $skin->get_upgrade_messages() ) );
		}

		$plugin_file = method_exists( $upgrader, 'plugin_info' ) ? $upgrader->plugin_info() : '';
		$activated   = false;
		if ( ! empty( $args['activate'] ) && $plugin_file ) {
			$activate_result = activate_plugin( $plugin_file );
			$activated = ! is_wp_error( $activate_result );
		}

		return [
			'installed'   => true,
			'plugin_file' => $plugin_file ?: null,
			'detected'    => $meta,
			'activated'   => $activated,
			'messages'    => $skin->get_upgrade_messages(),
		];
	}

	/**
	 * Look inside a plugin zip for the top-level PHP file carrying the
	 * "Plugin Name:" header. Used for the confirm-phase preview so the AI can
	 * tell the user exactly what's about to land on their site.
	 */
	private static function peek_plugin_zip( string $path ): array {
		if ( ! class_exists( 'ZipArchive' ) ) {
			return [ 'note' => 'ZipArchive unavailable — could not peek inside.' ];
		}
		$zip = new ZipArchive();
		if ( $zip->open( $path ) !== true ) {
			return [ 'note' => 'Could not open zip for inspection.' ];
		}
		$candidate = null;
		for ( $i = 0; $i < $zip->numFiles; $i++ ) {
			$name = $zip->getNameIndex( $i );
			if ( substr_count( $name, '/' ) === 1 && substr( $name, -4 ) === '.php' ) {
				$contents = $zip->getFromIndex( $i, 8192 );
				if ( $contents && stripos( $contents, 'Plugin Name:' ) !== false ) {
					$candidate = [
						'entry'  => $name,
						'header' => self::parse_plugin_header( $contents ),
					];
					break;
				}
			}
		}
		$zip->close();
		return $candidate ?: [ 'note' => 'No plugin header found in zip.' ];
	}

	private static function parse_plugin_header( string $contents ): array {
		$out  = [];
		$keys = [ 'Plugin Name', 'Version', 'Description', 'Author', 'Plugin URI', 'Requires PHP', 'Requires at least' ];
		foreach ( $keys as $key ) {
			if ( preg_match( '/^[ \t\/*#@]*' . preg_quote( $key, '/' ) . ':(.*)$/mi', $contents, $m ) ) {
				$out[ strtolower( str_replace( ' ', '_', $key ) ) ] = trim( wp_strip_all_tags( $m[1] ) );
			}
		}
		return $out;
	}

	public static function plugin_update( array $args ) {
		if ( $err = self::file_mods_blocked() ) return $err;
		$file = self::resolve_plugin_file( (string) ( $args['plugin'] ?? '' ) );
		if ( ! $file ) return new WP_Error( 'not_found', 'Plugin not found: ' . ( $args['plugin'] ?? '' ) );

		wp_update_plugins();
		$update_data = get_site_transient( 'update_plugins' );
		if ( ! isset( $update_data->response[ $file ] ) ) {
			return new WP_Error( 'no_update', 'No update available for ' . $file );
		}
		$update = $update_data->response[ $file ];

		self::load_plugin_includes();
		$plugins = get_plugins();
		$current = $plugins[ $file ];

		if ( empty( $args['confirm'] ) ) {
			return [
				'requires_confirmation' => true,
				'plugin'          => $file,
				'name'            => $current['Name'],
				'current_version' => $current['Version'],
				'new_version'     => $update->new_version,
				'message'         => sprintf(
					'Ready to update %s from v%s to v%s. Re-call with confirm:true to proceed.',
					$current['Name'],
					$current['Version'],
					$update->new_version
				),
			];
		}

		require_once ABSPATH . 'wp-admin/includes/file.php';
		require_once ABSPATH . 'wp-admin/includes/misc.php';
		require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';

		$skin     = new Automatic_Upgrader_Skin();
		$upgrader = new Plugin_Upgrader( $skin );
		$result   = $upgrader->upgrade( $file );

		if ( is_wp_error( $result ) ) return $result;
		if ( ! $result ) {
			return new WP_Error( 'update_failed', 'Update failed. ' . implode( '; ', $skin->get_upgrade_messages() ) );
		}

		return [
			'plugin'      => $file,
			'name'        => $current['Name'],
			'updated'     => true,
			'new_version' => $update->new_version,
		];
	}

	public static function plugin_delete( array $args ) {
		if ( $err = self::file_mods_blocked() ) return $err;
		$file = self::resolve_plugin_file( (string) ( $args['plugin'] ?? '' ) );
		if ( ! $file ) return new WP_Error( 'not_found', 'Plugin not found: ' . ( $args['plugin'] ?? '' ) );

		self::load_plugin_includes();
		$plugins   = get_plugins();
		$data      = $plugins[ $file ];
		$is_active = is_plugin_active( $file );

		if ( empty( $args['confirm'] ) ) {
			$warnings = [];
			if ( $is_active ) {
				$warnings[] = 'Plugin is currently ACTIVE — it will be deactivated before deletion.';
			}
			$warnings[] = 'Plugin uninstall scripts will run, which may permanently delete the plugin\'s database tables and options.';
			$warnings[] = 'Deletion cannot be undone via PressConduit snapshots — files are removed from disk.';

			return [
				'requires_confirmation' => true,
				'plugin'    => $file,
				'name'      => $data['Name'],
				'version'   => $data['Version'],
				'is_active' => $is_active,
				'warnings'  => $warnings,
				'message'   => sprintf(
					'Ready to delete %s v%s. Re-call with confirm:true to proceed.',
					$data['Name'],
					$data['Version']
				),
			];
		}

		// Snapshot what we can — active_plugins option, in case the user wants to re-install + re-activate.
		PCD_PC_Snapshot::capture( 'option', 'active_plugins', get_option( 'active_plugins' ), "active_plugins before delete {$file}" );

		if ( $is_active ) {
			deactivate_plugins( $file );
		}

		require_once ABSPATH . 'wp-admin/includes/plugin.php';
		require_once ABSPATH . 'wp-admin/includes/file.php';

		$result = delete_plugins( [ $file ] );
		if ( is_wp_error( $result ) ) return $result;
		if ( ! $result ) {
			return new WP_Error( 'delete_failed', 'Plugin deletion failed. Check filesystem permissions.' );
		}

		return [
			'plugin'  => $file,
			'name'    => $data['Name'],
			'deleted' => true,
		];
	}

	// ------------------------------------------------------------------
	// THEMES
	// ------------------------------------------------------------------

	public static function theme_list( array $args ): array {
		$themes = wp_get_themes();
		$active = get_stylesheet();
		$rows = [];
		foreach ( $themes as $slug => $theme ) {
			$rows[] = [
				'stylesheet' => $slug,
				'name'       => $theme->get( 'Name' ),
				'version'    => $theme->get( 'Version' ),
				'author'     => wp_strip_all_tags( $theme->get( 'Author' ) ),
				'template'   => $theme->get_template(),
				'is_child'   => $theme->parent() ? true : false,
				'parent'     => $theme->parent() ? $theme->parent()->get_stylesheet() : null,
				'status'     => $slug === $active ? 'active' : 'inactive',
			];
		}
		return [ 'count' => count( $rows ), 'active' => $active, 'themes' => $rows ];
	}

	public static function theme_delete( array $args ) {
		if ( $err = self::file_mods_blocked() ) return $err;
		$slug = (string) ( $args['stylesheet'] ?? '' );
		if ( ! $slug ) return new WP_Error( 'no_slug', 'stylesheet is required.' );

		$theme = wp_get_theme( $slug );
		if ( ! $theme->exists() ) return new WP_Error( 'not_found', "Theme not found: {$slug}" );
		if ( $slug === get_stylesheet() ) return new WP_Error( 'active_theme', "Cannot delete the active theme ({$slug}). Activate a different theme first." );
		if ( $slug === get_template() ) return new WP_Error( 'parent_theme', "Cannot delete the parent template ({$slug}) of the active theme." );

		if ( empty( $args['confirm'] ) ) {
			return [
				'requires_confirmation' => true,
				'stylesheet' => $slug,
				'name'       => $theme->get( 'Name' ),
				'version'    => $theme->get( 'Version' ),
				'message'    => sprintf( 'Ready to delete theme \'%s\'. Re-call with confirm:true to proceed.', $theme->get( 'Name' ) ),
			];
		}

		require_once ABSPATH . 'wp-admin/includes/theme.php';
		require_once ABSPATH . 'wp-admin/includes/file.php';

		$result = delete_theme( $slug );
		if ( is_wp_error( $result ) ) return $result;
		if ( ! $result ) {
			return new WP_Error( 'delete_failed', 'Theme deletion failed.' );
		}

		return [
			'stylesheet' => $slug,
			'name'       => $theme->get( 'Name' ),
			'deleted'    => true,
		];
	}

	public static function theme_activate( array $args ) {
		$slug = (string) ( $args['stylesheet'] ?? '' );
		if ( ! $slug ) {
			return new WP_Error( 'no_slug', 'stylesheet is required.' );
		}
		$theme = wp_get_theme( $slug );
		if ( ! $theme->exists() ) {
			return new WP_Error( 'not_found', "Theme not found: {$slug}" );
		}
		$prior_stylesheet = get_stylesheet();
		PCD_PC_Snapshot::capture( 'option', 'stylesheet', $prior_stylesheet, "stylesheet before activate {$slug}" );
		switch_theme( $slug );
		return [ 'stylesheet' => $slug, 'previous' => $prior_stylesheet, 'status' => 'activated' ];
	}

	// ------------------------------------------------------------------
	// TAXONOMIES / TERMS
	// ------------------------------------------------------------------

	public static function taxonomy_list( array $args ): array {
		$object_type = (string) ( $args['object_type'] ?? '' );
		$taxes = $object_type ? get_object_taxonomies( $object_type, 'objects' ) : get_taxonomies( [], 'objects' );
		$rows = [];
		foreach ( $taxes as $slug => $tax ) {
			$rows[] = [
				'slug'         => $slug,
				'label'        => $tax->label,
				'hierarchical' => $tax->hierarchical,
				'public'       => $tax->public,
				'object_type'  => $tax->object_type,
				'show_in_rest' => $tax->show_in_rest,
			];
		}
		return [ 'count' => count( $rows ), 'taxonomies' => $rows ];
	}

	public static function term_list( array $args ) {
		$tax = (string) ( $args['taxonomy'] ?? '' );
		if ( ! taxonomy_exists( $tax ) ) {
			return new WP_Error( 'no_tax', "Taxonomy not found: {$tax}" );
		}
		$terms = get_terms( [
			'taxonomy'   => $tax,
			'hide_empty' => false,
			'search'     => $args['search'] ?? '',
			'number'     => min( 500, (int) ( $args['per_page'] ?? 100 ) ),
		] );
		if ( is_wp_error( $terms ) ) return $terms;

		$rows = array_map( function ( $t ) {
			return [
				'id'          => $t->term_id,
				'name'        => $t->name,
				'slug'        => $t->slug,
				'description' => $t->description,
				'parent_id'   => $t->parent,
				'count'       => $t->count,
			];
		}, $terms );

		return [ 'count' => count( $rows ), 'taxonomy' => $tax, 'terms' => $rows ];
	}

	public static function term_create( array $args ) {
		$tax = (string) ( $args['taxonomy'] ?? '' );
		if ( ! taxonomy_exists( $tax ) ) {
			return new WP_Error( 'no_tax', "Taxonomy not found: {$tax}" );
		}
		$result = wp_insert_term(
			(string) $args['name'],
			$tax,
			array_filter( [
				'slug'        => $args['slug']        ?? null,
				'description' => $args['description'] ?? null,
				'parent'      => $args['parent_id']   ?? null,
			], function ( $v ) { return null !== $v; } )
		);
		if ( is_wp_error( $result ) ) return $result;
		return [ 'term_id' => $result['term_id'], 'taxonomy' => $tax ];
	}

	public static function term_assign( array $args ) {
		$post_id = (int) ( $args['post_id'] ?? 0 );
		$tax     = (string) ( $args['taxonomy'] ?? '' );
		$terms   = (array)  ( $args['terms'] ?? [] );
		$append  = ! empty( $args['append'] );

		if ( ! get_post( $post_id ) ) return new WP_Error( 'no_post', "Post #{$post_id} not found." );
		if ( ! taxonomy_exists( $tax ) ) return new WP_Error( 'no_tax', "Taxonomy not found: {$tax}" );

		// Convert numeric-looking strings to ints (term IDs).
		$normalized = array_map( function ( $t ) {
			return is_numeric( $t ) ? (int) $t : (string) $t;
		}, $terms );

		PCD_PC_Snapshot::capture( 'meta', "{$post_id}:tax:{$tax}", [
			'post_id' => $post_id,
			'key'     => "_tax:{$tax}",
			'value'   => wp_get_object_terms( $post_id, $tax, [ 'fields' => 'ids' ] ),
		], "Terms on post #{$post_id} / {$tax}" );

		$result = wp_set_object_terms( $post_id, $normalized, $tax, $append );
		if ( is_wp_error( $result ) ) return $result;
		return [ 'post_id' => $post_id, 'taxonomy' => $tax, 'term_ids' => $result ];
	}

	// ------------------------------------------------------------------
	// PERMALINKS
	// ------------------------------------------------------------------

	public static function flush_rewrite( array $args ): array {
		flush_rewrite_rules( false );
		return [ 'flushed' => true ];
	}

	// ------------------------------------------------------------------
	// USERS
	// ------------------------------------------------------------------

	public static function user_list( array $args ): array {
		$per_page = min( 200, max( 1, (int) ( $args['per_page'] ?? 50 ) ) );
		$page     = max( 1, (int) ( $args['page'] ?? 1 ) );
		$query_args = [
			'number' => $per_page,
			'paged'  => $page,
		];
		if ( ! empty( $args['role'] ) )   $query_args['role']    = $args['role'];
		if ( ! empty( $args['search'] ) ) $query_args['search']  = '*' . esc_attr( $args['search'] ) . '*';

		$q = new WP_User_Query( $query_args );
		$rows = [];
		foreach ( $q->get_results() as $u ) {
			$rows[] = [
				'id'           => $u->ID,
				'login'        => $u->user_login,
				'email'        => $u->user_email,
				'display_name' => $u->display_name,
				'roles'        => array_values( (array) $u->roles ),
				'registered'   => $u->user_registered,
			];
		}
		return [ 'count' => count( $rows ), 'total' => (int) $q->get_total(), 'users' => $rows ];
	}

	public static function user_get( array $args ) {
		$id = (int) ( $args['id'] ?? 0 );
		$u  = get_userdata( $id );
		if ( ! $u ) return new WP_Error( 'not_found', "User #{$id} not found." );
		return [
			'id'           => $u->ID,
			'login'        => $u->user_login,
			'email'        => $u->user_email,
			'first_name'   => $u->first_name,
			'last_name'    => $u->last_name,
			'display_name' => $u->display_name,
			'roles'        => array_values( (array) $u->roles ),
			'registered'   => $u->user_registered,
		];
	}

	public static function user_create( array $args ) {
		$login = sanitize_user( (string) ( $args['user_login'] ?? '' ), true );
		$email = sanitize_email( (string) ( $args['user_email'] ?? '' ) );
		if ( ! $login || ! $email ) return new WP_Error( 'bad_input', 'user_login and user_email are required.' );
		if ( username_exists( $login ) ) return new WP_Error( 'exists', "Username already exists: {$login}" );
		if ( email_exists( $email ) )    return new WP_Error( 'exists', "Email already exists: {$email}" );

		$pass = (string) ( $args['user_pass'] ?? wp_generate_password( 20, true, false ) );

		$user_id = wp_insert_user( [
			'user_login'   => $login,
			'user_email'   => $email,
			'user_pass'    => $pass,
			'first_name'   => $args['first_name']   ?? '',
			'last_name'    => $args['last_name']    ?? '',
			'display_name' => $args['display_name'] ?? '',
			'role'         => $args['role']         ?? 'subscriber',
		] );

		if ( is_wp_error( $user_id ) ) return $user_id;

		if ( empty( $args['skip_notification'] ) ) {
			wp_new_user_notification( $user_id, null, 'both' );
		}

		return [ 'id' => $user_id, 'login' => $login, 'email' => $email ];
	}

	public static function user_update( array $args ) {
		$id = (int) ( $args['id'] ?? 0 );
		$u  = get_userdata( $id );
		if ( ! $u ) return new WP_Error( 'not_found', "User #{$id} not found." );

		// Snapshot prior.
		PCD_PC_Snapshot::capture( 'meta', "user:{$id}", [
			'post_id' => 0,
			'key'     => "user:{$id}",
			'value'   => [
				'user_email'   => $u->user_email,
				'first_name'   => $u->first_name,
				'last_name'    => $u->last_name,
				'display_name' => $u->display_name,
				'roles'        => $u->roles,
			],
		], "User #{$id} before update" );

		$update = [ 'ID' => $id ];
		foreach ( [ 'user_email', 'first_name', 'last_name', 'display_name' ] as $field ) {
			if ( isset( $args[ $field ] ) ) $update[ $field ] = $args[ $field ];
		}
		if ( count( $update ) > 1 ) {
			$result = wp_update_user( $update );
			if ( is_wp_error( $result ) ) return $result;
		}

		if ( isset( $args['role'] ) ) {
			$u->set_role( (string) $args['role'] );
		}

		return [ 'id' => $id, 'updated' => true ];
	}

	public static function user_delete( array $args ) {
		$id = (int) ( $args['id'] ?? 0 );
		$u  = get_userdata( $id );
		if ( ! $u ) return new WP_Error( 'not_found', "User #{$id} not found." );
		if ( ! function_exists( 'wp_delete_user' ) ) {
			require_once ABSPATH . 'wp-admin/includes/user.php';
		}
		$reassign = ! empty( $args['reassign_to'] ) ? (int) $args['reassign_to'] : null;
		$result   = wp_delete_user( $id, $reassign );
		return $result ? [ 'id' => $id, 'deleted' => true, 'reassigned_to' => $reassign ] : new WP_Error( 'delete_failed', 'wp_delete_user returned false.' );
	}

	// ------------------------------------------------------------------
	// MENUS
	// ------------------------------------------------------------------

	public static function menu_list( array $args ): array {
		$menus = wp_get_nav_menus();
		$locations = get_nav_menu_locations();
		$location_by_menu = [];
		foreach ( $locations as $loc => $menu_id ) {
			$location_by_menu[ (int) $menu_id ][] = $loc;
		}
		$rows = [];
		foreach ( $menus as $m ) {
			$rows[] = [
				'id'         => $m->term_id,
				'name'       => $m->name,
				'slug'       => $m->slug,
				'item_count' => $m->count,
				'locations'  => $location_by_menu[ $m->term_id ] ?? [],
			];
		}
		$registered_locations = get_registered_nav_menus();
		return [
			'count'                => count( $rows ),
			'menus'                => $rows,
			'theme_locations'      => $registered_locations,
			'current_assignments'  => $locations,
		];
	}

	public static function menu_get( array $args ) {
		$menu = wp_get_nav_menu_object( $args['menu'] ?? '' );
		if ( ! $menu ) return new WP_Error( 'not_found', "Menu not found: {$args['menu']}" );
		$items = wp_get_nav_menu_items( $menu->term_id ) ?: [];
		$rows = [];
		foreach ( $items as $item ) {
			$rows[] = [
				'id'        => $item->ID,
				'title'     => $item->title,
				'url'       => $item->url,
				'type'      => $item->type,
				'object'    => $item->object,
				'object_id' => (int) $item->object_id,
				'parent_id' => (int) $item->menu_item_parent,
				'order'     => (int) $item->menu_order,
				'target'    => $item->target,
				'classes'   => is_array( $item->classes ) ? array_filter( $item->classes ) : [],
			];
		}
		return [
			'id'    => $menu->term_id,
			'name'  => $menu->name,
			'slug'  => $menu->slug,
			'items' => $rows,
		];
	}

	public static function menu_create( array $args ) {
		$name   = (string) ( $args['name'] ?? '' );
		if ( ! $name ) return new WP_Error( 'no_name', 'name is required.' );
		$result = wp_create_nav_menu( $name );
		if ( is_wp_error( $result ) ) return $result;
		return [ 'id' => (int) $result, 'name' => $name ];
	}

	public static function menu_assign_location( array $args ) {
		$location = (string) ( $args['location'] ?? '' );
		if ( ! $location ) return new WP_Error( 'no_location', 'location is required.' );

		$registered = get_registered_nav_menus();
		if ( ! isset( $registered[ $location ] ) ) {
			return new WP_Error( 'unknown_location', "Theme has no registered menu location: {$location}. Known: " . implode( ', ', array_keys( $registered ) ) );
		}

		$current = get_theme_mod( 'nav_menu_locations', [] );
		PCD_PC_Snapshot::capture( 'option', "theme_mod:nav_menu_locations", $current, 'Menu locations before assign' );

		$current[ $location ] = (int) ( $args['menu_id'] ?? 0 );
		set_theme_mod( 'nav_menu_locations', $current );
		return [ 'location' => $location, 'menu_id' => $current[ $location ] ];
	}

	public static function menu_item_add( array $args ) {
		$menu_id = (int) ( $args['menu_id'] ?? 0 );
		if ( ! wp_get_nav_menu_object( $menu_id ) ) {
			return new WP_Error( 'no_menu', "Menu #{$menu_id} not found." );
		}
		$type   = (string) ( $args['type'] ?? '' );
		$object = (string) ( $args['object'] ?? '' );
		$object_id = (int) ( $args['object_id'] ?? 0 );

		$item = [
			'menu-item-type'      => $type,
			'menu-item-status'    => 'publish',
			'menu-item-parent-id' => (int) ( $args['parent_id'] ?? 0 ),
		];
		if ( isset( $args['title'] ) ) $item['menu-item-title'] = (string) $args['title'];
		if ( isset( $args['url'] ) )   $item['menu-item-url']   = esc_url_raw( $args['url'] );

		if ( 'post_type' === $type ) {
			$item['menu-item-object']    = $object;
			$item['menu-item-object-id'] = $object_id;
		} elseif ( 'taxonomy' === $type ) {
			$item['menu-item-object']    = $object;
			$item['menu-item-object-id'] = $object_id;
		} elseif ( 'custom' === $type ) {
			if ( empty( $args['url'] ) ) return new WP_Error( 'no_url', 'custom items require url.' );
			if ( empty( $args['title'] ) ) return new WP_Error( 'no_title', 'custom items require title.' );
		} else {
			return new WP_Error( 'bad_type', "Unknown menu item type: {$type}" );
		}

		$result = wp_update_nav_menu_item( $menu_id, 0, $item );
		if ( is_wp_error( $result ) ) return $result;
		return [ 'menu_id' => $menu_id, 'item_id' => (int) $result ];
	}

	public static function menu_item_delete( array $args ) {
		$item_id = (int) ( $args['item_id'] ?? 0 );
		$post    = get_post( $item_id );
		if ( ! $post || 'nav_menu_item' !== $post->post_type ) {
			return new WP_Error( 'not_found', "Menu item #{$item_id} not found." );
		}
		$result = wp_delete_post( $item_id, true );
		return $result ? [ 'item_id' => $item_id, 'deleted' => true ] : new WP_Error( 'delete_failed', 'Could not delete menu item.' );
	}

	// ------------------------------------------------------------------
	// COMMENTS
	// ------------------------------------------------------------------

	public static function comment_list( array $args ): array {
		$status = $args['status'] ?? 'all';
		$query_args = [
			'number' => min( 200, max( 1, (int) ( $args['per_page'] ?? 50 ) ) ),
		];
		if ( 'all' !== $status ) $query_args['status'] = $status;
		if ( ! empty( $args['post_id'] ) ) $query_args['post_id'] = (int) $args['post_id'];
		if ( ! empty( $args['search'] ) )  $query_args['search']  = (string) $args['search'];

		$comments = get_comments( $query_args );
		$rows = [];
		foreach ( $comments as $c ) {
			$rows[] = [
				'id'        => (int) $c->comment_ID,
				'post_id'   => (int) $c->comment_post_ID,
				'author'    => $c->comment_author,
				'email'     => $c->comment_author_email,
				'content'   => mb_substr( $c->comment_content, 0, 1000 ),
				'date'      => $c->comment_date,
				'approved'  => $c->comment_approved,
			];
		}
		return [ 'count' => count( $rows ), 'comments' => $rows ];
	}

	public static function comment_moderate( array $args ) {
		$id     = (int) ( $args['id'] ?? 0 );
		$status = (string) ( $args['status'] ?? '' );
		$c      = get_comment( $id );
		if ( ! $c ) return new WP_Error( 'not_found', "Comment #{$id} not found." );

		PCD_PC_Snapshot::capture( 'meta', "comment:{$id}:status", [
			'post_id' => 0,
			'key'     => "comment:{$id}:status",
			'value'   => $c->comment_approved,
		], "Comment #{$id} status before moderate" );

		$map = [
			'approve' => '1',
			'hold'    => '0',
			'spam'    => 'spam',
			'trash'   => 'trash',
		];
		if ( ! isset( $map[ $status ] ) ) return new WP_Error( 'bad_status', "Status must be one of: " . implode( ', ', array_keys( $map ) ) );

		if ( 'trash' === $status ) {
			wp_trash_comment( $id );
		} else {
			wp_set_comment_status( $id, $status );
		}
		return [ 'id' => $id, 'status' => $status ];
	}

	// ------------------------------------------------------------------
	// BULK SEARCH-REPLACE
	// ------------------------------------------------------------------

	public static function search_replace( array $args ) {
		global $wpdb;
		$search   = (string) ( $args['search'] ?? '' );
		$replace  = (string) ( $args['replace'] ?? '' );
		$confirm  = ! empty( $args['confirm'] );
		$cs       = ! empty( $args['case_sensitive'] );
		$types    = is_array( $args['post_types'] ?? null ) && $args['post_types'] ? array_map( 'sanitize_key', $args['post_types'] ) : [ 'post', 'page' ];

		if ( '' === $search ) return new WP_Error( 'no_search', 'search is required.' );

		// Find candidate posts.
		$like         = '%' . $wpdb->esc_like( $search ) . '%';
		$placeholders = implode( ',', array_fill( 0, count( $types ), '%s' ) );
		$params       = array_merge( [ $like ], $types );

		// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery -- {$wpdb->posts} is core; the IN() placeholders are a fixed '%s' list passed to prepare().
		$sql = $wpdb->prepare(
			"SELECT ID, post_title, post_content FROM {$wpdb->posts} WHERE post_content LIKE %s AND post_type IN ({$placeholders}) AND post_status IN ('publish','draft','private') LIMIT 500",
			$params
		);
		$posts = $wpdb->get_results( $sql );
		// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery

		$matches = [];
		foreach ( $posts as $p ) {
			$count = $cs ? substr_count( $p->post_content, $search ) : substr_count( strtolower( $p->post_content ), strtolower( $search ) );
			if ( $count ) {
				$matches[] = [
					'id'          => (int) $p->ID,
					'title'       => $p->post_title,
					'match_count' => $count,
				];
			}
		}

		if ( ! $confirm ) {
			return [
				'dry_run'         => true,
				'matched_posts'   => count( $matches ),
				'total_matches'   => array_sum( array_column( $matches, 'match_count' ) ),
				'matches'         => $matches,
				'note'            => 'Re-call with confirm=true to apply.',
			];
		}

		// Apply with per-post snapshot.
		$updated = 0;
		foreach ( $matches as $m ) {
			$post = get_post( $m['id'] );
			if ( ! $post ) continue;
			PCD_PC_Snapshot::capture( 'post', (string) $post->ID, [
				'ID'           => $post->ID,
				'post_content' => $post->post_content,
			], "Post #{$post->ID} before search-replace" );

			$new_content = $cs
				? str_replace( $search, $replace, $post->post_content )
				: str_ireplace( $search, $replace, $post->post_content );
			wp_update_post( [ 'ID' => $post->ID, 'post_content' => wp_slash( $new_content ) ] );
			$updated++;
		}

		return [
			'applied'           => true,
			'updated_posts'     => $updated,
			'total_replacements'=> array_sum( array_column( $matches, 'match_count' ) ),
		];
	}
}
