<?php
/**
 * Shared wp-config.php path, backup, validation, and write helpers.
 *
 * @package WP_PowerSuite\Core
 * @since 1.0.0
 */

namespace WP_PowerSuite\Core;

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

final class WpConfigHelper {

	public const SECRET_MASK = '********';

	private const PRIVATE_DIR = 'wp-powersuite-private';
	private const BACKUP_DIR  = 'wp-config-backups';
	private const MAX_BACKUPS = 50;

	/** @var list<string> */
	private const SENSITIVE_DEFINES = [
		'DB_PASSWORD',
		'DB_USER',
		'DB_NAME',
		'AUTH_KEY',
		'SECURE_AUTH_KEY',
		'LOGGED_IN_KEY',
		'NONCE_KEY',
		'AUTH_SALT',
		'SECURE_AUTH_SALT',
		'LOGGED_IN_SALT',
		'NONCE_SALT',
	];

	/**
	 * Capability required to read or edit wp-config.php in the current context.
	 */
	public static function requiredManageCapability(): string {
		if ( is_multisite() ) {
			return 'manage_network_options';
		}

		return 'manage_options';
	}

	/**
	 * Whether the current user may manage wp-config.php.
	 */
	public static function canManageWpConfig(): bool {
		return current_user_can( self::requiredManageCapability() );
	}

	/**
	 * Resolve the active wp-config.php path (ABSPATH or one directory above).
	 */
	public static function resolveConfigPath(): ?string {
		$layout = self::getInstallLayout();
		if ( $layout === 'bedrock' ) {
			return null;
		}

		$path = ABSPATH . 'wp-config.php';
		if ( file_exists( $path ) ) {
			return wp_normalize_path( $path );
		}

		$path = dirname( ABSPATH ) . '/wp-config.php';
		if ( file_exists( $path ) ) {
			return wp_normalize_path( $path );
		}

		/**
		 * Filter the resolved wp-config.php path.
		 *
		 * @param string|null $path Absolute path or null when not found.
		 */
		$filtered = apply_filters( 'wp_powersuite_wp_config_path', null );
		if ( is_string( $filtered ) && $filtered !== '' && file_exists( $filtered ) ) {
			return wp_normalize_path( $filtered );
		}

		return null;
	}

	/**
	 * Detect common WordPress configuration layouts.
	 *
	 * @return 'standard'|'bedrock'|'missing'|'custom'
	 */
	public static function getInstallLayout(): string {
		if ( defined( 'BEDROCK_PROJECT' ) ) {
			return 'bedrock';
		}

		$bedrock_application = wp_normalize_path( dirname( ABSPATH ) . '/config/application.php' );
		if ( file_exists( $bedrock_application ) ) {
			return 'bedrock';
		}

		if ( file_exists( ABSPATH . 'wp-config.php' ) || file_exists( dirname( ABSPATH ) . '/wp-config.php' ) ) {
			return 'standard';
		}

		$filtered = apply_filters( 'wp_powersuite_wp_config_path', null );
		if ( is_string( $filtered ) && $filtered !== '' && file_exists( $filtered ) ) {
			return 'custom';
		}

		return 'missing';
	}

	/**
	 * Human-readable guidance for the detected install layout.
	 */
	public static function getLayoutMessage( string $layout ): string {
		switch ( $layout ) {
			case 'bedrock':
				return __( 'Bedrock-style installs manage configuration in config/application.php. This module cannot edit wp-config.php on this layout.', 'wp-powersuite' );
			case 'missing':
				return __( 'wp-config.php was not found in the default locations. If your host uses a custom path, ask your developer to set the wp_powersuite_wp_config_path filter.', 'wp-powersuite' );
			case 'custom':
				return __( 'Using a custom wp-config.php path via wp_powersuite_wp_config_path.', 'wp-powersuite' );
			default:
				return '';
		}
	}

