<?php

/**
 * WPMR_Events_Log
 *
 * Event logging helpers for security/audit telemetry.
 *
 * Methods in this trait are primarily invoked by WordPress core hooks/filters registered
 * in `wpmr.php` (e.g., plugin activation/deactivation, updates, logins) plus a small
 * number of internal calls (e.g., repair/delete/whitelist actions).
 *
 * @package WP_Malware_Removal
 */

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

trait WPMR_Events_Log {

	/**
	 * Persist a structured security/audit event into the events table.
	 *
	 * Stores an event row into `{$wpdb->prefix}wpmr_events` with JSON-encoded details.
	 * If the IP address is a valid IP, attempts to resolve a hostname and stores it
	 * inside the event payload for operator convenience.
	 *
	 * Performance notes:
	 * - `gethostbyaddr()` can block on DNS; the hostname lookup runs only for valid IPs.
	 * - A purge query is executed per call (deletes events older than 100 days).
	 *
	 * Suggested rename: `record_event()` or `write_event_log_row()`.
	 *
	 * @param string              $event_type    Event name (e.g., 'file_repaired', 'login_success').
	 * @param array<string,mixed> $event_details Arbitrary structured event payload (will be JSON-encoded).
	 * @param int|null            $user_id       Optional user id; defaults to current user.
	 * @return void
	 */
	function log_event( $event_type, $event_details, $user_id = null ) {
		global $wpdb;

		$user_id    = $user_id ?: get_current_user_id();
		$ip_address = isset( $event_details['ip'] ) ? $event_details['ip'] : $this->get_remote_ip();

		// Add hostname if IP is valid
		if ( filter_var( $ip_address, FILTER_VALIDATE_IP ) ) {
			$hostname = gethostbyaddr( $ip_address );
			if ( $hostname && $hostname !== $ip_address ) {
				$event_details['hostname'] = $hostname;
			}
		}

		// Serialize event data for storage
		$event_data = wp_json_encode( $event_details );

		// Insert into wpmr_events table
		$table = $wpdb->prefix . 'wpmr_events';
		$wpdb->insert(
			$table,
			array(
				'event_name' => $event_type,
				'event_data' => $event_data,
				'user_id'    => $user_id,
				'ip_address' => $ip_address,
			),
			array( '%s', '%s', '%d', '%s' )
		);

		// Auto-purge events older than 100 days
		$purge_date = gmdate( 'Y-m-d H:i:s', time() - ( 100 * DAY_IN_SECONDS ) );
		$wpdb->query(
			$wpdb->prepare(
				"DELETE FROM $table WHERE created_at < %s",
				$purge_date
			)
		);
	}

	/**
	 * Log completion results from WordPress automatic update runs.
	 *
	 * Hooked to `automatic_updates_complete`.
	 *
	 * Suggested rename: `log_automatic_updates_complete()`.
	 *
	 * @param array<int,array<string,mixed>> $update_results Array of update result structures provided by WordPress.
	 * @return void
	 */
	function log_automatic_update( $update_results ) {
		foreach ( $update_results as $result ) {
			$this->log_event(
				'automatic_update',
				array(
					'type'    => $result['type'] ?? 'unknown',
					'item'    => $result['item'] ?? 'unknown',
					'success' => $result['result'] ?? false,
				)
			);
		}
	}

