<?php

declare(strict_types=1);

namespace SiteFort\Includes;

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

/**
 * Shared protection helpers for the plugin-owned runtime storage directory.
 */
class DataDirectoryProtection {

	private const DATA_DIR_NAME = 'sitefort-data';
	private const RUNTIME_ROOT_MODE = 0755;
	private const RUNTIME_FILE_MODE = 0644;
	private const HEALTH_OPTION = 'sitefort_runtime_storage_health';
	public const SCAN_SKIP_REASON = 'runtime_artifact';
	public const SCAN_ERROR_CODE = 'runtime_artifact_ignored';
	public const SCAN_SKIP_MESSAGE = 'SiteFort runtime data file changed during scan and was ignored';

	public static function get_data_dir_path(): string {
		return WP_CONTENT_DIR . '/' . self::DATA_DIR_NAME;
	}

	public static function get_public_path(): ?string {
		return SitePathResolver::get_content_public_path( self::DATA_DIR_NAME . '/' );
	}

	/** FS_CHMOD_DIR when sane (not world-writable, at least 755), else 755. */
	public static function get_runtime_root_mode(): int {
		if ( defined( 'FS_CHMOD_DIR' ) && is_int( FS_CHMOD_DIR ) ) {
			$mode = FS_CHMOD_DIR & 0777;
			if ( ( $mode & 0002 ) === 0 && ( $mode & self::RUNTIME_ROOT_MODE ) === self::RUNTIME_ROOT_MODE ) {
				return $mode;
			}
		}

		return self::RUNTIME_ROOT_MODE;
	}

	/** FS_CHMOD_FILE when sane, else 644. Never used for secrets. */
	public static function get_runtime_file_mode(): int {
		if ( defined( 'FS_CHMOD_FILE' ) && is_int( FS_CHMOD_FILE ) ) {
			$mode = FS_CHMOD_FILE & 0777;
			if ( ( $mode & 0002 ) === 0 && ( $mode & self::RUNTIME_FILE_MODE ) === self::RUNTIME_FILE_MODE ) {
				return $mode;
			}
		}

		return self::RUNTIME_FILE_MODE;
	}

	public static function ensure_runtime_root(): bool {
		return self::ensure_local_protection( self::get_data_dir_path() );
	}

	/** Ensure a directory exists and stays usable; cheap when already healthy. */
	public static function ensure_runtime_directory( string $dir ): bool {
		if ( ! self::is_runtime_path( $dir ) ) {
			return FilesystemMutation::ensure_directory( $dir );
		}

		if ( is_dir( $dir ) ) {
			$mode = self::directory_mode( $dir );
			if ( is_int( $mode ) && self::runtime_mode_is_usable( $mode, $dir ) ) {
				return true;
			}
		}

		return self::ensure_local_protection( $dir );
	}

	/**
	 * Return true when a scanner path points inside the SiteFort data directory.
	 */
	public static function is_data_dir_file_path( string $file_path ): bool {
		$candidate = trim( wp_normalize_path( $file_path ) );
		if ( '' === $candidate || preg_match( '#(?:^|/)\.\.(?:/|$)#', $candidate ) ) {
			return false;
		}

		$data_dir = rtrim( wp_normalize_path( self::get_data_dir_path() ), '/' );
		if ( self::path_matches_prefix( rtrim( $candidate, '/' ), $data_dir ) ) {
			return true;
		}

		if ( ! defined( 'ABSPATH' ) ) {
			return false;
		}

		$abspath = trailingslashit( wp_normalize_path( ABSPATH ) );
		if ( 0 !== strpos( $data_dir . '/', $abspath ) ) {
			return false;
		}

		$relative_data_dir  = ltrim( substr( $data_dir, strlen( $abspath ) ), '/' );
		$relative_candidate = ltrim( $candidate, '/' );
		return self::path_matches_prefix( $relative_candidate, $relative_data_dir );
	}

