<?php

declare(strict_types=1);
/**
 * Loads and stores the SiteFort master key.
 */

namespace SiteFort\Includes;

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

class MasterKey {

	private const KEY_BYTES     = 32;
	private const KEY_FILE_NAME = 'sitefort-key.php';
	private const DATA_DIR_NAME = 'sitefort-data';

	/** @var string|null Cached master key for the current request. */
	private static ?string $cache = null;

	/** @var array|null Last error from get() for downstream diagnostics. */
	private static ?array $last_error = null;

		/**
		 * Load the 32-byte master key or create it on first use.
		 *
		 * Checks SITEFORT_MASTER_KEY first, then the key file.
		 *
		 * @return string Raw 32-byte key.
		 * @throws \RuntimeException If the key cannot be loaded or created.
		 */
	public static function get(): string {
		if ( null !== self::$cache ) {
			return self::$cache;
		}

		self::$last_error = null;

		// Explicit constant (managed hosting, containers, CI).
		$constant_key = self::get_configured_constant_key();
		if ( null !== $constant_key ) {
			$key = base64_decode( $constant_key['value'], true );
			if ( false === $key || strlen( $key ) !== self::KEY_BYTES ) {
				self::$last_error = [
					'code'    => 'key_constant_invalid',
					'message' => $constant_key['name'] . ' is not a valid base64-encoded ' . self::KEY_BYTES . '-byte key.',
				];
				// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception message.
				throw new \RuntimeException( self::$last_error['message'] );
			}
			self::$cache = $key;
			return $key;
		}

		// Key file at wp-content/sitefort-data/sitefort-key.php.
		$path = self::get_key_file_path();
		if ( file_exists( $path ) ) {
			// Do not gate on is_readable() - it can return false on some Apache/PHP-FPM
			// setups (open_basedir, suPHP ownership mismatches) even when the file is
			// accessible. Attempt the read directly via @file_get_contents instead.
			return self::load_from_file( $path );
		}

		// Create the key on first use.
		try {
			$key = self::create_key_with_host_fallback( $path );
		} catch ( \RuntimeException $e ) {
			if ( null === self::$last_error ) {
				self::$last_error = [
					'code'    => 'key_write_failed',
					'path'    => $path,
					'message' => $e->getMessage(),
				];
			}
			throw $e;
		}
		self::$cache = $key;
		return $key;
	}

	/**
	 * Last error set by get() before throwing.
	 *
	 * Returns null when get() succeeded or hasn't been called yet.
	 * Array keys: 'code' (string), 'path' (string|absent), 'message' (string).
	 *
	 * @return array<string, string>|null
	 */
	public static function get_last_error(): ?array {
		return self::$last_error;
	}

	/**
	 * @return array{name:string,value:string}|null
	 */
	private static function get_configured_constant_key(): ?array {
		if ( defined( 'SITEFORT_MASTER_KEY' ) ) {
			return [
				'name'  => 'SITEFORT_MASTER_KEY',
				'value' => (string) constant( 'SITEFORT_MASTER_KEY' ),
			];
		}

		return null;
	}

	/**
	 * Clear the in-memory cache so the next get() reads fresh from disk.
	 */
	public static function clear_cache(): void {
		self::$cache      = null;
		self::$last_error = null;
	}

	/**
	 * Override the in-memory key for the remainder of this request.
	 *
	 * Used by the key rotation tool to switch SecretManager to the new key
	 * for re-encryption before the request ends. The override takes effect
	 * because get() returns the cache before checking the constant or file.
	 *
	 * @param string $key Raw 32-byte key.
	 * @throws \InvalidArgumentException If key length is wrong.
	 */
		public static function set_override( string $key ): void {
			if ( strlen( $key ) !== self::KEY_BYTES ) {
				throw new \InvalidArgumentException( 'Override key must be exactly 32 bytes.' );
			}
			self::$cache = $key;
		}

	/**
	 * Absolute path to the key file.
	 *
	 * @return string
	 */
	public static function get_key_file_path(): string {
		return WP_CONTENT_DIR . '/' . self::DATA_DIR_NAME . '/' . self::KEY_FILE_NAME;
	}

	/**
	 * Key source identifier for diagnostics.
	 *
	 * @return string 'constant' or 'file'.
	 */
	public static function get_source(): string {
		return null !== self::get_configured_constant_key() ? 'constant' : 'file';
	}

	/**
	 * Whether activation can persist its initial key material without prompting
	 * for filesystem credentials.
	 */
	public static function can_complete_activation_without_credentials(): bool {
		if ( null !== self::get_configured_constant_key() || file_exists( self::get_key_file_path() ) ) {
			return true;
		}

		return self::can_directly_write_key_directory()
			|| self::can_directly_write_wp_config();
	}

