<?php
/**
 * File integrity monitoring for Security Pack.
 *
 * @package ArkHost_Security_Pack
 */

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

/**
 * File integrity monitoring class.
 */
class ARKSP_File_Integrity {

	/**
	 * Option key for file hashes.
	 *
	 * @var string
	 */
	const HASHES_OPTION = 'arksp_file_hashes';

	/**
	 * Option key for last scan time.
	 *
	 * @var string
	 */
	const LAST_SCAN_OPTION = 'arksp_file_integrity_last_scan';

	/**
	 * Option key for changed files.
	 *
	 * @var string
	 */
	const CHANGES_OPTION = 'arksp_file_changes';

	/**
	 * Constructor.
	 */
	public function __construct() {
		if ( ! ARKSP_Plugin::get_setting( 'file_integrity_enabled', true ) ) {
			return;
		}

		// Schedule daily scan.
		add_action( 'arksp_daily_file_scan', array( $this, 'run_scheduled_scan' ) );

		if ( ! wp_next_scheduled( 'arksp_daily_file_scan' ) ) {
			wp_schedule_event( time(), 'daily', 'arksp_daily_file_scan' );
		}
	}

	/**
	 * Run scheduled scan.
	 */
	public function run_scheduled_scan() {
		$changes = $this->scan_core_files();

		if ( ! empty( $changes['modified'] ) || ! empty( $changes['added'] ) || ! empty( $changes['removed'] ) ) {
			// Store changes.
			update_option( self::CHANGES_OPTION, $changes );

			// Send alert if enabled.
			$this->send_alert( $changes );
		}
	}

	/**
	 * Scan WordPress core files.
	 *
	 * @param bool $update_baseline Whether to update the baseline.
	 * @return array
	 */
	public function scan_core_files( $update_baseline = false ) {
		global $wp_version;

		$changes = array(
			'modified' => array(),
			'added'    => array(),
			'removed'  => array(),
		);

		// Get stored hashes.
		$stored_hashes = get_option( self::HASHES_OPTION, array() );

		// Get official checksums from WordPress.org.
		$official_checksums = $this->get_official_checksums( $wp_version );

		// Current file hashes.
		$current_hashes = array();

		// Core directories to scan.
		$core_paths = array(
			ABSPATH . 'wp-admin/',
			ABSPATH . 'wp-includes/',
			ABSPATH . 'index.php',
			ABSPATH . 'wp-activate.php',
			ABSPATH . 'wp-blog-header.php',
			ABSPATH . 'wp-comments-post.php',
			ABSPATH . 'wp-cron.php',
			ABSPATH . 'wp-links-opml.php',
			ABSPATH . 'wp-load.php',
			ABSPATH . 'wp-login.php',
			ABSPATH . 'wp-mail.php',
			ABSPATH . 'wp-settings.php',
			ABSPATH . 'wp-signup.php',
			ABSPATH . 'wp-trackback.php',
			ABSPATH . 'xmlrpc.php',
		);

		// Scan each path.
		foreach ( $core_paths as $path ) {
			if ( is_file( $path ) ) {
				$relative_path = str_replace( ABSPATH, '', $path );
				$hash          = md5_file( $path );

				$current_hashes[ $relative_path ] = $hash;
			} elseif ( is_dir( $path ) ) {
				$files = $this->get_directory_files( $path );

				foreach ( $files as $file ) {
					$relative_path = str_replace( ABSPATH, '', $file );
					$hash          = md5_file( $file );

					$current_hashes[ $relative_path ] = $hash;
				}
			}
		}

		// Compare with official checksums if available.
		if ( ! empty( $official_checksums ) ) {
			foreach ( $current_hashes as $file => $hash ) {
				if ( isset( $official_checksums[ $file ] ) ) {
					if ( $hash !== $official_checksums[ $file ] ) {
						$changes['modified'][] = array(
							'file'     => $file,
							'expected' => $official_checksums[ $file ],
							'actual'   => $hash,
							'source'   => 'official',
						);
					}
				}
			}
		} elseif ( ! empty( $stored_hashes ) ) {
			// Compare with stored baseline.
			foreach ( $current_hashes as $file => $hash ) {
				if ( isset( $stored_hashes[ $file ] ) ) {
					if ( $hash !== $stored_hashes[ $file ] ) {
						$changes['modified'][] = array(
							'file'     => $file,
							'previous' => $stored_hashes[ $file ],
							'current'  => $hash,
							'source'   => 'baseline',
						);
					}
				} else {
					$changes['added'][] = $file;
				}
			}

			// Check for removed files.
			foreach ( $stored_hashes as $file => $hash ) {
				if ( ! isset( $current_hashes[ $file ] ) ) {
					$changes['removed'][] = $file;
				}
			}
		}

		// Update baseline if requested or first scan.
		if ( $update_baseline || empty( $stored_hashes ) ) {
			update_option( self::HASHES_OPTION, $current_hashes );
		}

		// Update last scan time.
		update_option( self::LAST_SCAN_OPTION, time() );

		return $changes;
	}