	/**
	 * Summary for admin UI and capability checks.
	 *
	 * @return array{path: string, writable: bool, layout: string, layout_message: string, requires_network_admin: bool, can_manage: bool}
	 */
	public static function getConfigStatus(): array {
		$layout = self::getInstallLayout();
		$path   = self::resolveConfigPath();

		return [
			'path'                     => $path ?? '',
			'writable'                 => $path !== null && is_writable( $path ),
			'layout'                   => $layout,
			'layout_message'           => self::getLayoutMessage( $layout ),
			'requires_network_admin'   => is_multisite() && ! current_user_can( 'manage_network_options' ),
			'can_manage'               => self::canManageWpConfig(),
		];
	}

	/**
	 * Resolve debug.log path from runtime constants.
	 */
	public static function resolveDebugLogPath(): string {
		if ( defined( 'WP_DEBUG_LOG' ) ) {
			if ( is_string( WP_DEBUG_LOG ) && WP_DEBUG_LOG !== '' ) {
				return wp_normalize_path( WP_DEBUG_LOG );
			}
			if ( WP_DEBUG_LOG ) {
				return wp_normalize_path( WP_CONTENT_DIR . '/debug.log' );
			}
		}

		return wp_normalize_path( WP_CONTENT_DIR . '/debug.log' );
	}

	/**
	 * Attempt to block direct HTTP access to debug.log under wp-content.
	 *
	 * @return array{success: bool, message: string}
	 */
	public static function protectDebugLogAccess(): array {
		$log_path = self::resolveDebugLogPath();
		$log_dir  = wp_normalize_path( dirname( $log_path ) );

		if ( wp_normalize_path( WP_CONTENT_DIR ) !== $log_dir ) {
			return [
				'success' => false,
				'message' => __( 'Debug log is outside wp-content. Add server rules manually to block public access.', 'wp-powersuite' ),
			];
		}

		if ( ! file_exists( $log_path ) ) {
			if ( file_put_contents( $log_path, '', LOCK_EX ) === false ) {
				return [
					'success' => false,
					'message' => __( 'Could not create debug.log for protection.', 'wp-powersuite' ),
				];
			}
		}

		$htaccess = WP_CONTENT_DIR . '/.htaccess';
		$rules    = "\n# WP PowerSuite: deny direct access to debug.log\n<Files debug.log>\n\tRequire all denied\n</Files>\n<IfModule !mod_authz_core.c>\n\t<Files debug.log>\n\t\tOrder deny,allow\n\t\tDeny from all\n\t</Files>\n</IfModule>\n";
		$marker   = '# WP PowerSuite: deny direct access to debug.log';

		$existing = file_exists( $htaccess ) ? (string) file_get_contents( $htaccess ) : '';
		if ( str_contains( $existing, $marker ) ) {
			return [
				'success' => true,
				'message' => __( 'debug.log is already protected in wp-content/.htaccess.', 'wp-powersuite' ),
			];
		}

		if ( file_put_contents( $htaccess, $existing . $rules, LOCK_EX ) === false ) {
			return [
				'success' => false,
				'message' => __( 'Could not update wp-content/.htaccess to protect debug.log.', 'wp-powersuite' ),
			];
		}

		return [
			'success' => true,
			'message' => __( 'Added debug.log deny rules to wp-content/.htaccess.', 'wp-powersuite' ),
		];
	}

	/**
	 * Read a file and return a descriptive error when unavailable.
	 *
	 * @return array{success: bool, content?: string, message?: string}
	 */
	public static function readFileContents( string $path ): array {
		if ( ! is_readable( $path ) ) {
			return [
				'success' => false,
				'message' => __( 'File is not readable.', 'wp-powersuite' ),
			];
		}

		$content = file_get_contents( $path );
		if ( $content === false ) {
			$error = error_get_last();
			$detail = is_array( $error ) && isset( $error['message'] ) ? $error['message'] : __( 'Unknown filesystem error.', 'wp-powersuite' );

			return [
				'success' => false,
				'message' => sprintf(
					/* translators: %s: PHP filesystem error message */
					__( 'Could not read file: %s', 'wp-powersuite' ),
					$detail
				),
			];
		}

		return [
			'success' => true,
			'content' => $content,
		];
	}