	/**
	 * True for SiteFort's own runtime artifacts, which the scanner may skip.
	 * Exact names are writer-verified or self-healed; pattern names must also
	 * start with an inert guard so a planted look-alike still gets scanned.
	 */
	public static function is_runtime_artifact_path( string $file_path ): bool {
		if ( ! self::is_data_dir_file_path( $file_path ) ) {
			return false;
		}

		$basename = strtolower( basename( rtrim( wp_normalize_path( $file_path ), '/' ) ) );
		if ( '' === $basename ) {
			return false;
		}

		$exact_names = [
			'index.php',
			'.htaccess',
			'bootstrap.php',
			'sitefort-key.php',
			'waf-config.php',
			'waf-blocklist.php',
			'waf-blocks.php',
			'waf-verify-nonces.php',
			'rate-limit-db-fallback.php',
			'waf-dns-breaker.php',
			'.waf-mutation.lock.php',
			'geolite2-country.mmdb.php',
			'geolite2-country.mmdb',
			'ranges.json.php',
		];
		if ( in_array( $basename, $exact_names, true ) ) {
			return true;
		}

		// Hex-suffixed atomic-write temps; skipped so a mid-write key file is never uploaded.
		if ( preg_match( '#\.tmp\.[0-9a-f]{8,32}$#', $basename ) ) {
			return true;
		}

		// Non-executable lock files from the pre-WP search-trust cache.
		if ( preg_match( '#^lock-shard-[0-9]{1,3}\.lock$#', $basename ) ) {
			return true;
		}

		$guarded_patterns = [
			'#\.log(?:\.[0-9]{1,2})?\.php$#',
			'#\.qbin\.php$#',
			'#^shard-[0-9]{1,3}\.php$#',
			'#^waf-blocks\.[0-9]{8}-[0-9]{6}\.php$#',
		];
		foreach ( $guarded_patterns as $pattern ) {
			if ( preg_match( $pattern, $basename ) ) {
				return self::has_inert_execution_guard( $file_path );
			}
		}

		return false;
	}

	/** A file whose first statement is a bare exit cannot run anything after it. */
	private static function has_inert_execution_guard( string $file_path ): bool {
		$absolute = $file_path;
		if ( ! is_file( $absolute ) && defined( 'ABSPATH' ) ) {
			$absolute = trailingslashit( wp_normalize_path( ABSPATH ) ) . ltrim( wp_normalize_path( $file_path ), '/' );
		}

		if ( ! is_file( $absolute ) ) {
			return false;
		}

		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Reads only the guard-length prefix during scan classification.
		$prefix = @file_get_contents( $absolute, false, null, 0, GuardedDataFile::header_length() );
		if ( ! is_string( $prefix ) ) {
			return false;
		}

		return GuardedDataFile::has_guard_prefix( $prefix )
			|| 0 === strncmp( $prefix, "<?php exit; ?>\n", 15 );
	}

	public static function get_local_htaccess_contents(): string {
		return implode(
			"\n",
			[
				'<IfModule mod_authz_core.c>',
				'Require all denied',
				'</IfModule>',
				'<IfModule mod_litespeed.c>',
				'Order Deny,Allow',
				'Deny from all',
				'</IfModule>',
				'<IfModule !mod_authz_core.c>',
				'  <IfModule !mod_litespeed.c>',
				'    Order Deny,Allow',
				'    Deny from all',
				'  </IfModule>',
				'</IfModule>',
				'',
			]
		);
	}

	public static function ensure_local_protection( string $dir ): bool {
		if ( ! FilesystemMutation::ensure_directory( $dir ) ) {
			if ( self::is_runtime_path( $dir ) ) {
				self::record_runtime_failure( 'directory_create_failed', $dir );
			}
			return false;
		}

		if ( self::is_runtime_path( $dir ) ) {
			if ( ! self::ensure_usable_runtime_mode( self::get_data_dir_path() ) ) {
				self::record_runtime_failure( 'directory_mode_unusable', self::get_data_dir_path() );
				return false;
			}

			if ( self::normalize_path( $dir ) !== self::normalize_path( self::get_data_dir_path() )
				&& ! self::ensure_usable_runtime_mode( $dir ) ) {
				self::record_runtime_failure( 'directory_mode_unusable', $dir );
				return false;
			}

			self::record_runtime_ok();
		}

		$index_contents = "<?php\n// Silence is golden.\n";
		$index = trailingslashit( $dir ) . 'index.php';
		$current_index = FilesystemMutation::read_file_contents( $index );
		if ( ! is_string( $current_index ) || self::normalize_contents( $current_index ) !== self::normalize_contents( $index_contents ) ) {
			FilesystemMutation::write_bytes_to_file( $index, $index_contents, 0644 );
		}

		if ( self::server_honors_htaccess() ) {
			$htaccess = trailingslashit( $dir ) . '.htaccess';
			$desired_htaccess = self::get_local_htaccess_contents();
			$current_htaccess = FilesystemMutation::read_file_contents( $htaccess );
			if ( ! is_string( $current_htaccess ) || self::normalize_contents( $current_htaccess ) !== self::normalize_contents( $desired_htaccess ) ) {
				FilesystemMutation::write_bytes_to_file( $htaccess, $desired_htaccess, 0644 );
			}
		}

		return true;
	}

	private static function is_runtime_path( string $dir ): bool {
		return self::path_matches_prefix(
			self::normalize_path( $dir ),
			self::normalize_path( self::get_data_dir_path() )
		);
	}

	private static function normalize_path( string $path ): string {
		return rtrim( wp_normalize_path( $path ), '/' );
	}

