<?php
namespace AdvancedUserActivityLog;

use WP_PowerSuite\Core\AbstractModule;
use WP_PowerSuite\Core\OptionStoreModuleSettingsRepository;
class Module extends AbstractModule {

	private const MODULE_ID = 'advanced-user-activity-log';
	private const MODULE_VERSION = '2.2.0';
	private const TABLE_NAME = 'wpps_activity_log';
	private const DB_VERSION = '2.2.0';
	private const EXPORT_MAX_ROWS = 25000;
	private const FAILED_LOGIN_THROTTLE_SECONDS = 60;
	private const CLEANUP_BATCH_SIZE = 1000;

	/** @var array<string, list<string>> */
	private const SEVERITY_TIERS = [
		'critical' => [ 'failed_login', 'user_deleted', 'post_deleted', 'password_reset', 'user_role_changed', 'module_disabled', 'plugin_deleted', 'theme_deleted', 'login_as_started' ],
		'warning'  => [ 'plugin_deactivated', 'logout', 'comment_deleted', 'media_deleted', 'term_deleted', 'user_updated', 'username_changed', 'core_update_started', 'menu_deleted', 'post_status_changed', 'comment_status_changed', 'permalink_changed', 'option_updated', 'widget_updated', 'login_as_expired', 'login_as_logout', 'login_as_invalid_original', 'login_as_module_disabled', 'session_destroy_session', 'session_destroy_all', 'session_destroy_all_site', 'session_limit_enforced' ],
		'success'  => [ 'login', 'user_created', 'plugin_activated', 'plugin_installed', 'plugin_updated', 'theme_installed', 'theme_switched', 'theme_updated', 'module_enabled', 'post_created', 'post_updated', 'comment_created', 'media_uploaded', 'menu_updated', 'term_created', 'term_edited', 'core_updated', 'login_as_switch_back' ],
	];

	private const INTERNAL_POST_TYPES = [
		'wp_font_face', 'wp_font_family', 'wp_global_styles', 'wp_navigation',
		'wp_template', 'wp_template_part', 'wp_block', 'oembed_cache',
		'user_request', 'customize_changeset', 'custom_css', 'revision', 'nav_menu_item',
	];
	private array $settings = [];
	/** @var array<string, true> */
	private static array $logged_event_keys = [];
	private static bool $cleanup_callback_registered = false;
	private static bool $table_ready = false;

	public function getId(): string {
		return self::MODULE_ID;
	}

	public function getName(): string {
		return __( 'Activity Log', 'wp-powersuite' );
	}

	public function getVersion(): string {
		return self::MODULE_VERSION;
	}

	public function getDescription(): string {
		return __( 'Keep a clear record of important dashboard activity—who logged in, what changed, and when—so you can investigate issues or stay audit-ready without digging through server logs.', 'wp-powersuite' );
	}

	public function getContext(): string|array {
		return 'both';
	}

	public function getDependencies(): array {
		return [];
	}

	private function isLicenseActive(): bool {
		return function_exists( 'wp_powersuite_is_license_valid' ) && wp_powersuite_is_license_valid();
	}

	/**
	 * Abort admin, export, or AJAX handlers when the premium license is inactive.
	 */
	private function denyUnlicensedRequest( bool $ajax = false ): void {
		$message = __( 'Activity Log requires an active license.', 'wp-powersuite' );

		if ( $ajax ) {
			wp_send_json_error(
				[
					'message'      => $message,
					'code'         => 'license_required',
					'license_page' => admin_url( 'admin.php?page=powersuite-license' ),
				]
			);
		}

		wp_die(
			esc_html( $message ),
			esc_html__( 'License required', 'wp-powersuite' ),
			[ 'response' => 403, 'back_link' => true ]
		);
	}

	protected function registerHooks(): void {
		if ( ! $this->isLicenseActive() ) {
			$this->clearScheduledCleanup();
			return;
		}

		add_action( 'wp_powersuite_module_toggled', [ $this, 'onPowerSuiteModuleToggled' ], 10, 3 );

		$this->settings = $this->getSettings();
		$this->registerActivityHooks();
		$this->scheduleCleanup();

		add_filter( 'wp_privacy_personal_data_exporters', [ $this, 'registerPrivacyExporter' ] );
		add_filter( 'wp_privacy_personal_data_erasers', [ $this, 'registerPrivacyEraser' ] );

		if ( ! is_admin() ) {
			return;
		}

		add_action( 'admin_init', [ $this, 'maybeCreateLogTable' ] );
		add_action( 'admin_menu', [ $this, 'addAdminPage' ], 20 );
		add_action( 'wp_ajax_wpps_activity_log_clear', [ $this, 'handleClearLogs' ] );
		add_action( 'wp_ajax_wpps_activity_log_export', [ $this, 'handleExportLogs' ] );
	}


	private function getSettings(): array {
$defaults = [
			'log_logins'           => true,
			'log_logouts'          => true,
			'log_failed_logins'    => true,
			'log_session_manager'  => true,
			'log_post_edits'       => true,
			'log_post_deletes'     => true,
			'log_user_changes'     => true,
			'log_user_deletes'     => true,
			'log_plugin_changes'   => true,
			'log_theme_changes'    => true,
			'log_settings_changes' => false,
			'log_media_uploads'    => true,
			'log_comments'         => true,
			'log_widget_changes'   => true,
			'log_menu_changes'     => true,
			'log_core_updates'     => true,
			'log_taxonomy_changes' => true,
			'log_permalink_changes' => true,
			'log_role_changes'     => true,
			'retention_days'       => 90,
			'auto_cleanup'         => true,
			'enable_ip_geolocation'        => false,
			'trust_proxy_headers'          => false,
			'exclude_internal_types_in_list' => true,
		];

		$settings = $this->moduleSettings()->all( [] );
		$merged   = array_merge( $defaults, $settings );
		$merged['retention_days'] = $this->clampRetentionDays( $merged['retention_days'] ?? 90 );
		$merged['auto_cleanup']   = true;

		return $this->applySettingDependencies( $merged );
	}

	private function applySettingDependencies( array $settings ): array {
		if ( ! $this->isLoginSessionManagerEnabled() ) {
			$settings['log_session_manager'] = false;
		}

		return $settings;
	}

	private function isLoginSessionManagerEnabled(): bool {
		return OptionStoreModuleSettingsRepository::isModuleEnabled( 'login-session-manager' );
	}

	private function shouldRegisterAdminActivityHooks(): bool {
		if ( is_admin() ) {
			return true;
		}

		if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
			return true;
		}

