<?php
/**
 * File Integrity Monitor
 *
 * Maintains SHA-256 hashes of WordPress core, active plugins,
 * and active themes. On each scheduled check it compares the
 * live hashes with the stored baseline and alerts on any change.
 *
 * Baseline is re-generated after a legitimate update via
 * 'upgrader_process_complete' hook so we don't spam false positives.
 *
 * @package GuardianShield
 */

namespace GuardianShield;

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

class FileMonitor {

	/** Extensions considered part of executable code. */
	private const MONITORED_EXTENSIONS = [ 'php', 'js', 'css', 'htm', 'html', 'htaccess', 'ini' ];

	/** Files/dirs to skip even inside monitored paths. */
	private const SKIP_PATHS = [
		'wp-content/uploads',
		'wp-content/cache',
		'wp-content/upgrade',
		'wp-content/guardian-shield-logs',
		'wp-content/guardian-shield-quarantine',
	];

	private Logger $logger;
	private bool   $monitor_core;
	private bool   $monitor_plugins;
	private bool   $monitor_themes;

	public function __construct( Logger $logger ) {
		$this->logger          = $logger;
		$this->monitor_core    = (bool) gs_get_option( 'monitor_core', true );
		$this->monitor_plugins = (bool) gs_get_option( 'monitor_plugins', true );
		$this->monitor_themes  = (bool) gs_get_option( 'monitor_themes', true );

		// Re-baseline after any update so normal updates don't trip alerts.
		add_action( 'upgrader_process_complete', [ $this, 'refresh_baseline_after_update' ], 10, 2 );
	}

	// ── Scheduled Check ──────────────────────────
	public function run_scheduled_check(): void {
		$changes = [];

		if ( $this->monitor_core ) {
			$changes = array_merge( $changes, $this->check_directory( ABSPATH, 'core', true ) );
		}

		if ( $this->monitor_plugins ) {
			// Only check active plugins.
			foreach ( $this->get_active_plugin_dirs() as $dir ) {
				$changes = array_merge( $changes, $this->check_directory( $dir, 'plugin' ) );
			}
		}

		if ( $this->monitor_themes ) {
			foreach ( $this->get_active_theme_dirs() as $dir ) {
				$changes = array_merge( $changes, $this->check_directory( $dir, 'theme' ) );
			}
		}

		if ( ! empty( $changes ) ) {
			do_action( 'gs_file_changes_detected', $changes );

			foreach ( $changes as $change ) {
				$this->logger->log_attack( [
					'ip_address'  => '0.0.0.0',
					'attack_type' => 'file_modified',
					'severity'    => $change['status'] === 'modified' ? 3 : 2,
					'description' => sprintf(
						'File %s: %s',
						$change['status'],
						$change['file_path']
					),
					'request_uri' => $change['file_path'],
					'user_agent'  => 'FileMonitor',
					'blocked'     => 0,
				] );
			}
		}

		update_option( 'gs_last_integrity_check', current_time( 'mysql', true ), false );
	}

	// ── Directory Walker ─────────────────────────
	/**
	 * Compare every monitored file against its stored hash.
	 * Returns array of changed files with status: modified|new|deleted.
	 */
	private function check_directory( string $dir, string $type, bool $skip_wp_content = false ): array {
		global $wpdb;

		if ( ! is_dir( $dir ) ) {
			return [];
		}

		$changes  = [];
		$seen     = [];

		try {
			$iterator = new \RecursiveIteratorIterator(
				new \RecursiveDirectoryIterator( $dir, \RecursiveDirectoryIterator::SKIP_DOTS ),
				\RecursiveIteratorIterator::LEAVES_ONLY
			);
		} catch ( \Exception $e ) {
			return [];
		}

		foreach ( $iterator as $file ) {
			if ( ! $file->isFile() ) {
				continue;
			}

			$path = $file->getRealPath();

			// Skip unmonitored extensions.
			$ext = strtolower( $file->getExtension() );
			if ( ! in_array( $ext, self::MONITORED_EXTENSIONS, true ) ) {
				continue;
			}

			// Skip excluded paths.
			if ( $this->should_skip( $path ) ) {
				continue;
			}

			// Skip wp-content when scanning core.
			if ( $skip_wp_content && str_contains( $path, WP_CONTENT_DIR ) ) {
				continue;
			}

			$seen[ $path ] = true;
			$current_hash  = hash_file( 'sha256', $path );
			$current_size  = (int) $file->getSize();

			// Look up stored hash.
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery
			$stored = $wpdb->get_row(
				$wpdb->prepare(
					"SELECT file_hash, file_size, status FROM `{$wpdb->prefix}gs_file_hashes`
					  WHERE file_path = %s LIMIT 1",
					$path
				)
			);

			if ( ! $stored ) {
				// New file — insert baseline and flag.
				$this->logger->upsert_file_hash( $path, $current_hash, $current_size, $type, 'new' );

				// New files in core are suspicious; in plugin/theme dirs possibly an update artifact.
				if ( $type === 'core' ) {
					$changes[] = [ 'file_path' => $path, 'status' => 'new', 'type' => $type ];
				}
			} elseif ( $stored->file_hash !== $current_hash ) {
				// Hash mismatch — file was modified.
				$this->logger->upsert_file_hash( $path, $current_hash, $current_size, $type, 'modified' );
				$changes[] = [
					'file_path'     => $path,
					'status'        => 'modified',
					'type'          => $type,
					'old_hash'      => $stored->file_hash,
					'new_hash'      => $current_hash,
				];
			} else {
				// File is clean — update last_checked timestamp.
				$this->logger->upsert_file_hash( $path, $current_hash, $current_size, $type, 'clean' );
			}
		}

		// Detect deleted files.
		$table = $wpdb->prefix . 'gs_file_hashes';
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery
		$stored_paths = $wpdb->get_col(
			$wpdb->prepare(
				"SELECT file_path FROM `{$table}` WHERE file_type = %s",
				$type
			)
		);

		foreach ( $stored_paths as $stored_path ) {
			if ( ! isset( $seen[ $stored_path ] ) && str_starts_with( $stored_path, $dir ) ) {
				$wpdb->update(
					$table,
					[ 'status' => 'deleted' ],
					[ 'file_path' => $stored_path ],
					[ '%s' ],
					[ '%s' ]
				);
				if ( $type === 'core' ) {
					$changes[] = [ 'file_path' => $stored_path, 'status' => 'deleted', 'type' => $type ];
				}
			}
		}

		return $changes;
	}