	/** Maximum secure backups retained per label. */
	public static function getMaxBackupsPerLabel(): int {
		return self::MAX_BACKUPS;
	}

	/**
	 * Whether the site environment type is production.
	 */
	public static function isProductionEnvironment(): bool {
		return function_exists( 'wp_get_environment_type' ) && wp_get_environment_type() === 'production';
	}

	/**
	 * Mask sensitive define() values for admin display.
	 */
	public static function maskSecrets( string $content ): string {
		foreach ( self::SENSITIVE_DEFINES as $name ) {
			$content = preg_replace_callback(
				'/define\s*\(\s*([\'"])' . preg_quote( $name, '/' ) . '\1\s*,\s*([\'"])((?:\\\\.|(?!\2).)*)\2\s*\)/s',
				static function ( array $matches ) use ( $name ): string {
					return 'define( ' . $matches[1] . $name . $matches[1] . ', ' . $matches[2] . self::SECRET_MASK . $matches[2] . ' )';
				},
				$content
			) ?? $content;
		}

		return $content;
	}

	/**
	 * Restore masked placeholder values from the original file before saving.
	 */
	public static function mergeMaskedSecrets( string $edited, string $original ): string {
		foreach ( self::SENSITIVE_DEFINES as $name ) {
			$edited = preg_replace_callback(
				'/define\s*\(\s*([\'"])' . preg_quote( $name, '/' ) . '\1\s*,\s*([\'"])' . preg_quote( self::SECRET_MASK, '/' ) . '\2\s*\)/',
				static function ( array $matches ) use ( $original, $name ): string {
					$pattern = '/define\s*\(\s*([\'"])' . preg_quote( $name, '/' ) . '\1\s*,\s*([\'"])((?:\\\\.|(?!\2).)*)\2\s*\)/s';
					if ( preg_match( $pattern, $original, $orig ) ) {
						return 'define( ' . $orig[1] . $name . $orig[1] . ', ' . $orig[2] . $orig[3] . $orig[2] . ' )';
					}

					return $matches[0];
				},
				$edited
			);
		}

		return $edited;
	}

	/**
	 * Validate PHP syntax for wp-config.php content.
	 *
	 * @return array{valid: bool, message: string}
	 */
	public static function validatePhpSyntax( string $content ): array {
		$content = str_replace( [ "\r\n", "\r" ], "\n", $content );
		if ( trim( $content ) === '' ) {
			return [
				'valid'   => false,
				'message' => __( 'wp-config.php content cannot be empty.', 'wp-powersuite' ),
			];
		}

		if ( stripos( $content, '<?php' ) === false && stripos( $content, '<?=' ) === false ) {
			return [
				'valid'   => false,
				'message' => __( 'wp-config.php must contain a PHP opening tag.', 'wp-powersuite' ),
			];
		}

		$cli_message = self::lintViaPhpCli( $content );
		if ( $cli_message !== null ) {
			if ( $cli_message === '' ) {
				return [
					'valid'   => true,
					'message' => '',
				];
			}

			return [
				'valid'   => false,
				'message' => self::formatSyntaxErrorMessage( $cli_message ),
			];
		}

		try {
			token_get_all( $content, TOKEN_PARSE );
		} catch ( \ParseError $e ) {
			return [
				'valid'   => false,
				'message' => self::formatSyntaxErrorMessage( $e->getMessage() ),
			];
		}

		return [
			'valid'   => true,
			'message' => '',
		];
	}