	/**
	 * Whether the active key store can be rewritten without prompting for
	 * filesystem credentials.
	 */
	public static function can_mutate_key_store_without_credentials(): bool {
		if ( null !== self::get_configured_constant_key() ) {
			return self::can_directly_write_wp_config();
		}

		return self::can_directly_write_key_directory();
	}

	/**
	 * Whether a destructive "generate fresh key" operation can run without
	 * prompting for filesystem credentials.
	 */
	public static function can_generate_fresh_key_without_credentials(): bool {
		if ( null !== self::get_configured_constant_key() ) {
			return self::can_directly_write_wp_config();
		}

		$key_path = self::get_key_file_path();
		if ( file_exists( $key_path ) ) {
			return self::can_directly_write_key_directory();
		}

		return self::can_directly_write_key_directory()
			|| self::can_directly_write_wp_config();
	}

	/**
	 * Delete a path through the shared verified filesystem mutation layer.
	 */
	public static function delete_path( string $path ): bool {
		return FilesystemMutation::delete_path( $path );
	}

	/**
	 * Copy a file through the shared verified filesystem mutation layer.
	 */
	public static function copy_path( string $source, string $destination, ?int $mode = null ): bool {
		return FilesystemMutation::copy_path( $source, $destination, $mode );
	}

	/**
	 * Load the master key from the key file using file_get_contents + regex.
	 *
	 * @param string $path Absolute file path.
	 * @return string Raw 32-byte key.
	 * @throws \RuntimeException If the file is corrupt or unreadable.
	 */
	private static function load_from_file( string $path ): string {
		$content = self::read_file_contents( $path );
		if ( $content !== false && preg_match( "/return\s+'([A-Za-z0-9+\/=]+)'/", $content, $matches ) ) {
			$key = base64_decode( $matches[1], true );
			if ( false !== $key && strlen( $key ) === self::KEY_BYTES ) {
				self::$cache = $key;
				return $key;
			}
		}
		// Two possible causes: the file is not readable by the PHP process (ownership /
		// open_basedir mismatch), or the content is genuinely corrupt.
		$readable = ( $content !== false );
		self::$last_error = [
			'code'    => $readable ? 'key_corrupt' : 'key_unreadable',
			'path'    => $path,
				'message' => $readable
					? 'Key file is corrupt or contains invalid data: ' . $path
					: 'Key file exists but could not be read: ' . $path
					. '. Check file ownership/permissions, or set define( \'SITEFORT_MASTER_KEY\', \'<base64-key>\' ) in wp-config.php.',
		];
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception message is internal control flow, not rendered HTML output.
		throw new \RuntimeException( self::$last_error['message'] );
	}

	/**
	 * Create the master key using the local key file first, then fall back to
	 * persisting SITEFORT_MASTER_KEY in wp-config.php on hosts where file-backed
	 * secret storage is incompatible with the PHP worker model.
	 *
	 * @throws \RuntimeException When neither persistence strategy succeeds.
	 */
	private static function create_key_with_host_fallback( string $path ): string {
		$key         = random_bytes( self::KEY_BYTES );
		$file_result = self::write_key_file_payload( $path, $key, 'Generated', true );

		if ( true === $file_result ) {
			return $key;
		}

		if ( is_wp_error( $file_result ) && $file_result->get_error_code() === 'key_file_exists' && file_exists( $path ) ) {
			return self::load_from_file( $path );
		}

		$config_result = self::persist_key_to_wp_config( $key, false, false );
		if ( ! is_wp_error( $config_result ) ) {
			return $key;
		}

		$file_message   = $file_result instanceof \WP_Error ? $file_result->get_error_message() : 'Unknown key-file write failure.';
		$config_message = $config_result->get_error_message();
		self::$last_error = [
			'code'    => 'key_write_failed',
			'path'    => $path,
			'message' => sprintf(
				'Cannot persist the SiteFort encryption key locally. File mode failed: %1$s Fallback to wp-config.php also failed: %2$s',
				sanitize_text_field( $file_message ),
				sanitize_text_field( $config_message )
			),
		];

		throw new \RuntimeException( 'Cannot persist the SiteFort encryption key locally.' );
	}