	/**
	 * Log upgrade events (core/plugin/theme/translation updates) via upgrader hooks.
	 *
	 * Hooked to `upgrader_process_complete` with 2 arguments.
	 * Only logs when `$hook_extra['action'] === 'update'`.
	 *
	 * Note: reads plugin header version data using the internal helper with translation disabled
	 * to avoid WordPress 6.7+ just-in-time textdomain loading notices.
	 *
	 * Suggested rename: `log_upgrader_process_complete()`.
	 *
	 * @param WP_Upgrader $upgrader    Upgrader instance.
	 * @param array       $hook_extra  Upgrader context array (expects at least 'action' and 'type').
	 * @return void
	 */
	function log_update_event( $upgrader, $hook_extra ) {
		// Ensure the action is 'update'
		if ( ! isset( $hook_extra['action'], $hook_extra['type'] ) || $hook_extra['action'] !== 'update' ) {
			return;
		}

		$type = $hook_extra['type'];

		// Check for errors during the update process
		if ( ! empty( $upgrader->skin->errors ) ) {
			foreach ( $upgrader->skin->errors as $error ) {
				$this->log_event(
					$type . '_update_failed',
					array(
						'error'         => $error->get_error_message(),
						'type'          => $type,
						'extra_details' => $hook_extra,
					)
				);
			}
			return; // Exit early since update failed
		}

		// Log Core Update
		if ( $type === 'core' ) {
			$old_version = get_option( 'core_update_previous_version', null );
			$new_version = get_bloginfo( 'version' );
			$this->log_event(
				$type . '_updated',
				array(
					'old_version' => $old_version,
					'new_version' => $new_version,
					'extra'       => $hook_extra,
				)
			);
		}

		// Log Plugin Updates
		elseif ( $type === 'plugin' && isset( $hook_extra['plugins'] ) ) {
			foreach ( $hook_extra['plugins'] as $plugin ) {
				$plugin_path = WP_PLUGIN_DIR . '/' . $plugin;
				if ( file_exists( $plugin_path ) ) {
					$plugin_meta = $this->get_plugin_data( $plugin_path, false, false );
					$old_version = ! empty( $plugin_meta['Version'] ) ? $plugin_meta['Version'] : 'Unknown';
				} else {
					$old_version = 'Unknown';
				}
				$new_version = $upgrader->skin->plugin_info['Version'] ?? 'Unknown';

				$this->log_event(
					$type . '_updated',
					array(
						'plugin'      => $plugin,
						'old_version' => $old_version,
						'new_version' => $new_version,
						'extra'       => $hook_extra,
					)
				);
			}
		}

		// Log Theme Updates
		elseif ( $type === 'theme' && isset( $hook_extra['themes'] ) ) {
			foreach ( $hook_extra['themes'] as $theme_slug ) {
				$theme       = wp_get_theme( $theme_slug );
				$old_version = $theme->get( 'Version' );
				$new_version = $upgrader->result['destination_name'] ?? 'Unknown';

				$this->log_event(
					$type . '_updated',
					array(
						'theme'       => $theme_slug,
						'old_version' => $old_version,
						'new_version' => $new_version,
						'extra'       => $hook_extra,
					)
				);
			}
		}

		// Log Translation Updates
		elseif ( $type === 'translation' ) {
			$this->log_event(
				$type . '_updated',
				array(
					'details' => 'WordPress translations updated successfully.',
					'extra'   => $hook_extra,
				)
			);
		}

		// Log Other Update Types
		else {
			$this->log_event(
				$type . '_updated',
				array(
					'type'    => $type,
					'details' => isset( $hook_extra ) ? json_encode( $hook_extra ) : 'Unknown update details.',
					'extra'   => $hook_extra,
				)
			);
		}
	}

	/**
	 * Log plugin activation/deactivation toggles.
	 *
	 * Hooked to both `activated_plugin` and `deactivated_plugin`.
	 * The event type is inferred from `current_filter()`.
	 *
	 * Suggested rename: `log_plugin_activation_state_change()`.
	 *
	 * @param string $plugin       Plugin path relative to plugins directory.
	 * @param bool   $network_wide Whether the activation/deactivation was network-wide.
	 * @return void
	 */
	function log_plugin_toggle( $plugin, $network_wide ) {
		// Determine the event type based on the current hook
		$event_type = current_filter() === 'activated_plugin' ? 'plugin_activated' : 'plugin_deactivated';

		// Log the event
		$this->log_event(
			$event_type,
			array(
				'plugin'       => $plugin,
				'network_wide' => $network_wide,
			)
		);
	}