	/**
	 * Get official checksums from WordPress.org.
	 *
	 * @param string $version WordPress version.
	 * @return array
	 */
	private function get_official_checksums( $version ) {
		$locale = get_locale();

		// Try to get from cache.
		$cache_key  = 'arksp_checksums_' . md5( $version . $locale );
		$cached     = get_transient( $cache_key );

		if ( false !== $cached ) {
			return $cached;
		}

		// Fetch from WordPress.org.
		$url = sprintf(
			'https://api.wordpress.org/core/checksums/1.0/?version=%s&locale=%s',
			$version,
			$locale
		);

		$response = wp_remote_get( $url, array( 'timeout' => 30 ) );

		if ( is_wp_error( $response ) ) {
			return array();
		}

		$body = wp_remote_retrieve_body( $response );
		$data = json_decode( $body, true );

		if ( ! isset( $data['checksums'] ) ) {
			return array();
		}

		$checksums = $data['checksums'];

		// Cache for 1 day.
		set_transient( $cache_key, $checksums, DAY_IN_SECONDS );

		return $checksums;
	}

	/**
	 * Get all PHP files in a directory recursively.
	 *
	 * @param string $dir Directory path.
	 * @return array
	 */
	private function get_directory_files( $dir ) {
		$files    = array();
		$iterator = new RecursiveIteratorIterator(
			new RecursiveDirectoryIterator( $dir, RecursiveDirectoryIterator::SKIP_DOTS ),
			RecursiveIteratorIterator::SELF_FIRST
		);

		foreach ( $iterator as $file ) {
			if ( $file->isFile() ) {
				$ext = strtolower( $file->getExtension() );
				// Only track PHP files and critical files.
				if ( in_array( $ext, array( 'php', 'js', 'css' ), true ) ) {
					$files[] = $file->getPathname();
				}
			}
		}

		return $files;
	}

	/**
	 * Send email alert about file changes.
	 *
	 * @param array $changes File changes.
	 */
	private function send_alert( $changes ) {
		if ( ! ARKSP_Plugin::get_setting( 'email_alerts_enabled', false ) ) {
			return;
		}

		$email     = ARKSP_Plugin::get_setting( 'email_alerts_address', get_option( 'admin_email' ) );
		$site_name = get_bloginfo( 'name' );
		$site_url  = home_url();

		$subject = sprintf(
			/* translators: %s: Site name */
			__( '[%s] File Integrity Alert - Core Files Changed', 'arkhost-security-pack' ),
			$site_name
		);

		$message = sprintf(
			/* translators: %s: Site URL */
			__( "ArkHost Security Pack has detected changes to WordPress core files on %s.\n\n", 'arkhost-security-pack' ),
			$site_url
		);

		if ( ! empty( $changes['modified'] ) ) {
			$message .= __( "Modified files:\n", 'arkhost-security-pack' );
			foreach ( $changes['modified'] as $file ) {
				$message .= '- ' . ( is_array( $file ) ? $file['file'] : $file ) . "\n";
			}
			$message .= "\n";
		}

		if ( ! empty( $changes['added'] ) ) {
			$message .= __( "New files detected:\n", 'arkhost-security-pack' );
			foreach ( $changes['added'] as $file ) {
				$message .= '- ' . $file . "\n";
			}
			$message .= "\n";
		}

		if ( ! empty( $changes['removed'] ) ) {
			$message .= __( "Removed files:\n", 'arkhost-security-pack' );
			foreach ( $changes['removed'] as $file ) {
				$message .= '- ' . $file . "\n";
			}
			$message .= "\n";
		}

		$message .= __( "This could indicate:\n", 'arkhost-security-pack' );
		$message .= __( "- A recent WordPress update (normal)\n", 'arkhost-security-pack' );
		$message .= __( "- Unauthorized modifications (investigate)\n", 'arkhost-security-pack' );
		$message .= __( "- Plugin/theme conflicts (rare)\n\n", 'arkhost-security-pack' );
		$message .= __( "Review these changes in your WordPress admin panel.", 'arkhost-security-pack' );

		wp_mail( $email, $subject, $message );
	}

	/**
	 * Get last scan results.
	 *
	 * @return array
	 */
	public function get_last_scan_results() {
		return array(
			'time'    => get_option( self::LAST_SCAN_OPTION, 0 ),
			'changes' => get_option( self::CHANGES_OPTION, array() ),
		);
	}

	/**
	 * Clear file changes.
	 */
	public function clear_changes() {
		delete_option( self::CHANGES_OPTION );
	}

	/**
	 * Reset baseline.
	 */
	public function reset_baseline() {
		delete_option( self::HASHES_OPTION );
		delete_option( self::CHANGES_OPTION );
	}
}