	/**
	 * Atomically create the key file using exclusive-create mode.
	 *
	 * fopen($path, 'x') (O_CREAT|O_EXCL) is intentional: it fails if the file
	 * already exists, so two concurrent first-run requests cannot generate
	 * different keys and overwrite each other.
	 *
	 * Permissions are selected from proven runtime file modes in the same
	 * environment, then verified after the write completes.
	 *
	 * @param string $path Absolute file path.
	 * @return string Raw 32-byte key.
	 * @throws \RuntimeException On write failure.
	 */
	private static function atomic_create_key_file( string $path ): string {
		$key = random_bytes( self::KEY_BYTES );
		$result = self::write_key_file_payload( $path, $key, 'Generated', true );
		if ( is_wp_error( $result ) ) {
			self::$last_error = [
				'code'    => 'key_write_failed',
				'path'    => $path,
				'message' => sanitize_text_field( $result->get_error_message() ),
			];
			throw new \RuntimeException( 'Failed to write the encryption key file.' );
		}

		return $key;
	}

	/**
	 * Persist key bytes to the local key file.
	 *
	 * @param string $path             Absolute key-file path.
	 * @param string $key              Raw key bytes.
	 * @param string $action           Header label: Generated or Restored.
	 * @param bool   $exclusive_create Whether the target must not already exist.
	 * @return true|\WP_Error
	 */
	private static function write_key_file_payload( string $path, string $key, string $action, bool $exclusive_create ) {
		$dir = dirname( $path );
		if ( ! is_dir( $dir ) && ! FilesystemMutation::ensure_directory( $dir, self::key_file_mode( $dir ) ) ) {
			return new \WP_Error( 'key_dir_create_failed', 'Cannot create directory for key file: ' . $dir );
		}

		$modes        = self::key_file_mode_candidates( $dir );
		$b64          = base64_encode( $key );
		$content      = self::build_key_file_content( $b64, $action );
		$filesystem   = FilesystemMutation::get_filesystem();
		$can_write_raw = self::can_directly_write_key_directory();

		if ( $filesystem && ! $can_write_raw ) {
			return self::write_key_file_via_filesystem( $filesystem, $path, $content, $modes, $exclusive_create );
		}

		$applied_mode = null;

		if ( $exclusive_create ) {
			// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- Exclusive-create mode preserves atomic first-write semantics for the master key file.
			$handle = @fopen( $path, 'x' );
			if ( $handle === false ) {
				if ( file_exists( $path ) ) {
					return new \WP_Error( 'key_file_exists', 'Key file already exists.' );
				}

				return new \WP_Error(
					'key_file_create_failed',
					sprintf(
						'Cannot create key file at %1$s. Check that the web server user has write permission on %2$s, or use SITEFORT_MASTER_KEY in wp-config.php.',
						$path,
						$dir
					)
				);
			}

			foreach ( $modes as $candidate_mode ) {
				if ( self::harden_key_file_permissions( $path, $candidate_mode ) ) {
					$applied_mode = $candidate_mode;
					break;
				}
			}

			if ( ! is_int( $applied_mode ) ) {
				// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
				fclose( $handle );
				wp_delete_file( $path );

				return new \WP_Error(
					'key_permissions_failed',
					sprintf(
						'Cannot apply a readable secure permission profile to key file at %s before writing secret data.',
						$path
					)
				);
			}

			$written = self::write_all( $handle, $content );
			// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Closes the exclusive-create handle after master-key write.
			fclose( $handle );

			if ( ! $written ) {
				wp_delete_file( $path );

				return new \WP_Error(
					'key_write_failed',
					sprintf( 'Failed to write the key file at %s.', $path )
				);
			}
		} else {
			$tmp_path = $path . '.tmp.' . self::random_suffix();
			// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- Exclusive-create temp file prevents clobbering concurrent key restoration writes.
			$handle = @fopen( $tmp_path, 'x' );
			if ( false === $handle ) {
				return new \WP_Error( 'key_temp_create_failed', 'Could not create temporary key file. Check directory permissions.' );
			}

			foreach ( $modes as $candidate_mode ) {
				if ( self::harden_key_file_permissions( $tmp_path, $candidate_mode ) ) {
					$applied_mode = $candidate_mode;
					break;
				}
			}

			if ( ! is_int( $applied_mode ) ) {
				// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
				fclose( $handle );
				wp_delete_file( $tmp_path );

				return new \WP_Error( 'key_permissions_failed', 'Could not apply a readable secure permission profile before writing key file.' );
			}

			$written = self::write_all( $handle, $content );
			// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Closes the temporary key-file handle after write.
			fclose( $handle );
			if ( ! $written ) {
				wp_delete_file( $tmp_path );

				return new \WP_Error( 'key_write_failed', 'Could not write key file. Check available disk space.' );
			}

			if ( ! self::move_file( $tmp_path, $path ) ) {
				wp_delete_file( $tmp_path );

				return new \WP_Error( 'key_finalize_failed', 'Could not finalize key file. Check directory permissions.' );
			}
		}

		if ( ! self::verify_written_contents( $path, $content ) ) {
			wp_delete_file( $path );

			return new \WP_Error( 'key_verify_failed', 'Key file write verification failed.' );
		}

		if ( is_int( $applied_mode ) ) {
			self::set_file_permissions( $path, $applied_mode );
		}

		return true;
	}