	private static function ensure_usable_runtime_mode( string $dir ): bool {
		$current = self::directory_mode( $dir );
		if ( is_int( $current ) && self::runtime_mode_is_usable( $current, $dir ) ) {
			return true;
		}

		FilesystemMutation::apply_file_permissions( $dir, self::get_runtime_root_mode() );

		$updated = self::directory_mode( $dir );
		return is_int( $updated ) && self::runtime_mode_is_usable( $updated, $dir );
	}

	/** Not world-writable, at least 755, and accessible to the current process. */
	public static function runtime_mode_is_usable( int $mode, string $dir ): bool {
		return ( $mode & 0002 ) === 0
			&& ( $mode & self::RUNTIME_ROOT_MODE ) === self::RUNTIME_ROOT_MODE
			&& self::can_use_runtime_dir( $dir );
	}

	private static function can_use_runtime_dir( string $dir ): bool {
		return is_dir( $dir )
			&& is_readable( $dir )
			&& is_executable( $dir )
			&& wp_is_writable( $dir );
	}

	/** Writes only on state change so healthy sites never touch the option. */
	public static function record_runtime_failure( string $reason, string $path = '' ): void {
		$health = self::get_runtime_storage_health();
		if ( 'error' === $health['status'] && $health['reason'] === $reason && $health['path'] === $path ) {
			return;
		}

		update_option(
			self::HEALTH_OPTION,
			[
				'status' => 'error',
				'reason' => $reason,
				'path'   => $path,
				'time'   => time(),
			]
		);
	}

	public static function record_runtime_ok(): void {
		if ( 'error' !== self::get_runtime_storage_health()['status'] ) {
			return;
		}

		update_option(
			self::HEALTH_OPTION,
			[
				'status' => 'ok',
				'reason' => '',
				'path'   => '',
				'time'   => time(),
			]
		);
	}

	/**
	 * @return array{status:string,reason:string,path:string,time:int}
	 */
	public static function get_runtime_storage_health(): array {
		$stored = get_option( self::HEALTH_OPTION );
		if ( ! is_array( $stored ) ) {
			return [
				'status' => 'unknown',
				'reason' => '',
				'path'   => '',
				'time'   => 0,
			];
		}

		return [
			'status' => is_string( $stored['status'] ?? null ) ? $stored['status'] : 'unknown',
			'reason' => is_string( $stored['reason'] ?? null ) ? $stored['reason'] : '',
			'path'   => is_string( $stored['path'] ?? null ) ? $stored['path'] : '',
			'time'   => is_int( $stored['time'] ?? null ) ? $stored['time'] : 0,
		];
	}

	private static function directory_mode( string $dir ): ?int {
		clearstatcache( true, $dir );
		$mode = @fileperms( $dir );
		return is_int( $mode ) ? ( $mode & 0777 ) : null;
	}

	private static function path_matches_prefix( string $candidate, string $prefix ): bool {
		$prefix = rtrim( $prefix, '/' );
		if ( '' === $candidate || '' === $prefix ) {
			return false;
		}

		return $candidate === $prefix || 0 === strpos( $candidate, $prefix . '/' );
	}

	private static function server_honors_htaccess(): bool {
		if ( ! class_exists( ServerConfigWriter::class ) ) {
			return true;
		}
		return 'nginx' !== ServerConfigWriter::detect_server();
	}

	private static function normalize_contents( string $contents ): string {
		return preg_replace( "/\R/u", "\n", trim( $contents ) ) ?? trim( $contents );
	}

	public static function get_htaccess_rules(): string {
		$public_path = self::get_public_path();
		if ( ! is_string( $public_path ) || $public_path === '' ) {
			return '';
		}

		$relative = trim( SitePathResolver::get_site_relative_path( $public_path ), '/' );
		$relative = trim( $relative, '/' );
		if ( $relative === '' ) {
			return '';
		}

		$relative_regex = preg_quote( $relative, '#' );

		return implode(
			"\n",
			[
				'',
				'# Protect SiteFort runtime storage',
				'<IfModule mod_rewrite.c>',
				'RewriteEngine On',
				"RewriteRule ^{$relative_regex}(?:$|/) - [F,L]",
				'</IfModule>',
				'',
			]
		);
	}

	public static function get_nginx_rules(): string {
		$public_path = self::get_public_path();
		if ( ! is_string( $public_path ) || $public_path === '' ) {
			return '';
		}

		$normalized = SitePathResolver::normalize_request_path( $public_path, true );
		if ( $normalized === '/' ) {
			return '';
		}

		$exact = untrailingslashit( $normalized );

		return implode(
			"\n",
			[
				'',
				'# Protect SiteFort runtime storage',
				"location = {$exact} {",
				'  return 403;',
				'}',
				"location ^~ {$normalized} {",
				'  return 403;',
				'}',
				'',
			]
		);
	}
}