	// ── Baseline Management ──────────────────────
	/**
	 * Called after WP updates. Re-hash updated files so the next check
	 * doesn't report legitimate updates as tampering.
	 */
	public function refresh_baseline_after_update( \WP_Upgrader $upgrader, array $hook_extra ): void {
		$type = $hook_extra['type'] ?? '';
		$action = $hook_extra['action'] ?? '';

		if ( $action !== 'update' ) {
			return;
		}

		if ( $type === 'core' && $this->monitor_core ) {
			$this->build_baseline_for_directory( ABSPATH, 'core', true );
		} elseif ( $type === 'plugin' && $this->monitor_plugins ) {
			foreach ( $this->get_active_plugin_dirs() as $dir ) {
				$this->build_baseline_for_directory( $dir, 'plugin' );
			}
		} elseif ( $type === 'theme' && $this->monitor_themes ) {
			foreach ( $this->get_active_theme_dirs() as $dir ) {
				$this->build_baseline_for_directory( $dir, 'theme' );
			}
		}
	}

	/** Force a full baseline rebuild (e.g., on plugin activation). */
	public function build_baseline_for_directory( string $dir, string $type, bool $skip_wp_content = false ): int {
		if ( ! is_dir( $dir ) ) {
			return 0;
		}

		$count = 0;

		try {
			$iterator = new \RecursiveIteratorIterator(
				new \RecursiveDirectoryIterator( $dir, \RecursiveDirectoryIterator::SKIP_DOTS )
			);
		} catch ( \Exception $e ) {
			return 0;
		}

		foreach ( $iterator as $file ) {
			if ( ! $file->isFile() ) {
				continue;
			}
			$path = $file->getRealPath();
			$ext  = strtolower( $file->getExtension() );

			if ( ! in_array( $ext, self::MONITORED_EXTENSIONS, true ) ) {
				continue;
			}
			if ( $this->should_skip( $path ) ) {
				continue;
			}
			if ( $skip_wp_content && str_contains( $path, WP_CONTENT_DIR ) ) {
				continue;
			}

			$hash = hash_file( 'sha256', $path );
			$size = (int) $file->getSize();
			$this->logger->upsert_file_hash( $path, $hash, $size, $type, 'clean' );
			$count++;
		}

		return $count;
	}

	// ── Helpers ──────────────────────────────────
	private function should_skip( string $path ): bool {
		$norm = str_replace( '\\', '/', $path );
		foreach ( self::SKIP_PATHS as $skip ) {
			if ( str_contains( $norm, $skip ) ) {
				return true;
			}
		}
		return false;
	}

	private function get_active_plugin_dirs(): array {
		$dirs    = [];
		$plugins = get_option( 'active_plugins', [] );
		foreach ( $plugins as $plugin ) {
			$dir = WP_PLUGIN_DIR . '/' . dirname( $plugin );
			if ( is_dir( $dir ) ) {
				$dirs[] = realpath( $dir );
			}
		}
		return array_filter( $dirs );
	}

	private function get_active_theme_dirs(): array {
		$dirs   = [];
		$theme  = wp_get_theme();
		$dirs[] = $theme->get_stylesheet_directory();
		$parent = $theme->parent();
		if ( $parent ) {
			$dirs[] = $parent->get_stylesheet_directory();
		}
		return array_filter( $dirs, 'is_dir' );
	}
}