	/**
	 * Harden a key-file placeholder before writing key material.
	 */
	private static function harden_key_file_permissions( string $path, int $mode ): bool {
		self::set_file_permissions( $path, $mode );
		clearstatcache( true, $path );

		$perms = @fileperms( $path );
		if ( false === $perms ) {
			return false;
		}

		$effective = $perms & 0777;

		return ( $effective & 0600 ) === 0600
			&& ( $effective & 0111 ) === 0
			&& ( $effective & 0002 ) === 0
			&& ( ( $effective | $mode ) === $mode );
	}

	/**
	 * Build the PHP key file content string.
	 *
	 * @param string $b64    Base64-encoded key.
	 * @param string $action 'Generated' or 'Restored' (appears in the file header comment).
	 * @return string
	 */
		private static function build_key_file_content( string $b64, string $action ): string {
			return implode(
				"\n",
				[
					'<?php',
					'/**',
					' * SiteFort Encryption Key',
					' *',
					' * WARNING: DO NOT MODIFY OR DELETE THIS FILE.',
					' *',
					' * This file contains the master encryption key used by the SiteFort security',
					' * plugin. All cloud credentials and sensitive data stored by the plugin are',
					' * encrypted with a key derived from this value.',
					' *',
					' * If this file is deleted or modified:',
					' *  - Stored credentials will become unreadable and must be recovered.',
					' *  - Scheduled scans will stop working until the issue is resolved.',
					' *  - You will be prompted to recover or regenerate via the SiteFort admin panel.',
					' *',
					' * If you need to migrate this site, copy this file along with your WordPress files.',
					' * For container or managed-hosting deployments, you may set the',
					' * SITEFORT_MASTER_KEY constant in wp-config.php instead.',
					' *',
					' * ' . $action . ' by SiteFort. Do not edit.',
					' */',
					"if ( ! defined( 'ABSPATH' ) ) { exit; }",
					"return '{$b64}';",
					'',
				]
			);
		}

	/**
	 * Determine the file mode to use for the key file.
	 *
	 * Prefers the strongest candidate from the current environment's proven file
	 * modes. The candidate list is built separately and can include fallback
	 * read profiles for hosts where the PHP reader is not the same owner as the
	 * first writer.
	 *
	 * @param string $dir Directory that will contain the key file.
	 * @return int Octal file mode.
	 */
	private static function key_file_mode( string $dir ): int {
		$candidates = self::key_file_mode_candidates( $dir );
		return $candidates[0];
	}

	/**
	 * Determine candidate file modes for the key file.
	 *
	 * Order is biased toward modes already proven to work in this environment,
	 * then falls back through progressively more permissive read profiles.
	 *
	 * @return int[]
	 */
	private static function key_file_mode_candidates( string $dir ): array {
		$candidates = [];
		$preferred  = self::preferred_runtime_file_mode( $dir );

		if ( is_int( $preferred ) ) {
			$candidates[] = self::normalize_key_file_mode( $preferred );
			$candidates[] = self::normalize_key_file_mode( $preferred & 0660 );
		}

		if ( defined( 'FS_CHMOD_FILE' ) ) {
			$candidates[] = self::normalize_key_file_mode( (int) FS_CHMOD_FILE );
		}

		$candidates[] = 0640;
		$candidates[] = 0600;
		$candidates[] = 0660;
		$candidates[] = 0644;
		$candidates[] = 0664;

		$normalized = [];
		foreach ( $candidates as $candidate ) {
			$mode = self::normalize_key_file_mode( (int) $candidate );
			if ( ! in_array( $mode, $normalized, true ) ) {
				$normalized[] = $mode;
			}
		}

		return $normalized;
	}

	/**
	 * Inspect sibling runtime files to find the host's proven file mode.
	 */
	private static function preferred_runtime_file_mode( string $dir ): ?int {
		// Probe plugin-owned files in the same sitefort-data directory.
		// These were written by the same PHP process, so their mode reflects the
		// actual ownership/group of this environment - same technique used by security plugins.
		$probes = [ 'waf-config.php', 'bootstrap.php' ];
		foreach ( $probes as $probe ) {
			$probe_path = rtrim( $dir, '/' ) . '/' . $probe;
			if ( file_exists( $probe_path ) ) {
				$stat = @stat( $probe_path );
				if ( $stat !== false ) {
					return $stat[2] & 0777;
				}
			}
		}

		return null;
	}