	/**
	 * Log theme activation.
	 *
	 * Hooked to `switch_theme`.
	 * Note: WordPress passes multiple parameters to `switch_theme`; this method only
	 * receives the first parameter (typically the new theme name).
	 *
	 * Suggested rename: `log_theme_switched()`.
	 *
	 * @param string $new_theme The new theme name (first argument from `switch_theme`).
	 * @return void
	 */
	function log_theme_activation( $new_theme ) {
		$this->log_event(
			'theme_activated',
			array(
				'theme' => $new_theme,
				'user'  => get_current_user_id(),
			)
		);
	}

	/**
	 * Log plugin deletion.
	 *
	 * Hooked to `delete_plugin`.
	 *
	 * Suggested rename: `log_plugin_deleted()`.
	 *
	 * @param string $plugin Plugin path relative to plugins directory.
	 * @return void
	 */
	function log_plugin_deletion( $plugin ) {
		$this->log_event(
			'plugin_deleted',
			array(
				'plugin' => $plugin,
				'user'   => get_current_user_id(),
				'ip'     => $this->get_remote_ip(),
			)
		);
	}

	/**
	 * Log theme deletion.
	 *
	 * Hooked to `deleted_theme`.
	 *
	 * Suggested rename: `log_theme_deleted()`.
	 *
	 * @param string $theme Theme stylesheet slug (first argument from `deleted_theme`).
	 * @return void
	 */
	function log_theme_deletion( $theme ) {
		$this->log_event(
			'theme_deleted',
			array(
				'theme' => $theme,
				'user'  => get_current_user_id(),
				'ip'    => $this->get_remote_ip(),
			)
		);
	}

	/**
	 * Log admin file editor writes.
	 *
	 * Hooked to `edit_file` with 2 arguments.
	 *
	 * Suggested rename: `log_core_file_editor_change()`.
	 *
	 * @param string $file Absolute or relative file path being edited.
	 * @param string $type Context/type for the file editor.
	 * @return void
	 */
	function log_file_edit( $file, $type ) {
		$this->log_event(
			'file_edited',
			array(
				'file' => $file,
				'type' => $type,
				'user' => get_current_user_id(),
			)
		);
	}

	/**
	 * Log file uploads.
	 *
	 * Hooked to the `wp_handle_upload` filter.
	 * Must return the (possibly modified) upload array.
	 *
	 * Suggested rename: `filter_log_file_upload()`.
	 *
	 * @param array{file:string,url:string,type:string} $file Upload data.
	 * @return array{file:string,url:string,type:string} Unmodified upload data.
	 */
	function log_file_upload( $file ) {
		$this->log_event(
			'file_uploaded',
			array(
				'file' => $file['file'],
				'user' => get_current_user_id(),
			)
		);
		return $file;
	}

	/**
	 * Log new media attachment creation.
	 *
	 * Hooked to `add_attachment`.
	 *
	 * Suggested rename: `log_attachment_added()`.
	 *
	 * @param int $post_id Attachment post ID.
	 * @return void
	 */
	function log_add_attachment( $post_id ) {
		$file = get_attached_file( $post_id );
		$this->log_event(
			'attachment_added',
			array(
				'file'    => $file,
				'user_id' => get_current_user_id(),
			)
		);
	}

	/**
	 * Log user account creation.
	 *
	 * Hooked to `user_register`.
	 *
	 * Suggested rename: `log_user_registered()`.
	 *
	 * @param int $user_id Newly created user id.
	 * @return void
	 */
	function log_user_creation( $user_id ) {
		$user_info = get_userdata( $user_id );
		$this->log_event(
			'user_created',
			array(
				'user_id'  => $user_id,
				'username' => $user_info->user_login,
				'email'    => $user_info->user_email,
			)
		);
	}

	/**
	 * Log user profile updates.
	 *
	 * Hooked to `profile_update` with 2 arguments.
	 *
	 * Suggested rename: `log_user_profile_updated()`.
	 *
	 * @param int     $user_id        Updated user id.
	 * @param WP_User $old_user_data  Previous user object.
	 * @return void
	 */
	function log_user_update( $user_id, $old_user_data ) {
		$user_info = get_userdata( $user_id );
		$this->log_event(
			'user_updated',
			array(
				'user_id'  => $user_id,
				'username' => $user_info->user_login,
				'email'    => $user_info->user_email,
				'changes'  => array_diff_assoc( (array) $user_info->data, (array) $old_user_data->data ),
			)
		);
	}