		if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
			return true;
		}

		if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
			return true;
		}

		if ( defined( 'WP_CLI' ) && WP_CLI ) {
			return true;
		}

		return false;
	}

	private function registerActivityHooks(): void {
		if ( ! empty( $this->settings['log_logins'] ) ) {
			add_action( 'wp_login', [ $this, 'logLogin' ], 10, 2 );
		}
		if ( ! empty( $this->settings['log_logouts'] ) ) {
			add_action( 'wp_logout', [ $this, 'logLogout' ] );
		}
		add_action( 'wp_powersuite_login_as_event', [ $this, 'logLoginAsEvent' ], 10, 4 );
		if ( ! empty( $this->settings['log_session_manager'] ) && $this->isLoginSessionManagerEnabled() ) {
			add_action( 'wp_powersuite_session_manager_event', [ $this, 'logSessionManagerEvent' ], 10, 4 );
		}
		if ( ! empty( $this->settings['log_failed_logins'] ) ) {
			// Second arg WP_Error @since WP 5.5 — skip 2FA intermediate step (password ok, code pending).
			add_action( 'wp_login_failed', [ $this, 'logFailedLogin' ], 10, 2 );
		}

		if ( ! empty( $this->settings['log_user_changes'] ) ) {
			add_action( 'wp_powersuite_username_changed', [ $this, 'logUsernameChanged' ], 10, 4 );
		}

		if ( ! $this->shouldRegisterAdminActivityHooks() ) {
			return;
		}

		if ( ! empty( $this->settings['log_post_edits'] ) ) {
			add_action( 'save_post', [ $this, 'logPostEdit' ], 10, 3 );
			add_action( 'transition_post_status', [ $this, 'logPostStatusChange' ], 10, 3 );
		}
		if ( ! empty( $this->settings['log_post_deletes'] ) ) {
			add_action( 'before_delete_post', [ $this, 'logPostDelete' ] );
		}

		if ( ! empty( $this->settings['log_user_changes'] ) ) {
			add_action( 'user_register', [ $this, 'logUserRegister' ] );
			add_action( 'profile_update', [ $this, 'logUserUpdate' ], 10, 2 );
			add_action( 'password_reset', [ $this, 'logPasswordReset' ], 10, 2 );
		}
		if ( ! empty( $this->settings['log_user_deletes'] ) ) {
			add_action( 'delete_user', [ $this, 'logUserDelete' ], 10, 2 );
		}
		if ( ! empty( $this->settings['log_role_changes'] ) ) {
			add_action( 'set_user_role', [ $this, 'logUserRoleChange' ], 10, 3 );
		}

		if ( ! empty( $this->settings['log_plugin_changes'] ) ) {
			add_action( 'activated_plugin', [ $this, 'logPluginActivate' ] );
			add_action( 'deactivated_plugin', [ $this, 'logPluginDeactivate' ] );
			add_action( 'delete_plugin', [ $this, 'logPluginDelete' ] );
			add_action( 'upgrader_process_complete', [ $this, 'logPluginUpdate' ], 10, 2 );
			add_action( 'install_plugin_complete', [ $this, 'logPluginInstall' ], 10, 3 );
		}

		if ( ! empty( $this->settings['log_theme_changes'] ) ) {
			add_action( 'switch_theme', [ $this, 'logThemeSwitch' ] );
			add_action( 'delete_theme', [ $this, 'logThemeDelete' ] );
			add_action( 'upgrader_process_complete', [ $this, 'logThemeUpdate' ], 10, 2 );
			add_action( 'install_theme_complete', [ $this, 'logThemeInstall' ], 10, 3 );
		}

		if ( ! empty( $this->settings['log_core_updates'] ) ) {
			add_action( 'upgrader_process_complete', [ $this, 'logCoreUpdate' ], 10, 2 );
			add_action( '_core_updated_successfully', [ $this, 'logCoreUpdated' ], 10, 2 );
		}

		if ( ! empty( $this->settings['log_settings_changes'] ) ) {
			add_action( 'updated_option', [ $this, 'logOptionUpdate' ], 10, 3 );
		}

		if ( ! empty( $this->settings['log_media_uploads'] ) ) {
			add_action( 'add_attachment', [ $this, 'logMediaUpload' ] );
			add_action( 'delete_attachment', [ $this, 'logMediaDelete' ] );
		}

		if ( ! empty( $this->settings['log_comments'] ) ) {
			add_action( 'wp_insert_comment', [ $this, 'logCommentInsert' ], 10, 2 );
			add_action( 'delete_comment', [ $this, 'logCommentDelete' ] );
			add_action( 'transition_comment_status', [ $this, 'logCommentStatusChange' ], 10, 3 );
		}

		if ( ! empty( $this->settings['log_widget_changes'] ) ) {
			add_action( 'update_option_sidebars_widgets', [ $this, 'logWidgetChange' ], 10, 2 );
		}

		if ( ! empty( $this->settings['log_menu_changes'] ) ) {
			add_action( 'wp_update_nav_menu', [ $this, 'logMenuUpdate' ] );
			add_action( 'wp_delete_nav_menu', [ $this, 'logMenuDelete' ] );
		}

		if ( ! empty( $this->settings['log_taxonomy_changes'] ) ) {
			add_action( 'created_term', [ $this, 'logTermCreated' ], 10, 3 );
			add_action( 'edited_term', [ $this, 'logTermEdited' ], 10, 3 );
			add_action( 'delete_term', [ $this, 'logTermDeleted' ], 10, 4 );
		}

		if ( ! empty( $this->settings['log_permalink_changes'] ) ) {
			add_action( 'update_option_permalink_structure', [ $this, 'logPermalinkChange' ], 10, 2 );
		}
	}

	private function shouldIgnorePostType( ?string $post_type ): bool {
		return (bool) $post_type && in_array( $post_type, self::INTERNAL_POST_TYPES, true );
	}

	private function clampRetentionDays( mixed $days ): int {
		$value = absint( $days );
		if ( $value < 1 ) {
			$value = 1;
		}
		if ( $value > 365 ) {
			$value = 365;
		}
		return $value;
	}

	private function dateFilterStart( string $date ): string {
		return $date . ' 00:00:00';
	}

	private function dateFilterEnd( string $date ): string {
		return $date . ' 23:59:59';
	}

	private function maskEmail( string $email ): string {
		if ( ! is_email( $email ) ) {
			return '[redacted]';
		}
		$parts = explode( '@', $email, 2 );
		$local = $parts[0];
		$domain = $parts[1] ?? '';
		if ( strlen( $local ) <= 1 ) {
			$masked_local = '*';
		} else {
			$masked_local = substr( $local, 0, 1 ) . str_repeat( '*', max( 1, strlen( $local ) - 1 ) );
		}
		return $masked_local . '@' . $domain;
	}

	private function formatUserRoles( \WP_User $user ): string {
		if ( empty( $user->roles ) || ! is_array( $user->roles ) ) {
			return '';
		}
		return implode( ', ', array_map( 'strval', $user->roles ) );
	}

	private function shouldSkipDuplicateEvent( string $event_key ): bool {
		if ( isset( self::$logged_event_keys[ $event_key ] ) ) {
			return true;
		}
		self::$logged_event_keys[ $event_key ] = true;
		return false;
	}

	private function markPostSavedThisRequest( int $post_id ): void {
		self::$logged_event_keys[ 'post_saved:' . $post_id ] = true;
	}

	private function wasPostSavedThisRequest( int $post_id ): bool {
		return isset( self::$logged_event_keys[ 'post_saved:' . $post_id ] );
	}

	public static function getSeverityForAction( string $action ): string {
		foreach ( self::SEVERITY_TIERS as $severity => $actions ) {
			if ( in_array( $action, $actions, true ) ) {
				return $severity;
			}
		}
		return 'info';
	}

	/**
	 * @return list<string>
	 */
	private function mergedHighPriorityActions(): array {
		return array_values(
			array_unique(
				array_merge(
					self::SEVERITY_TIERS['critical'],
					self::SEVERITY_TIERS['warning'],
					self::SEVERITY_TIERS['success']
				)
			)
		);
	}

	/**
	 * @param array<string, mixed> $filters
	 * @return array{where: string, values: list<mixed>}
	 */
	private function buildLogsWhere( array $filters ): array {
		$settings = $this->settings !== [] ? $this->settings : $this->getSettings();

		$where = [ '1=1' ];
		$values = [];

		$search = (string) ( $filters['search'] ?? '' );
		if ( $search !== '' ) {
			$where[] = '(description LIKE %s OR user_login LIKE %s OR object_name LIKE %s OR ip_address LIKE %s)';
			global $wpdb;
			$like = '%' . $wpdb->esc_like( $search ) . '%';
			$values[] = $like;
			$values[] = $like;
			$values[] = $like;
			$values[] = $like;
		}

		$filter_action = (string) ( $filters['filter_action'] ?? '' );
		if ( $filter_action !== '' ) {
			$where[] = 'action = %s';
			$values[] = $filter_action;
		}

		$filter_user = (int) ( $filters['filter_user'] ?? 0 );
		if ( $filter_user > 0 ) {
			$where[] = 'user_id = %d';
			$values[] = $filter_user;
		}

		$filter_object_type = (string) ( $filters['filter_object_type'] ?? '' );
		if ( $filter_object_type !== '' ) {
			$where[] = 'object_type = %s';
			$values[] = $filter_object_type;
		}

		$date_from = (string) ( $filters['date_from'] ?? '' );
		if ( $date_from !== '' && preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date_from ) ) {
			$where[] = 'created_at >= %s';
			$values[] = $this->dateFilterStart( $date_from );
		}

		$date_to = (string) ( $filters['date_to'] ?? '' );
		if ( $date_to !== '' && preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date_to ) ) {
			$where[] = 'created_at <= %s';
			$values[] = $this->dateFilterEnd( $date_to );
		}

		$filter_ip = (string) ( $filters['filter_ip'] ?? '' );
		if ( $filter_ip !== '' ) {
			global $wpdb;
			$where[] = 'ip_address LIKE %s';
			$values[] = '%' . $wpdb->esc_like( $filter_ip ) . '%';
		}

		$filter_role = (string) ( $filters['filter_role'] ?? '' );
		if ( $filter_role !== '' ) {
			$where[] = 'user_role = %s';
			$values[] = $filter_role;
		}

		$filter_severity = sanitize_key( (string) ( $filters['filter_severity'] ?? '' ) );
		if ( $filter_action === '' && $filter_severity !== '' ) {
			if ( isset( self::SEVERITY_TIERS[ $filter_severity ] ) ) {
				$acts = self::SEVERITY_TIERS[ $filter_severity ];
				$ph = implode( ',', array_fill( 0, count( $acts ), '%s' ) );
				$where[] = "action IN ({$ph})";
				$values = array_merge( $values, $acts );
			} elseif ( $filter_severity === 'info' ) {
				$exclude = $this->mergedHighPriorityActions();
				if ( ! empty( $exclude ) ) {
					$ph = implode( ',', array_fill( 0, count( $exclude ), '%s' ) );
					$where[] = "action NOT IN ({$ph})";
					$values = array_merge( $values, $exclude );
				}
			}
		}

		if ( ! empty( $settings['exclude_internal_types_in_list'] ) ) {
			$types = self::INTERNAL_POST_TYPES;
			$ph = implode( ',', array_fill( 0, count( $types ), '%s' ) );
			$where[] = "(object_type IS NULL OR object_type NOT IN ({$ph}))";
			$values = array_merge( $values, $types );
		}

		return [
			'where'  => implode( ' AND ', $where ),
			'values' => $values,
		];
	}

	public function getDashboardStats(): array {
		if ( ! $this->isLicenseActive() ) {
			return [
				'total'        => 0,
				'today'        => 0,
				'failed_30d'   => 0,
				'unique_users' => 0,
			];
		}

		global $wpdb;
		$table_name = $wpdb->prefix . self::TABLE_NAME;
		if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) ) !== $table_name ) {
			return [
				'total'        => 0,
				'today'        => 0,
				'failed_30d'   => 0,
				'unique_users' => 0,
			];
		}

		$base = $this->buildLogsWhere( [] );

		$make_where = static function ( string $extra_sql, array $extra_values ) use ( $base ): array {
			return [
				'sql'    => $base['where'] . ' AND ' . $extra_sql,
				'values' => array_merge( $base['values'], $extra_values ),
			];
		};

		$t = $make_where( 'created_at >= CURDATE() AND created_at < DATE_ADD(CURDATE(), INTERVAL 1 DAY)', [] );
		$today = (int) $wpdb->get_var( $wpdb->prepare(
			"SELECT COUNT(*) FROM {$table_name} WHERE {$t['sql']}",
			$t['values']
		) );

		$f = $make_where( "action = 'failed_login' AND created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)", [] );
		$failed = (int) $wpdb->get_var( $wpdb->prepare(
			"SELECT COUNT(*) FROM {$table_name} WHERE {$f['sql']}",
			$f['values']
		) );

		$u = $make_where( 'user_id IS NOT NULL AND user_id > 0', [] );
		$unique = (int) $wpdb->get_var( $wpdb->prepare(
			"SELECT COUNT(DISTINCT user_id) FROM {$table_name} WHERE {$u['sql']}",
			$u['values']
		) );

		$total_q = "SELECT COUNT(*) FROM {$table_name} WHERE {$base['where']}";
		$total = ! empty( $base['values'] )
			? (int) $wpdb->get_var( $wpdb->prepare( $total_q, $base['values'] ) )
			: (int) $wpdb->get_var( $total_q );

		return [
			'total'        => $total,
			'today'        => $today,
			'failed_30d'   => $failed,
			'unique_users' => $unique,
		];
	}

	private function sanitizeCsvValue( mixed $value ): string {
		if ( $value === null ) {
			return '';
		}
		$s = (string) $value;
		if ( preg_match( '/^[=+\-@\t\r]/', $s ) ) {
			return "'" . $s;
		}
		return $s;
	}

	/**
	 * Ensure Table Exists
	 *
	 * Ensures the log table exists (lazy initialization).
	 * Used when table is needed on frontend before admin_init runs.
	 *
	 * @return void
	 */
	private function ensureTableExists(): void {
		if ( self::$table_ready ) {
			return;
		}

		global $wpdb;
		$table_name = $wpdb->prefix . self::TABLE_NAME;
		$cache_key = 'wpps_activity_log_table_exists';

		$table_exists = wp_cache_get( $cache_key );
		if ( $table_exists === false ) {
			$table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) ) === $table_name;
			wp_cache_set( $cache_key, $table_exists, '', 3600 );
		}

		if ( ! $table_exists ) {
			$this->maybeCreateLogTable();
		}

		self::$table_ready = true;
	}

	/**
	 * Maybe Create Log Table
	 *
	 * Creates the log table if it doesn't exist.
	 * Called on admin_init to avoid hook bloat during module init.
	 *
	 * @return void
	 */
	public function maybeCreateLogTable(): void {
		global $wpdb;

		$table_name = $wpdb->prefix . self::TABLE_NAME;
		
		// Cache table existence check to avoid query on every init
		$cache_key = 'wpps_activity_log_table_exists';
		$table_exists = wp_cache_get( $cache_key );
		
		if ( $table_exists === false ) {
			$table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) ) === $table_name;
			wp_cache_set( $cache_key, $table_exists, '', 3600 ); // Cache for 1 hour
		}
		
		if ( $table_exists ) {
			// Only check for missing columns occasionally (not on every init)
			$columns_check_key = 'wpps_activity_log_columns_checked';
			if ( ! wp_cache_get( $columns_check_key ) ) {
				$this->addMissingColumns( $table_name );
				$this->maybeUpgradeSchema( $table_name );
				wp_cache_set( $columns_check_key, true, '', 3600 ); // Check once per hour
			}
			self::$table_ready = true;
			return;
		}

		// Table doesn't exist, create it
		$charset_collate = $wpdb->get_charset_collate();

		$sql = "CREATE TABLE IF NOT EXISTS {$table_name} (
			id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
			user_id bigint(20) UNSIGNED DEFAULT NULL,
			user_login varchar(60) DEFAULT NULL,
			user_role varchar(100) DEFAULT NULL,
			action varchar(100) NOT NULL,
			object_type varchar(50) DEFAULT NULL,
			object_id bigint(20) UNSIGNED DEFAULT NULL,
			object_name varchar(255) DEFAULT NULL,
			description text,
			ip_address varchar(45) DEFAULT NULL,
			user_agent text,
			country varchar(100) DEFAULT NULL,
			country_code varchar(2) DEFAULT NULL,
			device_type varchar(50) DEFAULT NULL,
			browser varchar(100) DEFAULT NULL,
			os varchar(100) DEFAULT NULL,
			created_at datetime NOT NULL,
			PRIMARY KEY (id),
			KEY user_id (user_id),
			KEY user_role (user_role),
			KEY action (action),
			KEY object_type (object_type),
			KEY object_id (object_id),
			KEY created_at (created_at),
			KEY country_code (country_code)
		) {$charset_collate};";

		require_once ABSPATH . 'wp-admin/includes/upgrade.php';
		dbDelta( $sql );

		$this->addMissingColumns( $table_name );
		$this->maybeUpgradeSchema( $table_name );
		wp_cache_set( $cache_key, true, '', 3600 );
	}

	private function maybeUpgradeSchema( string $table_name ): void {
		global $wpdb;

		$current = get_option( 'wpps_activity_log_db_version', '' );
		if ( $current === self::DB_VERSION ) {
			return;
		}

		$indexes = $wpdb->get_results( "SHOW INDEXES FROM {$table_name} WHERE Key_name = 'object_id'" );
		if ( empty( $indexes ) ) {
			$wpdb->query( "ALTER TABLE {$table_name} ADD INDEX object_id (object_id)" );
		}

		update_option( 'wpps_activity_log_db_version', self::DB_VERSION, false );
	}

	private function addMissingColumns( string $table_name ): void {
		global $wpdb;

		// Cache column list to avoid DESCRIBE query on every check
		$columns_cache_key = 'wpps_activity_log_columns_' . $table_name;
		$existing_columns = wp_cache_get( $columns_cache_key );
		
		if ( $existing_columns === false ) {
			$existing_columns = $wpdb->get_col( "DESCRIBE {$table_name}" );
			wp_cache_set( $columns_cache_key, $existing_columns, '', 3600 ); // Cache for 1 hour
		}

		$columns_to_add = [
			'user_role'    => "ALTER TABLE {$table_name} ADD COLUMN user_role varchar(100) DEFAULT NULL",
			'country'      => "ALTER TABLE {$table_name} ADD COLUMN country varchar(100) DEFAULT NULL",
			'country_code' => "ALTER TABLE {$table_name} ADD COLUMN country_code varchar(2) DEFAULT NULL",
			'device_type'  => "ALTER TABLE {$table_name} ADD COLUMN device_type varchar(50) DEFAULT NULL",
			'browser'      => "ALTER TABLE {$table_name} ADD COLUMN browser varchar(100) DEFAULT NULL",
			'os'           => "ALTER TABLE {$table_name} ADD COLUMN os varchar(100) DEFAULT NULL",
		];

		foreach ( $columns_to_add as $column_name => $sql ) {
			if ( ! in_array( $column_name, $existing_columns, true ) ) {
				$wpdb->query( $sql );
				if ( $column_name === 'country_code' ) {
					$indexes = $wpdb->get_results( "SHOW INDEXES FROM {$table_name} WHERE Key_name = 'country_code'" );
					if ( empty( $indexes ) ) {
						$wpdb->query( "ALTER TABLE {$table_name} ADD INDEX country_code (country_code)" );
					}
				}
				if ( $column_name === 'user_role' ) {
					$indexes = $wpdb->get_results( "SHOW INDEXES FROM {$table_name} WHERE Key_name = 'user_role'" );
					if ( empty( $indexes ) ) {
						$wpdb->query( "ALTER TABLE {$table_name} ADD INDEX user_role (user_role)" );
					}
				}
			}
		}

		wp_cache_delete( 'wpps_activity_log_geo_cols_' . md5( $table_name ), '' );
		wp_cache_delete( $columns_cache_key, '' );
	}

	private function scheduleCleanup(): void {
		$hook = 'wpps_activity_log_cleanup';

		if ( ! empty( $this->settings['auto_cleanup'] ) ) {
			if ( ! wp_next_scheduled( $hook ) ) {
				wp_schedule_event( time(), 'daily', $hook );
			}
			if ( ! self::$cleanup_callback_registered ) {
				add_action( $hook, [ $this, 'cleanupOldLogs' ] );
				self::$cleanup_callback_registered = true;
			}
		} else {
			while ( ( $timestamp = wp_next_scheduled( $hook ) ) ) {
				wp_unschedule_event( $timestamp, $hook );
			}
		}
	}

	private function logActivity( ?int $user_id, string $action, ?string $object_type, ?int $object_id, string $description, ?string $object_name = null ): void {
		if ( ! $this->isLicenseActive() ) {
			return;
		}

		global $wpdb;

		$table_name = $wpdb->prefix . self::TABLE_NAME;
		$dedupe_id  = $object_id ? (string) $object_id : md5( $description );
		$event_key  = $action . ':' . (string) ( $object_type ?? '' ) . ':' . $dedupe_id;

		if ( $this->shouldSkipDuplicateEvent( $event_key ) ) {
			return;
		}

		$this->ensureTableExists();

		$user_login = null;
		$user_role = null;
		if ( $user_id ) {
			$user = get_userdata( $user_id );
			$user_login = $user ? $user->user_login : null;
			if ( $user ) {
				$user_role = $this->formatUserRoles( $user );
			}
		}

		$ip_address = $this->getClientIp();
		$user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : null;

		$ip_data = [];
		if ( $ip_address && $ip_address !== '0.0.0.0' ) {
			$ip_data = $this->enrichIpData( $ip_address, $user_agent );
		}

		$row = [
			'user_id'     => $user_id,
			'user_login'  => $user_login,
		];
		$formats = [ '%d', '%s' ];

		if ( $this->hasUserRoleColumn( $table_name ) ) {
			$row['user_role'] = $user_role;
			$formats[]        = '%s';
		}

		$row['action']      = sanitize_key( $action );
		$row['object_type'] = $object_type ? sanitize_key( $object_type ) : null;
		$row['object_id']   = $object_id;
		$row['object_name'] = $object_name ? sanitize_text_field( $object_name ) : null;
		$row['description'] = sanitize_textarea_field( $description );
		$row['ip_address']  = $ip_address;
		$row['user_agent']  = $user_agent;
		$row['created_at']  = current_time( 'mysql' );
		array_push( $formats, '%s', '%s', '%d', '%s', '%s', '%s', '%s', '%s' );

		if ( $this->hasGeoMetaColumns( $table_name ) ) {
			$row['country']      = $ip_data['country'] ?? null;
			$row['country_code'] = $ip_data['country_code'] ?? null;
			$row['device_type']  = $ip_data['device_type'] ?? null;
			$row['browser']      = $ip_data['browser'] ?? null;
			$row['os']           = $ip_data['os'] ?? null;
			array_push( $formats, '%s', '%s', '%s', '%s', '%s' );
		}

		$wpdb->insert( $table_name, $row, $formats );

		if ( $wpdb->last_error && defined( 'WP_DEBUG' ) && WP_DEBUG ) {
			error_log( 'WP PowerSuite Activity Log: Failed to insert log entry. Error: ' . $wpdb->last_error );
		}
	}

	private function hasGeoMetaColumns( string $table_name ): bool {
		$cache_key = 'wpps_activity_log_geo_cols_' . md5( $table_name );
		$cached = wp_cache_get( $cache_key );
		if ( false !== $cached ) {
			return (bool) $cached;
		}

		global $wpdb;
		$columns = $wpdb->get_col( "DESCRIBE {$table_name}" );
		$required = [ 'country', 'country_code', 'device_type', 'browser', 'os' ];
		foreach ( $required as $col ) {
			if ( ! in_array( $col, $columns, true ) ) {
				wp_cache_set( $cache_key, 0, '', 3600 );
				return false;
			}
		}
		wp_cache_set( $cache_key, 1, '', 3600 );
		return true;
	}

	private function hasUserRoleColumn( string $table_name ): bool {
		$columns_cache_key = 'wpps_activity_log_columns_' . $table_name;
		$existing_columns  = wp_cache_get( $columns_cache_key );
		if ( false === $existing_columns ) {
			global $wpdb;
			$existing_columns = $wpdb->get_col( "DESCRIBE {$table_name}" );
			wp_cache_set( $columns_cache_key, $existing_columns, '', 3600 );
		}
		return in_array( 'user_role', $existing_columns, true );
	}

	private function getClientIp(): string {
		$trust = ! empty( $this->settings['trust_proxy_headers'] )
			|| ( defined( 'WPPS_ACTIVITY_LOG_TRUST_PROXY_HEADERS' ) && WPPS_ACTIVITY_LOG_TRUST_PROXY_HEADERS );

		if ( $trust ) {
			$ip_keys = [
				'HTTP_CF_CONNECTING_IP',
				'HTTP_X_REAL_IP',
				'HTTP_X_FORWARDED_FOR',
				'REMOTE_ADDR',
			];
			foreach ( $ip_keys as $key ) {
				if ( ! empty( $_SERVER[ $key ] ) ) {
					$ip = sanitize_text_field( wp_unslash( $_SERVER[ $key ] ) );
					$ip = explode( ',', $ip )[0];
					$ip = trim( $ip );
					if ( filter_var( $ip, FILTER_VALIDATE_IP ) ) {
						return apply_filters( 'wpps_activity_log_client_ip', $ip );
					}
				}
			}
		}

		$ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : '';
		if ( $ip && filter_var( $ip, FILTER_VALIDATE_IP ) ) {
			return apply_filters( 'wpps_activity_log_client_ip', $ip );
		}

		return apply_filters( 'wpps_activity_log_client_ip', '0.0.0.0' );
	}

	private function enrichIpData( string $ip_address, ?string $user_agent ): array {
		$data = [
			'country'      => null,
			'country_code' => null,
			'device_type'  => null,
			'browser'      => null,
			'os'           => null,
		];

		if ( $user_agent ) {
			$ua_data = $this->parseUserAgent( $user_agent );
			$data['device_type'] = $ua_data['device_type'] ?? null;
			$data['browser']     = $ua_data['browser'] ?? null;
			$data['os']          = $ua_data['os'] ?? null;
		}

		if ( empty( $this->settings['enable_ip_geolocation'] ) ) {
			return $data;
		}

		if ( empty( $ip_address ) || $ip_address === '0.0.0.0' ) {
			return $data;
		}

		if ( filter_var( $ip_address, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) {
			$geo_data = $this->getIpGeolocation( $ip_address );
			if ( $geo_data ) {
				$data['country']      = $geo_data['country'] ?? null;
				$data['country_code'] = $geo_data['country_code'] ?? null;
			}
		}

		return $data;
	}

	private function getIpGeolocation( string $ip_address ): ?array {
		$cache_key = 'wpps_ip_geo_' . md5( $ip_address );
		$cached = get_transient( $cache_key );
		if ( $cached !== false && is_array( $cached ) ) {
			return $cached;
		}

		// Skip geolocation on AJAX/REST/CRON to avoid blocking
		if ( ( defined( 'DOING_AJAX' ) && DOING_AJAX ) || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) || ( defined( 'DOING_CRON' ) && DOING_CRON ) ) {
			return null;
		}

		$api_url = apply_filters(
			'wpps_activity_log_geo_api_url',
			'https://ip-api.com/json/' . rawurlencode( $ip_address ) . '?fields=status,country,countryCode',
			$ip_address
		);
		
		$response = wp_remote_get(
			$api_url,
			[
				'timeout'     => 1, // Very short timeout to prevent blocking
				'httpversion' => '1.1',
				'user-agent'  => 'WP PowerSuite Activity Log',
			]
		);

		// If request fails or times out, return null (don't block)
		if ( is_wp_error( $response ) ) {
			return null;
		}

		$body = wp_remote_retrieve_body( $response );
		if ( empty( $body ) ) {
			return null;
		}

		$json = json_decode( $body, true );

		if ( ! $json || empty( $json['status'] ) || $json['status'] !== 'success' ) {
			return null;
		}

		$geo_data = [
			'country'      => $json['country'] ?? null,
			'country_code' => strtolower( $json['countryCode'] ?? '' ),
		];

		// Cache for longer to reduce API calls
		set_transient( $cache_key, $geo_data, WEEK_IN_SECONDS );

		return $geo_data;
	}

	private function parseUserAgent( string $user_agent ): array {
		$data = [
			'device_type' => 'Desktop',
			'browser'     => 'Unknown',
			'os'          => 'Unknown',
		];

		if ( preg_match( '/mobile|android|iphone|ipod|blackberry|opera mini|iemobile|wpdesktop/i', $user_agent ) ) {
			$data['device_type'] = 'Mobile';
		} elseif ( preg_match( '/tablet|ipad|playbook|silk/i', $user_agent ) ) {
			$data['device_type'] = 'Tablet';
		}

		if ( preg_match( '/chrome/i', $user_agent ) && ! preg_match( '/edg|opr/i', $user_agent ) ) {
			$data['browser'] = 'Chrome';
		} elseif ( preg_match( '/firefox/i', $user_agent ) ) {
			$data['browser'] = 'Firefox';
		} elseif ( preg_match( '/safari/i', $user_agent ) && ! preg_match( '/chrome/i', $user_agent ) ) {
			$data['browser'] = 'Safari';
		} elseif ( preg_match( '/edg/i', $user_agent ) ) {
			$data['browser'] = 'Edge';
		} elseif ( preg_match( '/opr|opera/i', $user_agent ) ) {
			$data['browser'] = 'Opera';
		} elseif ( preg_match( '/msie|trident/i', $user_agent ) ) {
			$data['browser'] = 'Internet Explorer';
		}

		if ( preg_match( '/windows nt/i', $user_agent ) ) {
			$data['os'] = 'Windows';
		} elseif ( preg_match( '/macintosh|mac os x/i', $user_agent ) ) {
			$data['os'] = 'macOS';
		} elseif ( preg_match( '/linux/i', $user_agent ) ) {
			$data['os'] = 'Linux';
		} elseif ( preg_match( '/android/i', $user_agent ) ) {
			$data['os'] = 'Android';
		} elseif ( preg_match( '/iphone|ipad|ipod/i', $user_agent ) ) {
			$data['os'] = 'iOS';
		}

		return $data;
	}

	public function logLogin( string $user_login, \WP_User $user ): void {
		$this->logActivity(
			$user->ID,
			'login',
			'user',
			$user->ID,
			sprintf( 'User logged in: %s', $user_login )
		);
	}

	public function logLogout(): void {
		$user_id = get_current_user_id();
		if ( $user_id ) {
			$user = get_userdata( $user_id );
			$this->logActivity(
				$user_id,
				'logout',
				'user',
				$user_id,
				sprintf( 'User logged out: %s', $user->user_login )
			);
		}
	}

	/**
	 * @param string               $event     Impersonation event key.
	 * @param int                  $actor_id  Administrator user ID.
	 * @param int                  $target_id Impersonated user ID.
	 * @param array<string, mixed> $context   Optional ip / user_agent context.
	 */
	public function logLoginAsEvent( string $event, int $actor_id, int $target_id, array $context = [] ): void {
		unset( $context );

		$action_map = [
			'start'            => 'login_as_started',
			'switch_back'      => 'login_as_switch_back',
			'expired'          => 'login_as_expired',
			'logout'           => 'login_as_logout',
			'module_disabled'  => 'login_as_module_disabled',
			'invalid_original' => 'login_as_invalid_original',
		];

		$action = $action_map[ $event ] ?? 'login_as_' . sanitize_key( $event );
		$actor  = $actor_id > 0 ? get_userdata( $actor_id ) : false;
		$target = $target_id > 0 ? get_userdata( $target_id ) : false;

		$actor_label  = $actor instanceof \WP_User ? $actor->user_login : ( $actor_id > 0 ? '#' . $actor_id : __( 'unknown', 'wp-powersuite' ) );
		$target_label = $target instanceof \WP_User ? $target->user_login : ( $target_id > 0 ? '#' . $target_id : __( 'none', 'wp-powersuite' ) );

		$messages = [
			'login_as_started'            => sprintf( 'Administrator %1$s started viewing as %2$s', $actor_label, $target_label ),
			'login_as_switch_back'        => sprintf( 'Administrator %1$s switched back from %2$s', $actor_label, $target_label ),
			'login_as_expired'            => sprintf( 'Impersonation session for %1$s (admin %2$s) expired', $target_label, $actor_label ),
			'login_as_logout'             => sprintf( 'Impersonation of %1$s ended on logout (admin %2$s)', $target_label, $actor_label ),
			'login_as_module_disabled'    => sprintf( 'Login as User module disabled while %1$s was impersonating %2$s', $actor_label, $target_label ),
			'login_as_invalid_original'   => sprintf( 'Impersonation of %1$s ended: original admin %2$s is no longer allowed', $target_label, $actor_label ),
		];

		$description = $messages[ $action ] ?? sprintf( 'Login as User event: %s', $action );

		$this->logActivity(
			$actor_id > 0 ? $actor_id : null,
			$action,
			'user',
			$target_id > 0 ? $target_id : null,
			$description,
			$target instanceof \WP_User ? $target->user_login : null
		);
	}

	/**
	 * @param array<string, mixed> $context
	 */
	public function logSessionManagerEvent( string $action, int $actor_id, int $target_user_id, array $context = [] ): void {
		if ( empty( $this->settings['log_session_manager'] ) || ! $this->isLoginSessionManagerEnabled() ) {
			return;
		}

		unset( $context );

		$actor  = $actor_id > 0 ? get_userdata( $actor_id ) : false;
		$target = $target_user_id > 0 ? get_userdata( $target_user_id ) : false;

		$actor_label  = $actor instanceof \WP_User ? $actor->user_login : ( $actor_id > 0 ? '#' . $actor_id : __( 'system', 'wp-powersuite' ) );
		$target_label = $target instanceof \WP_User ? $target->user_login : ( $target_user_id > 0 ? '#' . $target_user_id : __( 'site', 'wp-powersuite' ) );

		$messages = [
			'destroy_session'  => sprintf( 'Session revoked for %1$s by %2$s', $target_label, $actor_label ),
			'destroy_all'        => sprintf( 'All sessions revoked for %1$s by %2$s', $target_label, $actor_label ),
			'destroy_all_site'   => sprintf( 'All site sessions revoked by %1$s', $actor_label ),
			'limit_enforced'     => sprintf( 'Concurrent session limit enforced for %1$s', $target_label ),
		];

		$log_action  = 'session_' . sanitize_key( $action );
		$description = $messages[ $action ] ?? sprintf( 'Session manager event: %s', $action );

		$this->logActivity(
			$actor_id > 0 ? $actor_id : null,
			$log_action,
			'user',
			$target_user_id > 0 ? $target_user_id : null,
			$description,
			$target instanceof \WP_User ? $target->user_login : null
		);
	}

	public function logFailedLogin( string $username, $error = null ): void {
		if ( $error instanceof \WP_Error && $error->get_error_code() === 'wpps_2fa_required' ) {
			return;
		}

		$ip = $this->getClientIp();
		$throttle_key = 'wpps_al_fail_' . md5( strtolower( sanitize_user( $username ) ) . '|' . $ip );
		if ( get_transient( $throttle_key ) ) {
			return;
		}
		set_transient( $throttle_key, 1, self::FAILED_LOGIN_THROTTLE_SECONDS );

		$this->logActivity(
			null,
			'failed_login',
			'user',
			null,
			sprintf( 'Failed login attempt: %s', sanitize_user( $username ) )
		);
	}

	public function logPostEdit( int $post_id, \WP_Post $post, bool $update ): void {
		if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) {
			return;
		}

		if ( $this->shouldIgnorePostType( $post->post_type ) ) {
			return;
		}

		$action = $update ? 'post_updated' : 'post_created';
		$post_type_obj = get_post_type_object( $post->post_type );
		$post_type_label = $post_type_obj ? $post_type_obj->labels->singular_name : 'Post';

		$this->logActivity(
			get_current_user_id(),
			$action,
			$post->post_type,
			$post_id,
			sprintf( '%s %s: %s', $update ? 'Updated' : 'Created', $post_type_label, $post->post_title )
		);

		$this->markPostSavedThisRequest( $post_id );
	}

	public function logPostStatusChange( string $new_status, string $old_status, \WP_Post $post ): void {
		if ( $new_status === $old_status || wp_is_post_revision( $post->ID ) ) {
			return;
		}

		if ( $this->shouldIgnorePostType( $post->post_type ) ) {
			return;
		}

		if ( $this->wasPostSavedThisRequest( $post->ID ) ) {
			return;
		}

		$post_type_obj = get_post_type_object( $post->post_type );
		$post_type_label = $post_type_obj ? $post_type_obj->labels->singular_name : 'Post';
		
		$this->logActivity(
			get_current_user_id(),
			'post_status_changed',
			$post->post_type,
			$post->ID,
			sprintf( 'Changed %s status from %s to %s: %s', $post_type_label, $old_status, $new_status, $post->post_title )
		);
	}

	public function logPostDelete( int $post_id ): void {
		$post = get_post( $post_id );
		if ( ! $post ) {
			return;
		}

		if ( $this->shouldIgnorePostType( $post->post_type ) ) {
			return;
		}

		$post_type_obj = get_post_type_object( $post->post_type );
		$post_type_label = $post_type_obj ? $post_type_obj->labels->singular_name : 'Post';

		$this->logActivity(
			get_current_user_id(),
			'post_deleted',
			$post->post_type,
			$post_id,
			sprintf( 'Deleted %s: %s', $post_type_label, $post->post_title )
		);
	}

	public function logUserRegister( int $user_id ): void {
		$user = get_userdata( $user_id );
		if ( $user ) {
			$this->logActivity(
				get_current_user_id() ?: $user_id,
				'user_created',
				'user',
				$user_id,
				sprintf( 'New user registered: %s (%s)', $user->user_login, $this->maskEmail( $user->user_email ) )
			);
		}
	}

	public function logUserUpdate( int $user_id, \WP_User $old_user_data ): void {
		$user = get_userdata( $user_id );
		if ( ! $user ) {
			return;
		}

		$changes = [];
		if ( $user->user_email !== $old_user_data->user_email ) {
			$changes[] = 'email';
		}
		if ( $user->display_name !== $old_user_data->display_name ) {
			$changes[] = 'display name';
		}
		if ( $user->user_url !== $old_user_data->user_url ) {
			$changes[] = 'website';
		}

		if ( ! empty( $changes ) ) {
			$this->logActivity(
				get_current_user_id() ?: $user_id,
				'user_updated',
				'user',
				$user_id,
				sprintf( 'Updated user %s (%s)', $user->user_login, implode( ', ', $changes ) )
			);
		}
	}

	public function logUsernameChanged( int $user_id, string $old_username, string $new_username, int $actor_id = 0 ): void {
		if ( empty( $this->settings['log_user_changes'] ) ) {
			return;
		}

		$actor_id = $actor_id > 0 ? $actor_id : ( get_current_user_id() ?: $user_id );
		$target   = get_userdata( $user_id );
		$target_label = $target instanceof \WP_User ? $target->user_login : ( '#' . $user_id );

		$this->logActivity(
			$actor_id,
			'username_changed',
			'user',
			$user_id,
			sprintf(
				'Changed username for %1$s from %2$s to %3$s',
				$target_label,
				sanitize_user( $old_username ),
				sanitize_user( $new_username )
			),
			sanitize_user( $new_username )
		);
	}

	public function logUserRoleChange( int $user_id, string $role, array $old_roles ): void {
		$user = get_userdata( $user_id );
		if ( ! $user ) {
			return;
		}

		$old_role = ! empty( $old_roles ) ? $old_roles[0] : 'none';
		$this->logActivity(
			get_current_user_id() ?: $user_id,
			'user_role_changed',
			'user',
			$user_id,
			sprintf( 'Changed role for %s from %s to %s', $user->user_login, $old_role, $role )
		);
	}

	public function logPasswordReset( \WP_User $user, string $new_pass ): void {
		$this->logActivity(
			$user->ID,
			'password_reset',
			'user',
			$user->ID,
			sprintf( 'Password reset for user: %s', $user->user_login )
		);
	}

	public function logUserDelete( int $user_id, ?int $reassign ): void {
		$user = get_userdata( $user_id );
		if ( $user ) {
			$description = sprintf( 'Deleted user: %s', $user->user_login );
			if ( $reassign ) {
				$reassign_user = get_userdata( $reassign );
				$description .= sprintf( ' (posts reassigned to: %s)', $reassign_user ? $reassign_user->user_login : $reassign );
			}

			$this->logActivity(
				get_current_user_id(),
				'user_deleted',
				'user',
				$user_id,
				$description
			);
		}
	}

	public function logPluginActivate( string $plugin ): void {
		$plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin );
		$plugin_name = $plugin_data['Name'] ?? basename( $plugin );
		$this->logActivity(
			get_current_user_id(),
			'plugin_activated',
			'plugin',
			null,
			sprintf( 'Activated plugin: %s', $plugin_name )
		);
	}

	public function logPluginDeactivate( string $plugin ): void {
		$plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin );
		$plugin_name = $plugin_data['Name'] ?? basename( $plugin );
		$this->logActivity(
			get_current_user_id(),
			'plugin_deactivated',
			'plugin',
			null,
			sprintf( 'Deactivated plugin: %s', $plugin_name )
		);
	}

	public function logPluginDelete( string $plugin_file, bool $deleted ): void {
		if ( ! $deleted ) {
			return;
		}

		$plugin_name = basename( $plugin_file, '.php' );
		$this->logActivity(
			get_current_user_id(),
			'plugin_deleted',
			'plugin',
			null,
			sprintf( 'Deleted plugin: %s', $plugin_name )
		);
	}

	public function logPluginUpdate( \WP_Upgrader $upgrader, array $hook_extra ): void {
		if ( ! isset( $hook_extra['action'] ) || $hook_extra['action'] !== 'update' ) {
			return;
		}

		if ( ! isset( $hook_extra['type'] ) || $hook_extra['type'] !== 'plugin' ) {
			return;
		}

		if ( isset( $hook_extra['plugins'] ) && is_array( $hook_extra['plugins'] ) ) {
			foreach ( $hook_extra['plugins'] as $plugin ) {
				$plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin );
				$plugin_name = $plugin_data['Name'] ?? basename( $plugin );
				$this->logActivity(
					get_current_user_id(),
					'plugin_updated',
					'plugin',
					null,
					sprintf( 'Updated plugin: %s', $plugin_name )
				);
			}
		}
	}

	public function logPluginInstall( \WP_Upgrader $upgrader, array $hook_extra, array $result ): void {
		if ( ! isset( $hook_extra['type'] ) || $hook_extra['type'] !== 'plugin' ) {
			return;
		}

		$plugin_slug = $hook_extra['plugin'] ?? '';
		$plugin_file = $result['destination_name'] ?? $plugin_slug;
		
		if ( file_exists( WP_PLUGIN_DIR . '/' . $plugin_file ) ) {
			$plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin_file );
			$plugin_name = $plugin_data['Name'] ?? $plugin_slug;
		} else {
			$plugin_name = $plugin_slug;
		}

		$this->logActivity(
			get_current_user_id(),
			'plugin_installed',
			'plugin',
			null,
			sprintf( 'Installed plugin: %s', $plugin_name )
		);
	}

	public function logThemeSwitch( string $new_theme ): void {
		$theme = wp_get_theme( $new_theme );
		$this->logActivity(
			get_current_user_id(),
			'theme_switched',
			'theme',
			null,
			sprintf( 'Switched theme to: %s', $theme->get( 'Name' ) )
		);
	}

	/**
	 * @param string $stylesheet Stylesheet slug.
	 * @param bool   $deleted    Whether deletion succeeded.
	 */
	public function logThemeDelete( string $stylesheet, bool $deleted ): void {
		if ( ! $deleted ) {
			return;
		}

		$theme = wp_get_theme( $stylesheet );
		$theme_name = $theme->exists() ? $theme->get( 'Name' ) : $stylesheet;
		$this->logActivity(
			get_current_user_id(),
			'theme_deleted',
			'theme',
			null,
			sprintf( 'Deleted theme: %s', $theme_name )
		);
	}

	public function logThemeUpdate( \WP_Upgrader $upgrader, array $hook_extra ): void {
		if ( ! isset( $hook_extra['action'] ) || $hook_extra['action'] !== 'update' ) {
			return;
		}

		if ( ! isset( $hook_extra['type'] ) || $hook_extra['type'] !== 'theme' ) {
			return;
		}

		if ( isset( $hook_extra['themes'] ) && is_array( $hook_extra['themes'] ) ) {
			foreach ( $hook_extra['themes'] as $theme_slug ) {
				$theme = wp_get_theme( $theme_slug );
				$this->logActivity(
					get_current_user_id(),
					'theme_updated',
					'theme',
					null,
					sprintf( 'Updated theme: %s', $theme->get( 'Name' ) )
				);
			}
		}
	}

	public function logThemeInstall( \WP_Upgrader $upgrader, array $hook_extra, array $result ): void {
		if ( ! isset( $hook_extra['type'] ) || $hook_extra['type'] !== 'theme' ) {
			return;
		}

		$theme_slug = $hook_extra['theme'] ?? '';
		$theme = wp_get_theme( $theme_slug );
		
		$this->logActivity(
			get_current_user_id(),
			'theme_installed',
			'theme',
			null,
			sprintf( 'Installed theme: %s', $theme->get( 'Name' ) ?: $theme_slug )
		);
	}

	public function logCoreUpdate( \WP_Upgrader $upgrader, array $hook_extra ): void {
		if ( ! isset( $hook_extra['action'] ) || $hook_extra['action'] !== 'update' ) {
			return;
		}

		if ( ! isset( $hook_extra['type'] ) || $hook_extra['type'] !== 'core' ) {
			return;
		}

		global $wp_version;
		$this->logActivity(
			get_current_user_id(),
			'core_update_started',
			'core',
			null,
			sprintf( 'Started WordPress core update to version %s', $wp_version )
		);
	}

	public function logCoreUpdated( string $new_version, \WP_Upgrader $upgrader ): void {
		global $wp_version;
		$this->logActivity(
			get_current_user_id(),
			'core_updated',
			'core',
			null,
			sprintf( 'WordPress core updated to version %s', $new_version )
		);
	}

	public function logOptionUpdate( string $option_name, mixed $old_value, mixed $value ): void {
		$skip_patterns = [ '_transient_', '_site_transient_', 'cron', '_wc_session_' ];
		foreach ( $skip_patterns as $pattern ) {
			if ( strpos( $option_name, $pattern ) !== false ) {
				return;
			}
		}

		$important_options = [
			'siteurl',
			'home',
			'admin_email',
			'users_can_register',
			'default_role',
			'blogname',
			'blogdescription',
		];

		if ( ! in_array( $option_name, $important_options, true ) ) {
			return;
		}

		$this->logActivity(
			get_current_user_id(),
			'option_updated',
			'option',
			null,
			sprintf( 'Updated option: %s', $option_name )
		);
	}

	public function logMediaUpload( int $attachment_id ): void {
		$attachment = get_post( $attachment_id );
		if ( $attachment ) {
			$file_name = $attachment->post_title ?: basename( get_attached_file( $attachment_id ) );
			$this->logActivity(
				get_current_user_id(),
				'media_uploaded',
				'attachment',
				$attachment_id,
				sprintf( 'Uploaded media: %s', $file_name )
			);
		}
	}

	public function logMediaDelete( int $attachment_id ): void {
		$attachment = get_post( $attachment_id );
		if ( $attachment ) {
			$this->logActivity(
				get_current_user_id(),
				'media_deleted',
				'attachment',
				$attachment_id,
				sprintf( 'Deleted media: %s', $attachment->post_title ?: 'Media #' . $attachment_id )
			);
		}
	}

	public function logCommentInsert( int $comment_id, \WP_Comment $comment ): void {
		$post = get_post( $comment->comment_post_ID );
		if ( $post && $this->shouldIgnorePostType( $post->post_type ) ) {
			return;
		}
		$post_title = $post ? $post->post_title : 'Post #' . $comment->comment_post_ID;

		$this->logActivity(
			$comment->user_id ?: null,
			'comment_created',
			'comment',
			$comment_id,
			sprintf( 'New comment on %s', $post_title )
		);
	}

	public function logCommentDelete( int $comment_id ): void {
		$comment = get_comment( $comment_id );
		if ( $comment ) {
			$post = get_post( $comment->comment_post_ID );
			if ( $post && $this->shouldIgnorePostType( $post->post_type ) ) {
				return;
			}
			$post_title = $post ? $post->post_title : 'Post #' . $comment->comment_post_ID;
			
			$this->logActivity(
				get_current_user_id(),
				'comment_deleted',
				'comment',
				$comment_id,
				sprintf( 'Deleted comment on %s', $post_title )
			);
		}
	}

	public function logCommentStatusChange( string $new_status, string $old_status, \WP_Comment $comment ): void {
		if ( $new_status === $old_status ) {
			return;
		}

		$post = get_post( $comment->comment_post_ID );
		if ( $post && $this->shouldIgnorePostType( $post->post_type ) ) {
			return;
		}
		$post_title = $post ? $post->post_title : 'Post #' . $comment->comment_post_ID;
		
		$this->logActivity(
			get_current_user_id(),
			'comment_status_changed',
			'comment',
			$comment->comment_ID,
			sprintf( 'Changed comment status from %s to %s on %s', $old_status, $new_status, $post_title )
		);
	}

	public function logWidgetChange( mixed $old_value, mixed $value ): void {
		$this->logActivity(
			get_current_user_id(),
			'widget_updated',
			'widget',
			null,
			'Updated widget configuration'
		);
	}

	public function logMenuUpdate( int $menu_id ): void {
		$menu = wp_get_nav_menu_object( $menu_id );
		if ( $menu ) {
			$this->logActivity(
				get_current_user_id(),
				'menu_updated',
				'menu',
				$menu_id,
				sprintf( 'Updated menu: %s', $menu->name )
			);
		}
	}

	public function logMenuDelete( int $menu_id ): void {
		$this->logActivity(
			get_current_user_id(),
			'menu_deleted',
			'menu',
			$menu_id,
			sprintf( 'Deleted menu: #%d', $menu_id )
		);
	}

	public function logTermCreated( int $term_id, int $tt_id, string $taxonomy ): void {
		$term = get_term( $term_id, $taxonomy );
		if ( $term && ! is_wp_error( $term ) ) {
			$taxonomy_obj = get_taxonomy( $taxonomy );
			$taxonomy_label = $taxonomy_obj ? $taxonomy_obj->labels->singular_name : $taxonomy;
			
			$this->logActivity(
				get_current_user_id(),
				'term_created',
				'taxonomy',
				$term_id,
				sprintf( 'Created %s term: %s', $taxonomy_label, $term->name )
			);
		}
	}

	public function logTermEdited( int $term_id, int $tt_id, string $taxonomy ): void {
		$term = get_term( $term_id, $taxonomy );
		if ( $term && ! is_wp_error( $term ) ) {
			$taxonomy_obj = get_taxonomy( $taxonomy );
			$taxonomy_label = $taxonomy_obj ? $taxonomy_obj->labels->singular_name : $taxonomy;
			
			$this->logActivity(
				get_current_user_id(),
				'term_edited',
				'taxonomy',
				$term_id,
				sprintf( 'Edited %s term: %s', $taxonomy_label, $term->name )
			);
		}
	}

	public function logTermDeleted( int $term_id, int $tt_id, string $taxonomy, \WP_Term $term ): void {
		$taxonomy_obj = get_taxonomy( $taxonomy );
		$taxonomy_label = $taxonomy_obj ? $taxonomy_obj->labels->singular_name : $taxonomy;
		
		$this->logActivity(
			get_current_user_id(),
			'term_deleted',
			'taxonomy',
			$term_id,
			sprintf( 'Deleted %s term: %s', $taxonomy_label, $term->name )
		);
	}

	public function logPermalinkChange( mixed $old_value, mixed $value ): void {
		$this->logActivity(
			get_current_user_id(),
			'permalink_changed',
			'setting',
			null,
			sprintf( 'Changed permalink structure from %s to %s', $old_value ?: 'default', $value )
		);
	}

	/**
	 * Log module enable/disable and drop scheduled cleanup when this module is turned off.
	 *
	 * Note: runs after OptionStore updates. Disabling this module sets isEnabled() to false before this runs;
	 * we still log and unschedule so self-disable is recorded and cron is cleared.
	 */
	public function onPowerSuiteModuleToggled( string $module_id, bool $enabled, array $module_metadata ): void {
		$module_name = $module_metadata['name'] ?? $module_id;
		$this->logActivity(
			get_current_user_id(),
			$enabled ? 'module_enabled' : 'module_disabled',
			'wpps_module',
			null,
			sprintf( '%s WP PowerSuite module: %s', $enabled ? 'Enabled' : 'Disabled', $module_name )
		);

		if ( $module_id === self::MODULE_ID && ! $enabled ) {
			$this->clearScheduledCleanup();
		}

		if ( $module_id === 'login-session-manager' && ! $enabled ) {
			$repo    = OptionStoreModuleSettingsRepository::forModule( self::MODULE_ID );
			$current = $repo->all( [] );
			if ( is_array( $current ) && ! empty( $current['log_session_manager'] ) ) {
				$current['log_session_manager'] = false;
				$repo->save( $current );
			}
		}
	}

	/**
	 * Remove cron hook so it does not run after this module is disabled (module class may not load on that request).
	 */
	private function clearScheduledCleanup(): void {
		$hook = 'wpps_activity_log_cleanup';
		while ( ( $ts = wp_next_scheduled( $hook ) ) ) {
			wp_unschedule_event( $ts, $hook );
		}
	}

	public function addAdminPage(): void {
		$hook = add_submenu_page(
			'powersuite',
			__( 'Activity Log', 'wp-powersuite' ),
			__( 'Activity Log', 'wp-powersuite' ),
			'manage_options',
			'wpps-activity-log',
			[ $this, 'renderAdminPage' ]
		);

		add_action( "load-{$hook}", [ $this, 'onLoadActivityLogScreen' ] );
	}

	/**
	 * Runs only on the Activity Log admin screen: table bootstrap, assets, bulk POST handling.
	 */
	public function onLoadActivityLogScreen(): void {
		$this->maybeCreateLogTable();
		$this->enqueuePageAssets();
		$this->handleBulkActions();
	}

	public function enqueuePageAssets(): void {
		wp_enqueue_style( 'dashicons' );
		wp_enqueue_style( 'list-tables' );

		if ( ! wp_style_is( 'wp-powersuite-admin', 'registered' ) && defined( 'WP_POWERSUITE_PLUGIN_URL' ) ) {
			wp_register_style(
				'wp-powersuite-admin',
				WP_POWERSUITE_PLUGIN_URL . 'src/Admin/Assets/app.css',
				[ 'wp-admin' ],
				defined( 'WP_POWERSUITE_VERSION' ) ? WP_POWERSUITE_VERSION : self::MODULE_VERSION
			);
		}
		if ( wp_style_is( 'wp-powersuite-admin', 'registered' ) ) {
			wp_enqueue_style( 'wp-powersuite-admin' );
		}

		$css_file = __DIR__ . '/assets/css/activity-log.css';
		if ( file_exists( $css_file ) ) {
			if ( defined( 'WP_POWERSUITE_PLUGIN_URL' ) ) {
				$css_url = WP_POWERSUITE_PLUGIN_URL . 'modules/advanced-user-activity-log/assets/css/activity-log.css';
				$js_url  = WP_POWERSUITE_PLUGIN_URL . 'modules/advanced-user-activity-log/assets/js/activity-log-admin.js';
			} else {
				$plugin_dir = dirname( dirname( dirname( __DIR__ ) ) );
				$plugin_file = $plugin_dir . '/wp-powersuite.php';
				$css_url = plugins_url( 'modules/advanced-user-activity-log/assets/css/activity-log.css', $plugin_file );
				$js_url  = plugins_url( 'modules/advanced-user-activity-log/assets/js/activity-log-admin.js', $plugin_file );
			}

			$style_deps = [ 'dashicons', 'list-tables' ];
			if ( wp_style_is( 'wp-powersuite-admin', 'registered' ) ) {
				$style_deps[] = 'wp-powersuite-admin';
			}

			wp_enqueue_style(
				'wpps-activity-log',
				$css_url,
				$style_deps,
				self::MODULE_VERSION
			);

			wp_enqueue_script(
				'wpps-activity-log-admin',
				$js_url,
				[ 'jquery' ],
				self::MODULE_VERSION,
				true
			);

			wp_localize_script(
				'wpps-activity-log-admin',
				'wppsActivityLogAdmin',
				[
					'strings' => [
						'selectBulkAction'  => __( 'Please select a bulk action.', 'wp-powersuite' ),
						'selectAtLeastOne'  => __( 'Please select at least one log entry.', 'wp-powersuite' ),
						'confirmDelete'     => __( 'Are you sure you want to delete the selected logs? This action cannot be undone.', 'wp-powersuite' ),
						'confirmClearAll'   => __( 'Are you sure you want to clear all logs? This action cannot be undone.', 'wp-powersuite' ),
						'clearing'          => __( 'Clearing...', 'wp-powersuite' ),
						'clearAllLogs'      => __( 'Clear all logs', 'wp-powersuite' ),
						'errorClearing'     => __( 'Error clearing logs.', 'wp-powersuite' ),
					],
				]
			);
		}
	}

	public function renderAdminPage(): void {
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'You do not have permission to access this page.', 'wp-powersuite' ) );
		}

		if ( ! $this->isLicenseActive() ) {
			$this->denyUnlicensedRequest();
		}

		$current_page = isset( $_GET['paged'] ) ? max( 1, absint( $_GET['paged'] ) ) : 1;
		// List table page size (default Posts screen uses 20; 50 fits log review).
		$per_page = 50;
		$search = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : '';
		$filter_action = isset( $_GET['filter_action'] ) ? sanitize_key( wp_unslash( $_GET['filter_action'] ) ) : '';
		$filter_user = isset( $_GET['filter_user'] ) ? absint( $_GET['filter_user'] ) : 0;
		$filter_object_type = isset( $_GET['filter_object_type'] ) ? sanitize_key( wp_unslash( $_GET['filter_object_type'] ) ) : '';
		$date_from = isset( $_GET['date_from'] ) ? sanitize_text_field( wp_unslash( $_GET['date_from'] ) ) : '';
		$date_to = isset( $_GET['date_to'] ) ? sanitize_text_field( wp_unslash( $_GET['date_to'] ) ) : '';
		$filter_ip = isset( $_GET['filter_ip'] ) ? sanitize_text_field( wp_unslash( $_GET['filter_ip'] ) ) : '';
		$filter_severity = isset( $_GET['filter_severity'] ) ? sanitize_key( wp_unslash( $_GET['filter_severity'] ) ) : '';
		$filter_role = isset( $_GET['filter_role'] ) ? sanitize_text_field( wp_unslash( $_GET['filter_role'] ) ) : '';

		$list_filters = [
			'search'              => $search,
			'filter_action'       => $filter_action,
			'filter_user'         => $filter_user,
			'filter_object_type'  => $filter_object_type,
			'date_from'           => $date_from,
			'date_to'             => $date_to,
			'filter_ip'           => $filter_ip,
			'filter_severity'     => $filter_severity,
			'filter_role'         => $filter_role,
		];

		$total_logs  = $this->getLogsCount( $list_filters );
		$total_pages = max( 1, (int) ceil( $total_logs / $per_page ) );
		$current_page = min( $current_page, $total_pages );
		$logs         = $this->getLogs( $current_page, $per_page, $list_filters );

		$actions = $this->getUniqueActions();
		$object_types = $this->getUniqueObjectTypes();
		$users = $this->getUsersWithActivity();
		$roles = $this->getUniqueRoles();
		$stats = $this->getDashboardStats();

		$export_base = admin_url( 'admin-ajax.php' );
		$export_query = [
			'action'             => 'wpps_activity_log_export',
			's'                  => $search,
			'filter_action'      => $filter_action,
			'filter_user'        => $filter_user,
			'filter_object_type' => $filter_object_type,
			'date_from'          => $date_from,
			'date_to'            => $date_to,
			'filter_ip'          => $filter_ip,
			'filter_severity'    => $filter_severity,
			'filter_role'        => $filter_role,
		];
		$export_query = array_filter(
			$export_query,
			static function ( $v ) {
				if ( $v === null || $v === false ) {
					return false;
				}
				if ( $v === 0 || $v === '0' ) {
					return false;
				}
				if ( is_string( $v ) && $v === '' ) {
					return false;
				}
				return true;
			}
		);
		$export_url = wp_nonce_url( add_query_arg( $export_query, $export_base ), 'wpps_activity_log_export', 'nonce' );

		include __DIR__ . '/views/admin-page.php';
	}

	private function getLogs( int $page, int $per_page, array $filters ): array {
		global $wpdb;

		$table_name = $wpdb->prefix . self::TABLE_NAME;
		$offset = ( $page - 1 ) * $per_page;

		$built = $this->buildLogsWhere( $filters );
		$query = "SELECT * FROM {$table_name} WHERE {$built['where']} ORDER BY created_at DESC LIMIT %d OFFSET %d";
		$values = array_merge( $built['values'], [ $per_page, $offset ] );

		return $wpdb->get_results( $wpdb->prepare( $query, $values ) );
	}

	private function getLogsCount( array $filters ): int {
		global $wpdb;

		$table_name = $wpdb->prefix . self::TABLE_NAME;
		$built = $this->buildLogsWhere( $filters );
		$query = "SELECT COUNT(*) FROM {$table_name} WHERE {$built['where']}";

		if ( ! empty( $built['values'] ) ) {
			return (int) $wpdb->get_var( $wpdb->prepare( $query, $built['values'] ) );
		}

		return (int) $wpdb->get_var( $query );
	}

	private function getUniqueActions(): array {
		global $wpdb;

		$table_name = $wpdb->prefix . self::TABLE_NAME;
		$results = $wpdb->get_col( "SELECT DISTINCT action FROM {$table_name} ORDER BY action" );

		return $results ?: [];
	}

	private function getUniqueObjectTypes(): array {
		global $wpdb;

		$table_name = $wpdb->prefix . self::TABLE_NAME;
		$results = $wpdb->get_col( "SELECT DISTINCT object_type FROM {$table_name} WHERE object_type IS NOT NULL ORDER BY object_type" );

		return $results ?: [];
	}

	private function getUsersWithActivity(): array {
		global $wpdb;

		$table_name = $wpdb->prefix . self::TABLE_NAME;
		$user_ids = $wpdb->get_col( "SELECT DISTINCT user_id FROM {$table_name} WHERE user_id IS NOT NULL AND user_id > 0" );

		if ( empty( $user_ids ) ) {
			return [];
		}

		$users = [];
		foreach ( $user_ids as $user_id ) {
			$user = get_userdata( $user_id );
			if ( $user ) {
				$users[] = $user;
			}
		}

		return $users;
	}

	private function getUniqueRoles(): array {
		global $wpdb;

		$table_name = $wpdb->prefix . self::TABLE_NAME;
		if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) ) !== $table_name ) {
			return [];
		}

		$results = $wpdb->get_col(
			"SELECT DISTINCT user_role FROM {$table_name} WHERE user_role IS NOT NULL AND user_role != '' ORDER BY user_role"
		);

		return $results ?: [];
	}

	public function cleanupOldLogs(): void {
		if ( ! $this->isLicenseActive() ) {
			$this->clearScheduledCleanup();
			return;
		}

		global $wpdb;

		$this->settings = $this->getSettings();
		$retention_days = $this->clampRetentionDays( $this->settings['retention_days'] ?? 90 );
		$table_name = $wpdb->prefix . self::TABLE_NAME;

		do {
			$deleted = $wpdb->query(
				$wpdb->prepare(
					"DELETE FROM {$table_name} WHERE created_at < DATE_SUB(NOW(), INTERVAL %d DAY) LIMIT %d",
					$retention_days,
					self::CLEANUP_BATCH_SIZE
				)
			);
		} while ( $deleted !== false && $deleted >= self::CLEANUP_BATCH_SIZE );
	}

	public function handleClearLogs(): void {
		check_ajax_referer( 'wpps_activity_log_clear', 'nonce' );

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( [ 'message' => __( 'Permission denied.', 'wp-powersuite' ) ] );
		}

		if ( ! $this->isLicenseActive() ) {
			$this->denyUnlicensedRequest( true );
		}

		global $wpdb;
		$table_name = $wpdb->prefix . self::TABLE_NAME;
		$wpdb->query( "TRUNCATE TABLE {$table_name}" );

		wp_send_json_success( [ 'message' => __( 'All logs cleared successfully.', 'wp-powersuite' ) ] );
	}

	public function handleBulkActions(): void {
		if ( ! isset( $_REQUEST['page'] ) || $_REQUEST['page'] !== 'wpps-activity-log' ) {
			return;
		}

		if ( ! current_user_can( 'manage_options' ) ) {
			return;
		}

		if ( ! $this->isLicenseActive() ) {
			$this->denyUnlicensedRequest();
		}

		if ( $_SERVER['REQUEST_METHOD'] !== 'POST' ) {
			return;
		}

		$action = isset( $_POST['action'] ) && $_POST['action'] !== '-1' ? sanitize_text_field( wp_unslash( $_POST['action'] ) ) : '';
		$action2 = isset( $_POST['action2'] ) && $_POST['action2'] !== '-1' ? sanitize_text_field( wp_unslash( $_POST['action2'] ) ) : '';
		$selected_action = $action ?: $action2;

		if ( ! $selected_action || $selected_action === '-1' ) {
			return;
		}

		if ( ! isset( $_POST['log'] ) || ! is_array( $_POST['log'] ) || empty( $_POST['log'] ) ) {
			return;
		}

		if ( ! check_admin_referer( 'bulk-logs' ) ) {
			wp_die( esc_html__( 'Security check failed. Please try again.', 'wp-powersuite' ) );
		}

		$log_ids = array_map( 'absint', wp_unslash( $_POST['log'] ) );

		if ( empty( $log_ids ) ) {
			return;
		}

		if ( $selected_action === 'delete' ) {
			global $wpdb;
			$table_name = $wpdb->prefix . self::TABLE_NAME;
			$placeholders = implode( ',', array_fill( 0, count( $log_ids ), '%d' ) );
			
			$result = $wpdb->query(
				$wpdb->prepare(
					"DELETE FROM {$table_name} WHERE id IN ($placeholders)",
					...$log_ids
				)
			);

			if ( $result === false ) {
				wp_die( esc_html__( 'Error deleting logs.', 'wp-powersuite' ) );
			}

			$redirect_url = admin_url( 'admin.php' );
			$redirect_url = add_query_arg( 'page', 'wpps-activity-log', $redirect_url );
			
			if ( ! empty( $_POST['s'] ) ) {
				$redirect_url = add_query_arg( 's', sanitize_text_field( wp_unslash( $_POST['s'] ) ), $redirect_url );
			}
			if ( ! empty( $_POST['filter_action'] ) ) {
				$redirect_url = add_query_arg( 'filter_action', sanitize_key( wp_unslash( $_POST['filter_action'] ) ), $redirect_url );
			}
			if ( ! empty( $_POST['filter_user'] ) ) {
				$redirect_url = add_query_arg( 'filter_user', absint( $_POST['filter_user'] ), $redirect_url );
			}
			if ( ! empty( $_POST['filter_object_type'] ) ) {
				$redirect_url = add_query_arg( 'filter_object_type', sanitize_key( wp_unslash( $_POST['filter_object_type'] ) ), $redirect_url );
			}
			if ( ! empty( $_POST['date_from'] ) ) {
				$redirect_url = add_query_arg( 'date_from', sanitize_text_field( wp_unslash( $_POST['date_from'] ) ), $redirect_url );
			}
			if ( ! empty( $_POST['date_to'] ) ) {
				$redirect_url = add_query_arg( 'date_to', sanitize_text_field( wp_unslash( $_POST['date_to'] ) ), $redirect_url );
			}
			if ( ! empty( $_POST['filter_ip'] ) ) {
				$redirect_url = add_query_arg( 'filter_ip', sanitize_text_field( wp_unslash( $_POST['filter_ip'] ) ), $redirect_url );
			}
			if ( ! empty( $_POST['filter_severity'] ) ) {
				$redirect_url = add_query_arg( 'filter_severity', sanitize_key( wp_unslash( $_POST['filter_severity'] ) ), $redirect_url );
			}
			if ( ! empty( $_POST['filter_role'] ) ) {
				$redirect_url = add_query_arg( 'filter_role', sanitize_text_field( wp_unslash( $_POST['filter_role'] ) ), $redirect_url );
			}
			if ( ! empty( $_POST['paged'] ) ) {
				$redirect_url = add_query_arg( 'paged', absint( $_POST['paged'] ), $redirect_url );
			}

			$redirect_url = add_query_arg( 'deleted', count( $log_ids ), $redirect_url );

			wp_safe_redirect( $redirect_url );
			exit;
		}
	}

	public function handleExportLogs(): void {
		if ( ! isset( $_GET['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['nonce'] ) ), 'wpps_activity_log_export' ) ) {
			wp_die( esc_html__( 'Security check failed.', 'wp-powersuite' ) );
		}

		if ( ! current_user_can( 'manage_options' ) ) {
			wp_die( esc_html__( 'Permission denied.', 'wp-powersuite' ) );
		}

		if ( ! $this->isLicenseActive() ) {
			$this->denyUnlicensedRequest();
		}

		$filters = [
			'search'             => isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : '',
			'filter_action'      => isset( $_GET['filter_action'] ) ? sanitize_key( wp_unslash( $_GET['filter_action'] ) ) : '',
			'filter_user'        => isset( $_GET['filter_user'] ) ? absint( $_GET['filter_user'] ) : 0,
			'filter_object_type' => isset( $_GET['filter_object_type'] ) ? sanitize_key( wp_unslash( $_GET['filter_object_type'] ) ) : '',
			'date_from'          => isset( $_GET['date_from'] ) ? sanitize_text_field( wp_unslash( $_GET['date_from'] ) ) : '',
			'date_to'            => isset( $_GET['date_to'] ) ? sanitize_text_field( wp_unslash( $_GET['date_to'] ) ) : '',
			'filter_ip'          => isset( $_GET['filter_ip'] ) ? sanitize_text_field( wp_unslash( $_GET['filter_ip'] ) ) : '',
			'filter_severity'    => isset( $_GET['filter_severity'] ) ? sanitize_key( wp_unslash( $_GET['filter_severity'] ) ) : '',
			'filter_role'        => isset( $_GET['filter_role'] ) ? sanitize_text_field( wp_unslash( $_GET['filter_role'] ) ) : '',
		];

		global $wpdb;
		$table_name = $wpdb->prefix . self::TABLE_NAME;
		$built  = $this->buildLogsWhere( $filters );
		$count_query = "SELECT COUNT(*) FROM {$table_name} WHERE {$built['where']}";
		$total_matching = ! empty( $built['values'] )
			? (int) $wpdb->get_var( $wpdb->prepare( $count_query, $built['values'] ) )
			: (int) $wpdb->get_var( $count_query );

		$query  = "SELECT * FROM {$table_name} WHERE {$built['where']} ORDER BY created_at DESC LIMIT %d";
		$values = array_merge( $built['values'], [ self::EXPORT_MAX_ROWS ] );
		$logs   = $wpdb->get_results( $wpdb->prepare( $query, $values ), ARRAY_A );
		$truncated = $total_matching > count( $logs );

		nocache_headers();
		header( 'Content-Type: text/csv; charset=utf-8' );
		header( 'Content-Disposition: attachment; filename="activity-log-export-' . gmdate( 'Y-m-d' ) . '.csv"' );

		$output = fopen( 'php://output', 'w' );
		fprintf( $output, chr( 0xEF ) . chr( 0xBB ) . chr( 0xBF ) );

		$headers = [
			'ID',
			'User',
			'Role',
			'Action',
			'Object Type',
			'Object ID',
			'Object Name',
			'Description',
			'IP Address',
			'Country',
			'Country Code',
			'Device Type',
			'Browser',
			'OS',
			'User Agent',
			'Date',
		];
		fputcsv( $output, array_map( [ $this, 'sanitizeCsvValue' ], $headers ) );

		foreach ( $logs as $log ) {
			$row = [
				$log['id'] ?? '',
				$log['user_login'] ?? '',
				$log['user_role'] ?? '',
				$log['action'] ?? '',
				$log['object_type'] ?? '',
				$log['object_id'] ?? '',
				$log['object_name'] ?? '',
				$log['description'] ?? '',
				$log['ip_address'] ?? '',
				$log['country'] ?? '',
				$log['country_code'] ?? '',
				$log['device_type'] ?? '',
				$log['browser'] ?? '',
				$log['os'] ?? '',
				$log['user_agent'] ?? '',
				$log['created_at'] ?? '',
			];
			fputcsv( $output, array_map( [ $this, 'sanitizeCsvValue' ], $row ) );
		}

		if ( $truncated ) {
			fputcsv(
				$output,
				array_map(
					[ $this, 'sanitizeCsvValue' ],
					[
						'',
						'',
						'',
						'',
						'',
						'',
						'',
						sprintf(
							/* translators: 1: exported row count, 2: total matching rows */
							__( 'Export truncated at %1$d of %2$d matching rows.', 'wp-powersuite' ),
							count( $logs ),
							$total_matching
						),
						'',
						'',
						'',
						'',
						'',
						'',
						'',
						'',
					]
				)
			);
		}

		fclose( $output );
		exit;
	}

	/**
	 * @param array<string, mixed> $exporters
	 * @return array<string, mixed>
	 */
	public function registerPrivacyExporter( array $exporters ): array {
		$exporters['wp-powersuite-activity-log'] = [
			'exporter_friendly_name' => __( 'WP PowerSuite Activity Log', 'wp-powersuite' ),
			'callback'               => [ $this, 'exportPrivacyData' ],
		];
		return $exporters;
	}

	/**
	 * @param array<string, mixed> $erasers
	 * @return array<string, mixed>
	 */
	public function registerPrivacyEraser( array $erasers ): array {
		$erasers['wp-powersuite-activity-log'] = [
			'eraser_friendly_name' => __( 'WP PowerSuite Activity Log', 'wp-powersuite' ),
			'callback'             => [ $this, 'erasePrivacyData' ],
		];
		return $erasers;
	}

	/**
	 * @param string $email_address User email.
	 * @param int    $page          Export page.
	 * @return array<string, mixed>
	 */
	public function exportPrivacyData( string $email_address, int $page ): array {
		if ( ! $this->isLicenseActive() ) {
			return [
				'data' => [],
				'done' => true,
			];
		}

		global $wpdb;

		$user = get_user_by( 'email', $email_address );
		if ( ! $user ) {
			return [
				'data' => [],
				'done' => true,
			];
		}

		$table_name = $wpdb->prefix . self::TABLE_NAME;
		if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) ) !== $table_name ) {
			return [
				'data' => [],
				'done' => true,
			];
		}

		$per_page = 100;
		$offset   = max( 0, ( $page - 1 ) * $per_page );
		$like     = '%' . $wpdb->esc_like( $user->user_login ) . '%';

		$rows = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT action, description, ip_address, created_at FROM {$table_name}
				WHERE user_id = %d OR user_login = %s OR description LIKE %s
				ORDER BY created_at DESC LIMIT %d OFFSET %d",
				$user->ID,
				$user->user_login,
				$like,
				$per_page,
				$offset
			),
			ARRAY_A
		);

		$data = [];
		foreach ( $rows ?: [] as $row ) {
			$data[] = [
				'group_id'    => 'wpps-activity-log',
				'group_label' => __( 'Activity Log', 'wp-powersuite' ),
				'item_id'     => 'activity-log-' . md5( (string) ( $row['created_at'] ?? '' ) . (string) ( $row['action'] ?? '' ) ),
				'data'        => [
					[
						'name'  => __( 'Action', 'wp-powersuite' ),
						'value' => (string) ( $row['action'] ?? '' ),
					],
					[
						'name'  => __( 'Description', 'wp-powersuite' ),
						'value' => (string) ( $row['description'] ?? '' ),
					],
					[
						'name'  => __( 'IP Address', 'wp-powersuite' ),
						'value' => (string) ( $row['ip_address'] ?? '' ),
					],
					[
						'name'  => __( 'Date', 'wp-powersuite' ),
						'value' => (string) ( $row['created_at'] ?? '' ),
					],
				],
			];
		}

		return [
			'data' => $data,
			'done' => count( $rows ?: [] ) < $per_page,
		];
	}

	/**
	 * @param string $email_address User email.
	 * @param int    $page          Eraser page.
	 * @return array<string, mixed>
	 */
	public function erasePrivacyData( string $email_address, int $page ): array {
		if ( ! $this->isLicenseActive() ) {
			return [
				'items_removed'  => 0,
				'items_retained' => 0,
				'messages'       => [],
				'done'           => true,
			];
		}

		global $wpdb;

		if ( $page > 1 ) {
			return [
				'items_removed'  => 0,
				'items_retained' => 0,
				'messages'       => [],
				'done'           => true,
			];
		}

		$user = get_user_by( 'email', $email_address );
		if ( ! $user ) {
			return [
				'items_removed'  => 0,
				'items_retained' => 0,
				'messages'       => [],
				'done'           => true,
			];
		}

		$table_name = $wpdb->prefix . self::TABLE_NAME;
		if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) ) !== $table_name ) {
			return [
				'items_removed'  => 0,
				'items_retained' => 0,
				'messages'       => [],
				'done'           => true,
			];
		}

		$like = '%' . $wpdb->esc_like( $user->user_login ) . '%';
		$removed = (int) $wpdb->query(
			$wpdb->prepare(
				"DELETE FROM {$table_name} WHERE user_id = %d OR user_login = %s OR description LIKE %s",
				$user->ID,
				$user->user_login,
				$like
			)
		);

		return [
			'items_removed'  => max( 0, $removed ),
			'items_retained' => 0,
			'messages'       => [ __( 'Activity log entries for this user were removed.', 'wp-powersuite' ) ],
			'done'           => true,
		];
	}

}