	/**
	 * Normalize a candidate key-file mode.
	 *
	 * Executable bits and world-write are never allowed. Owner read/write is
	 * always preserved, while world-read remains available only when explicitly
	 * included in the candidate mode.
	 */
	private static function normalize_key_file_mode( int $mode ): int {
		$mode = $mode & 0666;
		$mode |= 0600;
		$mode &= ~0002;

		return $mode;
	}

	/**
	 * Whether the key directory can be written directly by PHP.
	 */
	private static function can_directly_write_key_directory(): bool {
		return self::can_directly_write_directory( dirname( self::get_key_file_path() ) );
	}

	/**
	 * Whether a directory can be created or updated directly by PHP.
	 */
	private static function can_directly_write_directory( string $dir ): bool {
		$current = $dir;
		while ( $current !== '' && $current !== '.' && $current !== dirname( $current ) ) {
			if ( is_dir( $current ) ) {
				return wp_is_writable( $current );
			}

			$current = dirname( $current );
		}

		return is_dir( $current ) && wp_is_writable( $current );
	}

	/**
	 * Write the key file using an initialized WP_Filesystem adapter.
	 *
	 * This path is used when direct local writes are not available but the site
	 * admin supplied WordPress filesystem credentials.
	 *
	 * @param \WP_Filesystem_Base $filesystem       Initialized filesystem adapter.
	 * @param string              $path             Absolute key-file path.
	 * @param string              $content          File contents.
	 * @param int[]               $modes            Candidate file modes.
	 * @param bool                $exclusive_create Whether the target must not already exist.
	 * @return true|\WP_Error
	 */
	private static function write_key_file_via_filesystem( \WP_Filesystem_Base $filesystem, string $path, string $content, array $modes, bool $exclusive_create ) {
		$dir = dirname( $path );

		try {
			if ( ! $filesystem->is_dir( $dir ) && ! $filesystem->mkdir( $dir, self::key_file_mode( $dir ), true ) ) {
				return new \WP_Error( 'key_dir_create_failed', 'Cannot create directory for key file: ' . $dir );
			}

			if ( $exclusive_create && $filesystem->exists( $path ) ) {
				return new \WP_Error( 'key_file_exists', 'Key file already exists.' );
			}

			$temp_path    = $path . '.tmp.' . self::random_suffix();
			$applied_mode = null;

			foreach ( $modes as $candidate_mode ) {
				if ( $filesystem->put_contents( $temp_path, $content, $candidate_mode ) ) {
					$read_back = $filesystem->get_contents( $temp_path );
					if ( is_string( $read_back ) && $read_back === $content ) {
						$applied_mode = $candidate_mode;
						break;
					}
				}

				if ( $filesystem->exists( $temp_path ) ) {
					$filesystem->delete( $temp_path, false );
				}
			}

			if ( ! is_int( $applied_mode ) ) {
				return new \WP_Error( 'key_write_failed', 'Could not write the key file through the configured WordPress filesystem transport.' );
			}

			if ( $exclusive_create && $filesystem->exists( $path ) ) {
				$filesystem->delete( $temp_path, false );
				return new \WP_Error( 'key_file_exists', 'Key file already exists.' );
			}

			if ( ! self::move_file( $temp_path, $path ) ) {
				if ( $filesystem->exists( $temp_path ) ) {
					$filesystem->delete( $temp_path, false );
				}

				return new \WP_Error( 'key_finalize_failed', 'Could not finalize key file. Check directory permissions.' );
			}

			if ( ! self::verify_written_contents( $path, $content ) ) {
				self::delete_path( $path );
				return new \WP_Error( 'key_verify_failed', 'Key file write verification failed.' );
			}

			self::set_file_permissions( $path, $applied_mode );
		} catch ( \Throwable $exception ) {
			return new \WP_Error( 'key_write_failed', 'The configured WordPress filesystem transport failed during the key file write.' );
		}

		return true;
	}

	/**
	 * Persist a known key locally for backup restoration.
	 *
	 * File-backed sites update the local key file. Constant-backed sites update
	 * the SITEFORT_MASTER_KEY value in wp-config.php instead.
	 *
	 * @param string $key Raw 32-byte key.
	 * @return true|\WP_Error True on success, WP_Error on failure.
	 */
	public static function write_key_file( string $key ) {
		if ( strlen( $key ) !== self::KEY_BYTES ) {
			return new \WP_Error( 'invalid_key', 'Key must be exactly ' . self::KEY_BYTES . ' bytes.' );
		}

		if ( null !== self::get_configured_constant_key() ) {
			$result = self::persist_key_to_wp_config( $key, true, false );
			if ( is_wp_error( $result ) ) {
				return $result;
			}

			self::$cache      = $key;
			self::$last_error = null;

			return true;
		}

		$path    = self::get_key_file_path();
		$result = self::write_key_file_payload( $path, $key, 'Restored', false );
		if ( is_wp_error( $result ) ) {
			return $result;
		}

		self::$cache      = $key;
		self::$last_error = null;

		return true;
	}