	/**
	 * Log user role changes.
	 *
	 * Hooked to `set_user_role` with 2 arguments (see `wpmr.php` hook registration).
	 *
	 * Suggested rename: `log_user_role_set()`.
	 *
	 * @param int    $user_id  User id.
	 * @param string $new_role New role slug.
	 * @return void
	 */
	function log_user_role_change( $user_id, $new_role ) {
		$this->log_event(
			'user_role_changed',
			array(
				'user_id'  => $user_id,
				'new_role' => $new_role,
				'user'     => get_current_user_id(),
			)
		);
	}

	/**
	 * Log attempts to access the password reset form.
	 *
	 * Hooked to `login_form_resetpass`.
	 * Captures the submitted username (when present) and request IP.
	 *
	 * Suggested rename: `log_password_reset_form_visit()`.
	 *
	 * @return void
	 */
	function log_password_reset_attempt() {
		$username = isset( $_POST['user_login'] ) ? sanitize_text_field( wp_unslash( $_POST['user_login'] ) ) : 'unknown'; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- This is a logging function that captures reset attempts, not processing form data
		$this->log_event(
			'password_reset_attempt',
			array(
				'username' => $username,
				'ip'       => $this->get_remote_ip(),
			)
		);
	}

	/**
	 * Log user deletion.
	 *
	 * Hooked to `delete_user` (receives the first argument: user id).
	 *
	 * Suggested rename: `log_user_deleted()`.
	 *
	 * @param int $user_id Deleted user id.
	 * @return void
	 */
	function log_user_deletion( $user_id ) {
		$user_info = get_userdata( $user_id );
		$this->log_event(
			'user_deleted',
			array(
				'user_id'  => $user_id,
				'username' => $user_info ? $user_info->user_login : 'unknown',
				'email'    => $user_info ? $user_info->user_email : 'unknown',
			)
		);
	}

	/**
	 * Log when a user is added to a multisite blog.
	 *
	 * Hooked to `add_user_to_blog` with 3 arguments.
	 * WordPress passes arguments as: (user_id, role, blog_id).
	 *
	 * Suggested rename: `log_user_added_to_site()`.
	 *
	 * @param int    $blog_id Blog id.
	 * @param int    $user_id User id.
	 * @param string $role    Assigned role slug.
	 * @return void
	 */
	function log_add_user_to_blog( $blog_id, $user_id, $role ) {
		$this->log_event(
			'user_added_to_blog',
			array(
				'user_id' => $user_id,
				'blog_id' => $blog_id,
				'role'    => $role,
			)
		);
	}