	/**
	 * Create a backup of wp-config.php in a private, HTTP-denied directory.
	 *
	 * @param string $config_path Absolute path to wp-config.php.
	 * @param string $label       Short label (e.g. editor, debug).
	 * @return string|false Absolute backup path or false on failure.
	 */
	public static function createSecureBackup( string $config_path, string $label ): string|false {
		$read = self::readFileContents( $config_path );
		if ( ! $read['success'] ) {
			return false;
		}

		$content = $read['content'];

		if ( ! self::ensureBackupStorageDir() ) {
			return false;
		}

		$label    = sanitize_key( $label );
		$label    = $label !== '' ? $label : 'config';
		$token    = wp_generate_password( 16, false, false );
		$filename = sprintf( '%s-%s-%s.bak', $label, gmdate( 'Y-m-d-His' ), $token );
		$backup   = wp_normalize_path( self::getBackupStorageDir() . '/' . $filename );

		if ( ! self::atomicWrite( $backup, $content ) ) {
			return false;
		}

		self::pruneOldBackups( $label );
		self::pruneOldBackups( null );

		return $backup;
	}

	/**
	 * List recent secure backups.
	 *
	 * @return list<array{id: string, label: string, created: int, size: int, size_label: string}>
	 */
	public static function listBackups( ?string $label = null, int $limit = 20 ): array {
		self::ensureBackupStorageDir();

		$dir     = self::getBackupStorageDir();
		$pattern = $label !== null && $label !== ''
			? $dir . '/' . sanitize_key( $label ) . '-*.bak'
			: $dir . '/*.bak';

		$files = glob( $pattern );
		if ( ! is_array( $files ) ) {
			return [];
		}

		usort(
			$files,
			static function ( string $a, string $b ): int {
				return filemtime( $b ) <=> filemtime( $a );
			}
		);

		$files = array_slice( $files, 0, max( 1, $limit ) );
		$items = [];

		foreach ( $files as $file ) {
			if ( ! is_file( $file ) ) {
				continue;
			}

			$basename = basename( $file );
			$parts    = explode( '-', $basename, 2 );
			$items[]  = [
				'id'          => $basename,
				'label'       => $parts[0] ?? 'config',
				'created'     => (int) filemtime( $file ),
				'size'        => (int) filesize( $file ),
				'size_label'  => size_format( (int) filesize( $file ) ),
				'created_utc' => gmdate( 'Y-m-d H:i:s', (int) filemtime( $file ) ),
			];
		}

		return $items;
	}

	/**
	 * Restore wp-config.php from a secure backup basename.
	 *
	 * @return array{success: bool, message: string}
	 */
	public static function restoreBackupByBasename( string $basename, string $config_path ): array {
		$basename = basename( sanitize_file_name( $basename ) );
		if ( $basename === '' || ! preg_match( '/^[a-z0-9]+-\d{4}-\d{2}-\d{2}-\d{6}-[A-Za-z0-9]+\.bak$/', $basename ) ) {
			return [
				'success' => false,
				'message' => __( 'Invalid backup file.', 'wp-powersuite' ),
			];
		}

		$backup_path = wp_normalize_path( self::getBackupStorageDir() . '/' . $basename );
		if ( ! self::restoreFromBackup( $backup_path, $config_path ) ) {
			return [
				'success' => false,
				'message' => __( 'Could not restore backup.', 'wp-powersuite' ),
			];
		}

		return [
			'success' => true,
			'message' => sprintf(
				/* translators: %s: backup filename */
				__( 'Restored wp-config.php from backup: %s', 'wp-powersuite' ),
				$basename
			),
		];
	}

	/**
	 * Restore wp-config.php from a backup file created by this helper.
	 */
	public static function restoreFromBackup( string $backup_path, string $config_path ): bool {
		$backup_path = wp_normalize_path( $backup_path );
		$config_path = wp_normalize_path( $config_path );
		$storage_dir = wp_normalize_path( self::getBackupStorageDir() );

		if ( ! str_starts_with( $backup_path, $storage_dir . '/' ) ) {
			return false;
		}
		if ( ! is_readable( $backup_path ) || ! is_writable( $config_path ) ) {
			return false;
		}

		$content = self::readFileContents( $backup_path );
		if ( ! $content['success'] ) {
			return false;
		}

		return self::atomicWrite( $config_path, $content['content'] );
	}