	/**
	 * Persist SITEFORT_MASTER_KEY into wp-config.php.
	 *
	 * @param string $key                       Raw key bytes.
	 * @param bool   $require_existing_constant Whether the define must already exist.
	 * @param bool   $keep_backup               Whether to preserve the backup path for rollback.
	 * @return array{path:string,backup_path:?string,permissions:?int}|\WP_Error
	 */
	public static function persist_key_to_wp_config( string $key, bool $require_existing_constant = false, bool $keep_backup = false ) {
		if ( strlen( $key ) !== self::KEY_BYTES ) {
			return new \WP_Error( 'invalid_key', 'Key must be exactly ' . self::KEY_BYTES . ' bytes.' );
		}

		if ( ! self::can_write_to_wp_config() ) {
			return new \WP_Error(
				'writes_disabled',
				'Write to Files is disabled in SiteFort Settings. Enable it to update SITEFORT_MASTER_KEY automatically, or update wp-config.php manually.'
			);
		}

		$wp_config = self::resolve_wp_config_path();
		if ( ! is_string( $wp_config ) || $wp_config === '' || ! file_exists( $wp_config ) ) {
			return new \WP_Error( 'config_not_found', 'Could not locate wp-config.php in ABSPATH or one directory above.' );
		}

		$dir          = dirname( $wp_config );
		$file_writable = wp_is_writable( $wp_config );
		$dir_writable  = is_dir( $dir ) && wp_is_writable( $dir );
		$filesystem    = FilesystemMutation::get_filesystem();

		if ( ! $dir_writable && ! $file_writable && ! $filesystem ) {
			return new \WP_Error(
				'config_not_writable',
				'wp-config.php is not writable, and its parent directory is not writable for atomic replacement.'
			);
		}

		$current = self::read_file_contents( $wp_config );
		if ( ! is_string( $current ) ) {
			return new \WP_Error( 'config_read_failed', 'Could not read wp-config.php.' );
		}

		$updated = self::upsert_key_constant_in_config( $current, base64_encode( $key ), $require_existing_constant );
		if ( ! is_string( $updated ) ) {
			return new \WP_Error(
				$require_existing_constant ? 'constant_not_found' : 'config_compose_failed',
				$require_existing_constant
						? 'Could not locate SITEFORT_MASTER_KEY define in wp-config.php.'
					: 'Could not safely inject SITEFORT_MASTER_KEY into wp-config.php.'
			);
		}

		if ( $updated === $current ) {
			return [
				'path'        => $wp_config,
				'backup_path' => null,
				'permissions' => is_int( @fileperms( $wp_config ) ) ? ( @fileperms( $wp_config ) & 0777 ) : null,
			];
		}

		$backup = $wp_config . '.sitefort-key-backup-' . self::random_suffix();
		if ( ! self::copy_path( $wp_config, $backup ) ) {
			return new \WP_Error( 'config_backup_failed', 'Could not create backup of wp-config.php.' );
		}

		$original_perms = @fileperms( $wp_config );
		$original_mode  = is_int( $original_perms ) ? ( $original_perms & 0777 ) : null;
		$write_result   = $filesystem && ! $dir_writable && ! $file_writable
			? self::write_existing_file_via_filesystem( $filesystem, $wp_config, $updated, $original_mode, true )
			: ( $dir_writable
				? self::atomic_replace_existing_file( $wp_config, $updated, $original_mode )
				: self::direct_write_existing_file( $wp_config, $updated, $original_mode ) );

		if ( is_wp_error( $write_result ) ) {
			if ( ! self::restore_backup_file( $backup, $wp_config, $original_mode ) ) {
				return new \WP_Error(
					'config_restore_failed',
					$write_result->get_error_message() . ' wp-config.php backup could not be restored automatically; backup preserved at ' . basename( $backup ) . '.'
				);
			}

			wp_delete_file( $backup );

			return $write_result;
		}

		if ( ! self::verify_written_contents( $wp_config, $updated ) ) {
			if ( ! self::restore_backup_file( $backup, $wp_config, $original_mode ) ) {
				return new \WP_Error(
					'config_verify_restore_failed',
					'Post-write verification for wp-config.php failed, and the backup could not be restored automatically.'
				);
			}

			wp_delete_file( $backup );

			return new \WP_Error( 'config_verify_failed', 'Post-write verification for wp-config.php failed. The original file was restored.' );
		}

		if ( is_int( $original_mode ) ) {
			self::set_file_permissions( $wp_config, $original_mode );
		}

		if ( ! $keep_backup ) {
			wp_delete_file( $backup );
		}

		return [
			'path'        => $wp_config,
			'backup_path' => $keep_backup ? $backup : null,
			'permissions' => $original_mode,
		];
	}