	/**
	 * Log failed login attempts.
	 *
	 * Hooked to `wp_login_failed`.
	 * Captures IP and a best-effort URL reconstructed from `$_SERVER`.
	 *
	 * Suggested rename: `log_login_failed()`.
	 *
	 * @param string $username Submitted username.
	 * @return void
	 */
	function log_failed_login( $username ) {
		// Determine the protocol
		$protocol = 'http';
		if ( isset( $_SERVER['HTTPS'] ) && $_SERVER['HTTPS'] === 'on' ) {
			$protocol = 'https';
		}

		// Retrieve the host
		$host = isset( $_SERVER['HTTP_HOST'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : 'unknown_host'; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- HTTP_HOST is sanitized below

		// Retrieve the request URI
		$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : 'unknown_uri'; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- REQUEST_URI is sanitized below

		// Construct the full URL
		$full_url = $protocol . '://' . $host . $request_uri;

		// Log the failed login attempt with the full URL
		$event_details = array(
			'username' => $username,
			'ip'       => $this->get_remote_ip(),
			'url'      => $full_url,
		);

		// Assuming you have a function to log events
		$this->log_event( 'failed_login', $event_details );
	}

	/**
	 * Log when a password reset is requested.
	 *
	 * Hooked to `retrieve_password`.
	 *
	 * Suggested rename: `log_password_reset_requested()`.
	 *
	 * @param string $user_login Submitted username/email.
	 * @return void
	 */
	function log_password_reset_request( $user_login ) {
		$this->log_event(
			'password_reset_requested',
			array(
				'username' => $user_login,
				'ip'       => $this->get_remote_ip(),
			)
		);
	}

	/**
	 * Log successful logins.
	 *
	 * Hooked to `wp_login` with 2 arguments.
	 *
	 * Suggested rename: `log_login_success()`.
	 *
	 * @param string  $user_login Username.
	 * @param WP_User $user       User object.
	 * @return void
	 */
	function log_successful_login( $user_login, $user ) {
		$this->log_event(
			'login_success',
			array(
				'username' => $user_login,
				'user_id'  => $user->ID,
				'ip'       => $this->get_remote_ip(),
			)
		);
	}

	/**
	 * Log XML-RPC post publishing.
	 *
	 * Hooked to `xmlrpc_publish_post`.
	 *
	 * Suggested rename: `log_xmlrpc_post_published()`.
	 *
	 * @param int $post_id Published post id.
	 * @return void
	 */
	function log_xmlrpc_publish_post( $post_id ) {
		$post = get_post( $post_id );
		$this->log_event(
			'xmlrpc_post_published',
			array(
				'post_id'    => $post_id,
				'post_title' => $post->post_title,
				'user_id'    => $post->post_author,
				'ip'         => $this->get_remote_ip(),
			)
		);
	}

	/**
	 * Log the start of a malware scan.
	 *
	 * Hooked to the custom action `wpmr_scan_init` (see scanner init).
	 *
	 * Suggested rename: `log_scan_initiated()`.
	 *
	 * @param array<string,mixed> $settings Scan state/settings payload.
	 * @return void
	 */
	function log_malware_scan_start( $settings ) {
		$this->log_event(
			'malcure_scan_initiated',
			array(
				'settings' => $settings,
				'ip'       => $this->get_remote_ip(),
			)
		);
	}

	/**
	 * Log the completion of a malware scan.
	 *
	 * Call sites:
	 * - Triggered via the generic `wpmr_ajax_request` mechanism from the admin UI
	 *   (see `traits/wpmr_client_js.php` -> `log_scan_completed()`), not from a WP action.
	 *
	 * Suggested rename: `log_scan_completed()`.
	 *
	 * @return void
	 */
	function log_malware_scan_complete() {
		$this->log_event(
			'malcure_scan_completed',
			array(
				'ip' => $this->get_remote_ip(),
			)
		);
	}

	/**
	 * Convert raw event detail payloads into a display-friendly string.
	 *
	 * Used by the admin UI event log meta box when rendering the `event_data` JSON.
	 *
	 * Suggested rename: `stringify_event_details()`.
	 *
	 * @param mixed $event_details Event details payload.
	 * @return string Formatted string (pretty JSON for arrays/objects, otherwise the original scalar).
	 */
	function format_event_details( $event_details ) {
		// Handle array or object details
		if ( is_array( $event_details ) || is_object( $event_details ) ) {
			return wp_json_encode( $event_details, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES );
		}

		// Handle string or other scalar values
		return $event_details;
	}

	/**
	 * Resolve a user id to a displayable name for event log tables.
	 *
	 * Used by the admin UI event log meta box.
	 *
	 * Suggested rename: `get_event_user_label()`.
	 *
	 * @param int|null $user_id User id (null/0 treated as system).
	 * @return string Display name, 'SYSTEM' for empty id, or 'Unknown User' when not found.
	 */
	function get_user_display_name( $user_id ) {
		// If no user ID is provided, return "System"
		if ( ! $user_id ) {
			return 'SYSTEM';
		}

		$user = get_userdata( $user_id );
		return $user ? $user->display_name : 'Unknown User';
	}
}