	/**
	 * Write wp-config.php after syntax validation.
	 *
	 * @return array{success: bool, message: string}
	 */
	public static function writeValidatedConfig( string $config_path, string $content ): array {
		if ( ! is_writable( $config_path ) ) {
			return [
				'success' => false,
				'message' => __( 'wp-config.php is not writable.', 'wp-powersuite' ),
			];
		}

		$content = str_replace( [ "\r\n", "\r" ], "\n", $content );
		$content = rtrim( $content ) . "\n";

		$validation = self::validatePhpSyntax( $content );
		if ( ! $validation['valid'] ) {
			return [
				'success' => false,
				'message' => $validation['message'],
			];
		}

		if ( ! self::atomicWrite( $config_path, $content ) ) {
			return [
				'success' => false,
				'message' => __( 'Could not write wp-config.php.', 'wp-powersuite' ),
			];
		}

		return [
			'success' => true,
			'message' => '',
		];
	}

	/**
	 * Backup, write, and auto-restore on failure.
	 *
	 * @return array{success: bool, message: string, backup: string|false, restored: bool}
	 */
	public static function writeValidatedConfigSafely( string $config_path, string $content, string $backup_label ): array {
		$backup = self::createSecureBackup( $config_path, $backup_label );
		if ( $backup === false ) {
			return [
				'success'  => false,
				'message'  => __( 'Could not create backup. Write aborted.', 'wp-powersuite' ),
				'backup'   => false,
				'restored' => false,
			];
		}

		$write = self::writeValidatedConfig( $config_path, $content );
		if ( $write['success'] ) {
			return [
				'success'  => true,
				'message'  => '',
				'backup'   => $backup,
				'restored' => false,
			];
		}

		self::restoreFromBackup( $backup, $config_path );

		$message = $write['message'] !== ''
			? $write['message'] . ' ' . __( 'Previous config was restored from backup.', 'wp-powersuite' )
			: __( 'Could not write wp-config.php. Backup restored.', 'wp-powersuite' );

		return [
			'success'  => false,
			'message'  => $message,
			'backup'   => $backup,
			'restored' => true,
		];
	}

	/**
	 * Absolute path to the private backup storage directory.
	 */
	public static function getBackupStorageDir(): string {
		return wp_normalize_path( WP_CONTENT_DIR . '/' . self::PRIVATE_DIR . '/' . self::BACKUP_DIR );
	}

	/**
	 * Create backup directory and deny direct HTTP access.
	 */
	public static function ensureBackupStorageDir(): bool {
		$root = wp_normalize_path( WP_CONTENT_DIR . '/' . self::PRIVATE_DIR );
		$dir  = self::getBackupStorageDir();

		foreach ( [ $root, $dir ] as $path ) {
			if ( ! wp_mkdir_p( $path ) ) {
				return false;
			}
			self::writeDirectorySilencer( $path );
			self::writeAccessDenyRules( $path );
		}

		return is_dir( $dir ) && is_writable( $dir );
	}