	/**
	 * Replace or inject SITEFORT_MASTER_KEY in wp-config.php contents.
	 *
	 * @return string|null
	 */
	private static function upsert_key_constant_in_config( string $content, string $b64, bool $require_existing_constant ): ?string {
		$define = "define('SITEFORT_MASTER_KEY', '" . $b64 . "');";
		$pattern = "/define\s*\(\s*['\"]SITEFORT_MASTER_KEY['\"]\s*,\s*['\"][^'\"]*['\"]\s*\)\s*;/";
		$updated = preg_replace( $pattern, $define, $content, 1, $count );

		if ( null === $updated ) {
			return null;
		}

		if ( $count > 0 ) {
			return $updated;
		}

		if ( $require_existing_constant ) {
			return null;
		}

		$comment_pattern = "/^[ \t]*\/\*\s*That's all, stop editing!.*$/mi";
		$updated = preg_replace( $comment_pattern, $define . "\n\n$0", $content, 1, $comment_count );
		if ( is_string( $updated ) && $comment_count > 0 ) {
			return $updated;
		}

		$settings_pattern = "/^[ \t]*require(?:_once)?\s*\(\s*ABSPATH\s*\.\s*['\"]wp-settings\.php['\"]\s*\)\s*;\s*$/mi";
		$updated = preg_replace( $settings_pattern, $define . "\n\n$0", $content, 1, $settings_count );
		if ( is_string( $updated ) && $settings_count > 0 ) {
			return $updated;
		}

		return rtrim( $content ) . "\n\n" . $define . "\n";
	}

	/**
	 * Atomically replace an existing file via temp file + move.
	 *
	 * @return true|\WP_Error
	 */
	private static function atomic_replace_existing_file( string $path, string $contents, ?int $mode ) {
		$dir = dirname( $path );
		$temp = tempnam( $dir, '.sitefort-' );
		if ( ! is_string( $temp ) || $temp === '' ) {
				$filesystem = FilesystemMutation::get_filesystem();
			if ( $filesystem ) {
				return self::write_existing_file_via_filesystem( $filesystem, $path, $contents, $mode, true );
			}

			return new \WP_Error( 'temp_file_failed', 'Could not create temporary file for atomic write.' );
		}

		if ( is_int( $mode ) ) {
			self::set_file_permissions( $temp, $mode );
		}

		if ( ! self::write_bytes_to_file( $temp, $contents, $mode ) ) {
			wp_delete_file( $temp );
			return new \WP_Error( 'config_write_failed', 'Could not write updated file contents.' );
		}

		if ( ! self::move_file( $temp, $path ) ) {
			wp_delete_file( $temp );
			return new \WP_Error( 'config_rename_failed', 'Atomic replacement of the file failed.' );
		}

		return true;
	}

	/**
	 * Overwrite an existing file directly when the directory cannot be replaced atomically.
	 *
	 * @return true|\WP_Error
	 */
	private static function direct_write_existing_file( string $path, string $contents, ?int $mode ) {
		$filesystem = FilesystemMutation::get_filesystem();
		if ( $filesystem && ! wp_is_writable( $path ) ) {
			return self::write_existing_file_via_filesystem( $filesystem, $path, $contents, $mode, false );
		}

		return self::with_temporary_file_permissions(
			$path,
			static function () use ( $path, $contents, $mode ) {
				if ( ! self::write_bytes_to_file( $path, $contents, $mode ) ) {
					return new \WP_Error( 'config_write_failed', 'Could not write updated file contents.' );
				}

				return true;
			}
		);
	}

	/**
	 * Rewrite an existing file through a WP_Filesystem adapter.
	 *
	 * @return true|\WP_Error
	 */
	private static function write_existing_file_via_filesystem( \WP_Filesystem_Base $filesystem, string $path, string $contents, ?int $mode, bool $prefer_atomic ) {
		$target_mode = is_int( $mode ) && $mode > 0
			? $mode
			: ( defined( 'FS_CHMOD_FILE' ) ? (int) FS_CHMOD_FILE : 0644 );

		try {
			if ( $prefer_atomic ) {
				$temp_path = $path . '.tmp.' . self::random_suffix();
				if ( $filesystem->put_contents( $temp_path, $contents, $target_mode ) ) {
					if ( self::move_file( $temp_path, $path ) ) {
						return true;
					}

					if ( $filesystem->exists( $temp_path ) ) {
						$filesystem->delete( $temp_path, false );
					}
				}
			}

			if ( $filesystem->put_contents( $path, $contents, $target_mode ) ) {
				return true;
			}
		} catch ( \Throwable $exception ) {
			return new \WP_Error( 'config_write_failed', 'The configured WordPress filesystem transport failed during the write.' );
		}

		return new \WP_Error( 'config_write_failed', 'Could not write updated file contents through the configured WordPress filesystem transport.' );
	}

	/**
	 * Temporarily add owner-write permission to a file for direct-overwrite fallback.
	 *
	 * @return true|\WP_Error
	 */
	private static function with_temporary_file_permissions( string $file, callable $writer ) {
		$original_perms = is_file( $file ) ? @fileperms( $file ) : false;
		$changed        = false;

		if ( is_int( $original_perms ) && ! wp_is_writable( $file ) ) {
			$mode = $original_perms & 0777;
			if ( self::set_file_permissions( $file, $mode | 0200 ) ) {
				$changed = true;
			}
		}

		try {
			return $writer();
		} finally {
			if ( $changed && is_int( $original_perms ) ) {
				self::set_file_permissions( $file, $original_perms & 0777 );
			}
		}
	}

	private static function restore_backup_file( string $backup, string $destination, ?int $mode ): bool {
		if ( ! self::copy_path( $backup, $destination, $mode ) ) {
			return false;
		}

		return true;
	}

	/**
	 * @return string|false
	 */
	private static function read_file_contents( string $path ) {
		return FilesystemMutation::read_file_contents( $path );
	}

	private static function write_bytes_to_file( string $path, string $contents, ?int $mode = null ): bool {
		return FilesystemMutation::write_bytes_to_file( $path, $contents, $mode );
	}

	private static function verify_written_contents( string $path, string $expected ): bool {
		$read = self::read_file_contents( $path );
		return is_string( $read ) && $read === $expected;
	}

	private static function move_file( string $source, string $destination ): bool {
		return FilesystemMutation::move_path( $source, $destination );
	}

	private static function set_file_permissions( string $path, int $permissions ): bool {
		return FilesystemMutation::apply_file_permissions( $path, $permissions );
	}

	/**
	 * @param resource $handle
	 */
	private static function write_all( $handle, string $data ): bool {
		$written = 0;
		$length  = strlen( $data );

		while ( $written < $length ) {
			$result = fwrite( $handle, substr( $data, $written ) ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite
			if ( false === $result || 0 === $result ) {
				return false;
			}

			$written += $result;
		}

		return true;
	}

	private static function can_write_to_wp_config(): bool {
		if ( class_exists( '\SiteFort\Modules\Tools\ConfigFilePolicy' ) ) {
			return \SiteFort\Modules\Tools\ConfigFilePolicy::can_write_to_files();
		}

		return true;
	}

	/**
	 * Whether wp-config.php can be updated directly without prompting for
	 * filesystem credentials.
	 */
	private static function can_directly_write_wp_config(): bool {
		if ( ! self::can_write_to_wp_config() ) {
			return false;
		}

		$wp_config = self::resolve_wp_config_path();
		if ( ! is_string( $wp_config ) || $wp_config === '' || ! file_exists( $wp_config ) ) {
			return false;
		}

		$dir = dirname( $wp_config );

		return wp_is_writable( $wp_config ) || ( is_dir( $dir ) && wp_is_writable( $dir ) );
	}

	/**
	 * @return string|false
	 */
	private static function resolve_wp_config_path() {
		if ( class_exists( '\SiteFort\Modules\Tools\ConfigFilePolicy' ) ) {
			return \SiteFort\Modules\Tools\ConfigFilePolicy::get_wp_config_path();
		}

		$candidates = [ ABSPATH . 'wp-config.php' ];
		$parent     = dirname( ABSPATH ) . '/wp-config.php';
		if ( ! file_exists( dirname( ABSPATH ) . '/wp-settings.php' ) ) {
			$candidates[] = $parent;
		}

		foreach ( $candidates as $candidate ) {
			if ( file_exists( $candidate ) ) {
				return $candidate;
			}
		}

		return false;
	}

	private static function random_suffix(): string {
		try {
			return bin2hex( random_bytes( 8 ) );
		} catch ( \Throwable $exception ) {
			return wp_generate_password( 16, false, false );
		}
	}
}