	/**
	 * @return string|null Empty string when valid; error message when invalid; null when CLI unavailable.
	 */
	private static function lintViaPhpCli( string $content ): ?string {
		if ( ! function_exists( 'proc_open' ) ) {
			return null;
		}

		$temp = tempnam( sys_get_temp_dir(), 'wpps-wpcfg-' );
		if ( $temp === false ) {
			return null;
		}

		$written = file_put_contents( $temp, $content, LOCK_EX );
		if ( $written === false ) {
			@unlink( $temp );
			return null;
		}

		$php_binary = defined( 'PHP_BINARY' ) && is_string( PHP_BINARY ) && PHP_BINARY !== '' ? PHP_BINARY : 'php';
		$command    = escapeshellarg( $php_binary ) . ' -d display_errors=0 -l ' . escapeshellarg( $temp );

		$output = [];
		$code   = 1;
		@exec( $command, $output, $code ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.system_calls_exec

		@unlink( $temp );

		if ( $code === 0 ) {
			return '';
		}

		$message = trim( implode( "\n", $output ) );
		return $message !== '' ? $message : __( 'PHP syntax check failed.', 'wp-powersuite' );
	}

	private static function formatSyntaxErrorMessage( string $raw ): string {
		$raw = trim( preg_replace( '/\s+/', ' ', $raw ) ?? $raw );
		if ( $raw === '' ) {
			return __( 'wp-config.php contains invalid PHP syntax.', 'wp-powersuite' );
		}

		return sprintf(
			/* translators: %s: PHP parse/lint error detail */
			__( 'wp-config.php contains invalid PHP syntax: %s', 'wp-powersuite' ),
			$raw
		);
	}

	/**
	 * Write atomically via temp file in the target directory.
	 */
	public static function atomicWriteFile( string $path, string $content ): bool {
		return self::atomicWrite( $path, $content );
	}

	/**
	 * Write atomically via temp file in the target directory.
	 */
	private static function atomicWrite( string $path, string $content ): bool {
		$path = wp_normalize_path( $path );
		$dir  = wp_normalize_path( dirname( $path ) );

		if ( ! is_dir( $dir ) || ! is_writable( $dir ) ) {
			return false;
		}

		$temp = wp_normalize_path( $dir . '/.wpps-wpcfg-' . wp_generate_password( 16, false, false ) . '.tmp' );
		if ( file_put_contents( $temp, $content, LOCK_EX ) === false ) {
			return false;
		}

		@chmod( $temp, 0640 ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_chmod

		if ( ! @rename( $temp, $path ) ) {
			@unlink( $temp );
			return false;
		}

		return true;
	}

	private static function writeDirectorySilencer( string $dir ): void {
		$index = $dir . '/index.php';
		if ( file_exists( $index ) ) {
			return;
		}

		file_put_contents( $index, "<?php\n// Silence is golden.\n", LOCK_EX );
	}

	private static function writeAccessDenyRules( string $dir ): void {
		$htaccess = $dir . '/.htaccess';
		$rules    = "# WP PowerSuite: deny direct web access\n"
			. "<IfModule mod_authz_core.c>\n"
			. "\tRequire all denied\n"
			. "</IfModule>\n"
			. "<IfModule !mod_authz_core.c>\n"
			. "\tOrder deny,allow\n"
			. "\tDeny from all\n"
			. "</IfModule>\n";

		if ( ! file_exists( $htaccess ) || file_get_contents( $htaccess ) !== $rules ) {
			file_put_contents( $htaccess, $rules, LOCK_EX );
		}

		$web_config = $dir . '/web.config';
		$xml        = '<?xml version="1.0" encoding="UTF-8"?>'
			. '<configuration><system.webServer><security><authorization>'
			. '<remove users="*" roles="" verbs="" />'
			. '<add accessType="Deny" users="*" />'
			. '</authorization></security></system.webServer></configuration>';

		if ( ! file_exists( $web_config ) || file_get_contents( $web_config ) !== $xml ) {
			file_put_contents( $web_config, $xml, LOCK_EX );
		}
	}

	private static function pruneOldBackups( ?string $label ): void {
		if ( ! is_dir( self::getBackupStorageDir() ) ) {
			return;
		}

		$pattern = $label !== null && $label !== ''
			? self::getBackupStorageDir() . '/' . sanitize_key( $label ) . '-*.bak'
			: self::getBackupStorageDir() . '/*.bak';
		$files   = glob( $pattern );
		if ( ! is_array( $files ) || count( $files ) <= self::MAX_BACKUPS ) {
			return;
		}

		usort(
			$files,
			static function ( string $a, string $b ): int {
				return filemtime( $b ) <=> filemtime( $a );
			}
		);

		$to_delete = array_slice( $files, self::MAX_BACKUPS );
		foreach ( $to_delete as $file ) {
			if ( is_file( $file ) ) {
				@unlink( $file );
			}
		}
	}
}
