<?php
/**
 * Logger class handles logging messages to a file with size management.
 *
 * This file defines a logging utility that writes log entries to a file.
 * It supports file size rotation to prevent the log file from growing too large.
 *
 * PHP version 7.4+
 *
 * @package   Author_Kit
 * @since 1.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * AuthorKit Logger Class
 *
 * Handles logging of PHP errors, exceptions, WordPress errors, and custom plugin context.
 * Also includes a viewer for log data in the WP Admin Tools menu.
 */
class Author_Kit_Logger {

	/**
	 * Path to the log file where log entries will be saved.
	 *
	 * @var string
	 */
	private static $log_file;

	/**
	 * Maximum size of the log file in bytes before it gets rotated.
	 * Default is 5MB (5242880 bytes).
	 *
	 * @var int
	 */
	private static $max_size = 5242880; // 5MB

	/**
	 * Indicates whether the logging system has been initialized.
	 *
	 * @var bool
	 */
	private static $initialized = false;

	/**
	 * Initialize the logger by setting up the log file and registering
	 * error, exception, and shutdown handlers.
	 *
	 * This method ensures the logger is initialized only once.
	 *
	 * @return void
	 */
	public static function init() {
		if ( self::$initialized ) {
			return;
		}

		self::$log_file = AUTHOR_KIT_PLUGIN_PATH . 'logs/author-kit-error-log.txt';
		self::create_log_file();

		// Register error and exception handlers
		// Functions are used for logging purpose in production.
        // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler
		set_error_handler( array( __CLASS__, 'handle_php_error' ) );
		set_exception_handler( array( __CLASS__, 'handle_exception' ) );
		register_shutdown_function( array( __CLASS__, 'handle_shutdown' ) );

		self::$initialized = true;
	}

	/**
	 * Create the log file and its directory if they do not exist.
	 * If the log file exists and exceeds the maximum allowed size,
	 * rotate it by renaming with a timestamp backup.
	 *
	 * Uses the WordPress filesystem API to handle files and directories.
	 *
	 * @return void
	 */
	private static function create_log_file() {
		global $wp_filesystem;

		if ( empty( $wp_filesystem ) ) {
			include_once ABSPATH . '/wp-admin/includes/file.php';
			WP_Filesystem();
		}

		$log_dir = dirname( self::$log_file );

		// Check if directory exists, if not, create it.
		if ( ! $wp_filesystem->is_dir( $log_dir ) ) {
			$wp_filesystem->mkdir( $log_dir, FS_CHMOD_DIR );
		}

		// Rotate log file if it exists and exceeds max size.
		if ( $wp_filesystem->exists( self::$log_file ) && $wp_filesystem->size( self::$log_file ) > self::$max_size ) {
				$backup_name = self::$log_file . '.' . self::get_date( 'Ymd_His' ) . '.bak';
			$wp_filesystem->move( self::$log_file, $backup_name );
		}

		// Create empty file if it doesn't exist.
		if ( ! $wp_filesystem->exists( self::$log_file ) ) {
			$wp_filesystem->put_contents( self::$log_file, '', FS_CHMOD_FILE );
		}
	}

	/**
	 * Sanitize a string to mask password values for logging.
	 *
	 * This method replaces any occurrence of "password=" followed by
	 * non-space or non-& characters with "password=***" to hide sensitive data.
	 *
	 * @param  string $string_value The input string to sanitize.
	 * @return string The sanitized string with passwords masked.
	 */
	private static function sanitize_for_log( $string_value ) {
		return preg_replace( '/password=[^&\s]+/', 'password=***', $string_value );
	}

	/**
	 * Retrieve a formatted date string.
	 *
	 * Uses wp_date() if available (WP 5.3+), otherwise falls back to date_i18n() for compatibility.
	 *
	 * @param string $format Date format string.
	 * @return string Formatted date.
	 */
	private static function get_date( $format ) {
		if ( function_exists( 'wp_date' ) ) {
			return wp_date( $format );
		}

		return date_i18n( $format ); // Fallback for WP < 5.3.
	}


	/**
	 * Log a message to the log file with a given context level.
	 *
	 * The message is sanitized to mask sensitive data such as passwords.
	 * Each log entry is prepended with a timestamp and context.
	 *
	 * @param mixed  $message The message or data to log. Can be any type that print_r supports.
	 * @param string $context Optional. The context or log level for the message. Default 'INFO'.
	 * @return void
	 */
	public static function log( $message, $context = 'INFO' ) {
		self::create_log_file();
		$timestamp = self::get_date( 'Y-m-d H:i:s' );
        // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
		$formatted = "[{$timestamp}] {$context}: " . self::sanitize_for_log( print_r( $message, true ) ) . PHP_EOL;
        // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
		error_log( $formatted, 3, self::$log_file );
	}

	/**
	 * Custom PHP error handler that logs plugin-related errors only.
	 *
	 * @param int    $errno   The level of the error raised.
	 * @param string $errstr  The error message.
	 * @param string $errfile The filename where the error was raised.
	 * @param int    $errline The line number where the error was raised.
	 * @return bool  False to allow normal PHP/WP error handling to continue.
	 */
	public static function handle_php_error( $errno, $errstr, $errfile, $errline ) {
		// Used in a custom error handler to selectively log errors, not for displaying errors.
		// phpcs:ignore WordPress.PHP.DevelopmentFunctions.prevent_path_disclosure_error_reporting, WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_error_reporting
		if ( ! ( error_reporting() & $errno ) ) {
			// Respect error_reporting level.
			return;
		}

		// Only log errors related to this plugin.
		if ( ! self::is_plugin_related( $errfile ) ) {
			return false;
		}

		$types = array(
			E_ERROR             => 'ERROR',
			E_WARNING           => 'WARNING',
			E_PARSE             => 'PARSE',
			E_NOTICE            => 'NOTICE',
			E_CORE_ERROR        => 'CORE_ERROR',
			E_CORE_WARNING      => 'CORE_WARNING',
			E_COMPILE_ERROR     => 'COMPILE_ERROR',
			E_COMPILE_WARNING   => 'COMPILE_WARNING',
			E_USER_ERROR        => 'USER_ERROR',
			E_USER_WARNING      => 'USER_WARNING',
			E_USER_NOTICE       => 'USER_NOTICE',
			E_RECOVERABLE_ERROR => 'RECOVERABLE_ERROR',
			E_DEPRECATED        => 'DEPRECATED',
			E_USER_DEPRECATED   => 'USER_DEPRECATED',
		);

		$type = $types[ $errno ] ?? 'UNKNOWN';
		// Generate stack trace (skip first frame to exclude current handler).
		// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace
		$trace = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS );
		array_shift( $trace ); // Remove this function from the trace.

		$trace_lines = array();

		foreach ( $trace as $index => $frame ) {
			$file     = isset( $frame['file'] ) ? $frame['file'] : '[internal function]';
			$line     = isset( $frame['line'] ) ? $frame['line'] : '';
			$function = $frame['function'] ?? '';
			$class    = $frame['class'] ?? '';
			$type     = $frame['type'] ?? '';

			$trace_lines[] = "#$index $file($line): {$class}{$type}{$function}()";
		}

		$trace_string = implode( "\n", $trace_lines );

		$message = "$errstr in $errfile on line $errline\nStack Trace:\n$trace_string";
		self::log( $message, $type );

		// Allow PHP/WP to handle it as usual.
		return false;
	}

	/**
	 * Handles uncaught exceptions and logs only those from this plugin.
	 *
	 * @param Throwable $exception The uncaught exception object.
	 * @throws Exception Rethrow the exception if is it not related to author kit.
	 * @return void
	 */
	public static function handle_exception( $exception ) {
		$file = $exception->getFile();

		// Only log if the exception originated in this plugin.
		if ( self::is_plugin_related( $file ) ) {
			$message = 'Uncaught Exception: ' . $exception->getMessage() .
			' in ' . $file .
			' on line ' . $exception->getLine();

			$trace_string = $exception->getTraceAsString();
			$full_message = $message . "\nStack Trace:\n" . $trace_string;

			self::log( $full_message, 'EXCEPTION' );
		}

		// Optional: rethrow the exception to allow WordPress to handle it too.
		// (especially useful in development/debug mode).
		if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
			throw $exception;
		}   }

	/**
	 * Handles PHP shutdown to catch fatal errors and log them only if from this plugin.
	 *
	 * Checks for the last error on shutdown, and if it is a fatal error,
	 * logs the error message along with the file and line number.
	 *
	 * @return void
	 */
	public static function handle_shutdown() {
		$error = error_get_last();

		if ( $error && in_array( $error['type'], array( E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR ), true ) ) {
			// Only log if fatal error came from this plugin.
			if ( self::is_plugin_related( $error['file'] ) ) {
				$message = "{$error['message']} in {$error['file']} on line {$error['line']}";
				self::log( $message, 'FATAL' );
			}
		}
	}

	/**
	 * Determines if a given file path is within this plugin directory.
	 *
	 * @param string $file File path to check.
	 * @return bool True if the file is part of this plugin.
	 */
	private static function is_plugin_related( $file ) {
		$plugin_path = realpath( AUTHOR_KIT_PLUGIN_PATH );
		$error_path  = realpath( $file );

		if ( ! $plugin_path || ! $error_path ) {
			// Fallback: basic string matching if realpath fails.
			return strpos( str_replace( '\\', '/', $file ), str_replace( '\\', '/', AUTHOR_KIT_PLUGIN_PATH ) ) !== false;
		}

		// Normalize slashes for consistent comparison.
		$plugin_path = trailingslashit( str_replace( '\\', '/', $plugin_path ) );
		$error_path  = str_replace( '\\', '/', $error_path );

		return strpos( $error_path, $plugin_path ) === 0;
	}

	/**
	 * Logs a WP_Error object with detailed backtrace information.
	 *
	 * This method checks if the provided argument is a WP_Error instance,
	 * retrieves the backtrace to find the origin of the error outside this class,
	 * and logs the error message along with the originating file and line number.
	 *
	 * @param WP_Error $error The WP_Error object to log.
	 *
	 * @return void
	 */
	public static function log_wp_error( $error ) {
		if ( is_wp_error( $error ) ) {
			// Used for internal error logging only; output is never exposed to the browser.
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace 
			$backtrace = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS );
			$file      = 'Unknown file';
			$line      = 'Unknown line';

			// Find the first trace outside of Author_Kit_Logger.
			foreach ( $backtrace as $trace ) {
				if ( isset( $trace['file'] )
					&& strpos( $trace['file'], 'class-author-kit-logger.php' ) === false
				) {
					$file = $trace['file'];
					$line = isset( $trace['line'] ) ? $trace['line'] : 'Unknown line';
					break;
				}
			}

			$message = sprintf(
				'%s in %s on line %s',
				$error->get_error_message(),
				$file,
				$line
			);

			self::log( $message, 'WP_ERROR' );
		}
	}

	/**
	 * Logs the current request context including the request URI and user ID.
	 *
	 * This method captures the current request URI (or marks it as 'CLI' if unavailable)
	 * and the ID of the currently logged-in user, then logs this information with the context label 'CONTEXT'.
	 *
	 * @return void
	 */
	public static function log_request_context() {
		$user_id = get_current_user_id();
		$uri     = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : 'CLI';
		self::log( "---- REQUEST: $uri | User ID: $user_id ----", 'CONTEXT' );
	}

	/**
	 * Returns the content of the log file.
	 *
	 * @return string Log file content or a message if empty/not found.
	 */
	public static function get_log_contents() {
		global $wp_filesystem;

		if ( empty( $wp_filesystem ) ) {
			include_once ABSPATH . 'wp-admin/includes/file.php';
			WP_Filesystem();
		}

		// Check if file exists.
		if ( ! $wp_filesystem->exists( self::$log_file ) ) {
			return 'Log file not found.';
		}

		$contents = $wp_filesystem->get_contents( self::$log_file );
		return ! empty( $contents ) ? $contents : 'Log file is empty.';
	}

	/**
	 * Clears the log file by overwriting it with an empty string.
	 *
	 * @return bool True on success, false on failure.
	 */
	public static function clear_log_file() {
		if ( ! file_exists( self::$log_file ) ) {
			return false;
		}

		global $wp_filesystem;

		if ( empty( $wp_filesystem ) ) {
			include_once ABSPATH . 'wp-admin/includes/file.php';
			WP_Filesystem();
		}

		// Use WP_Filesystem to put empty content into the log file.
		$result = $wp_filesystem->put_contents( self::$log_file, '', FS_CHMOD_FILE );

		return false !== $result;
	}

	/**
	 * Handles the download of the AuthorKit log file via the browser.
	 *
	 * This function checks if the log file exists, reads its content, and sends it as a downloadable plain text file.
	 * If the file doesn't exist or cannot be read, it stops execution with an error message.
	 *
	 * @return void
	 */
	public static function download_log_file() {
		global $wp_filesystem;

		if ( empty( $wp_filesystem ) ) {
			include_once ABSPATH . 'wp-admin/includes/file.php';
			WP_Filesystem();
		}

		// Check if file exists.
		if ( ! $wp_filesystem->exists( self::$log_file ) ) {
			wp_die( 'Log file not found.' );
		}

		// Get file contents.
		$log_content = $wp_filesystem->get_contents( self::$log_file );
		if ( false === $log_content ) {
			wp_die( 'Unable to read the log file.' );
		}

		// Send download headers.
		header( 'Content-Description: File Transfer' );
		header( 'Content-Type: text/plain' );
		header( 'Content-Disposition: attachment; filename="author-kit-error-log.txt"' );
		header( 'Content-Length: ' . strlen( $log_content ) );

		// Outputting content into a downloadable file.
        // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		echo $log_content;
		exit;
	}


	/**
	 * Returns the last N lines of the log file (like `tail -n` in Linux).
	 *
	 * @param  int $lines Number of lines to fetch.
	 * @return string Requested number of lines from end of log file.
	 */
	public static function get_last_log_lines( $lines = 100 ) {
		if ( ! file_exists( self::$log_file ) ) {
			return 'Log file does not exist.';
		}

		$data        = file( self::$log_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES );
		$total_lines = count( $data );

		if ( $lines > $total_lines ) {
			$lines = $total_lines;
		}

		return implode( PHP_EOL, array_slice( $data, -$lines ) );
	}


	/**
	 * Generates a detailed WordPress system report.
	 *
	 * This report includes information about:
	 * - WordPress core settings
	 * - File paths and their sizes
	 * - Active/inactive themes and plugins
	 * - Media capabilities and configuration
	 * - Server environment and PHP settings
	 * - Database size and connection info
	 * - Constants and filesystem status
	 *
	 * Uses shell commands like `du` and `exec()` to determine folder sizes,
	 * and fetches data from WordPress APIs and global settings.
	 *
	 * @global wpdb $wpdb WordPress database abstraction object.
	 * @return string A formatted text report containing all collected system data.
	 */
	public static function get_system_report() {
		global $wpdb;
		global $wp_filesystem;

		if ( empty( $wp_filesystem ) ) {
			include_once ABSPATH . '/wp-admin/includes/file.php';
			WP_Filesystem();
		}

		// Helpers.
		$get_size = function ( $path ) {
			if ( ! file_exists( $path ) ) {
				return 'not found';
			}
			if ( ! is_readable( $path ) ) {
				return 'unreadable';
			}

			$size     = 0;
			$iterator = new RecursiveIteratorIterator(
				new RecursiveDirectoryIterator( $path, FilesystemIterator::SKIP_DOTS )
			);

			foreach ( $iterator as $file ) {
				if ( $file->isFile() ) {
					$size += $file->getSize();
				}
			}

			return size_format( $size );
		};

		$auto_update_themes  = get_site_option( 'auto_update_themes', array() );
		$auto_update_plugins = get_site_option( 'auto_update_plugins', array() );

		$report  = "### wp-core ###\n\n";
		$report .= 'version: ' . get_bloginfo( 'version' ) . "\n";
		$report .= 'site_language: ' . get_locale() . "\n";
		$report .= 'user_language: ' . get_user_locale() . "\n";
		$report .= 'timezone: ' . get_option( 'timezone_string' ) . "\n";
		$report .= 'permalink: ' . get_option( 'permalink_structure' ) . "\n";
		$report .= 'https_status: ' . ( is_ssl() ? 'true' : 'false' ) . "\n";
		$report .= 'multisite: ' . ( is_multisite() ? 'true' : 'false' ) . "\n";
		$report .= 'user_registration: ' . get_option( 'users_can_register' ) . "\n";
		$report .= 'blog_public: ' . get_option( 'blog_public' ) . "\n";
		$report .= 'default_comment_status: ' . get_option( 'default_comment_status' ) . "\n";
		$report .= 'environment_type: ' . wp_get_environment_type() . "\n";
		$report .= 'user_count: ' . count_users()['total_users'] . "\n";
		$report .= 'dotorg_communication: ' . ( wp_http_supports( array( 'ssl' => true ) ) ? 'true' : 'false' ) . "\n";

		$report    .= "\n### wp-paths-sizes ###\n\n";
		$wp_path    = ABSPATH;
		$report    .= 'wordpress_path: ' . $wp_path . "\n";
		$report    .= 'wordpress_size: ' . $get_size( $wp_path ) . "\n";
		$uploads    = wp_get_upload_dir();
		$report    .= 'uploads_path: ' . $uploads['basedir'] . "\n";
		$report    .= 'uploads_size: ' . $get_size( $uploads['basedir'] ) . "\n";
		$report    .= 'themes_path: ' . get_theme_root() . "\n";
		$report    .= 'themes_size: ' . $get_size( get_theme_root() ) . "\n";
		$report    .= 'plugins_path: ' . WP_PLUGIN_DIR . "\n";
		$report    .= 'plugins_size: ' . $get_size( WP_PLUGIN_DIR ) . "\n";
		$fonts_path = $uploads['basedir'] . '/fonts';
		$report    .= 'fonts_path: ' . $fonts_path . "\n";
		$report    .= 'fonts_size: ' . $get_size( $fonts_path ) . "\n";

		$database_size = 0;
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Needed for system report to list table sizes and row info.
		$tables = $wpdb->get_results( 'SHOW TABLE STATUS', ARRAY_A );
		foreach ( $tables as $table ) {
			$database_size += $table['Data_length'] + $table['Index_length'];
		}
		$report .= 'database_size: ' . size_format( $database_size, 2 ) . "\n";

		$wp_size      = self::parse_size( $get_size( ABSPATH ) );
		$plugins_size = self::parse_size( $get_size( WP_PLUGIN_DIR ) );
		$themes_size  = self::parse_size( $get_size( get_theme_root() ) );
		$uploads_size = self::parse_size( $get_size( $uploads['basedir'] ) );
		$fonts_size   = self::parse_size( $get_size( $fonts_path ) );
		$total        = $wp_size + $plugins_size + $themes_size + $uploads_size + $fonts_size;
		$report      .= 'total_size: ' . size_format( $total, 2 ) . "\n";

		$theme              = wp_get_theme();
		$report            .= "\n### wp-active-theme ###\n\n";
		$report            .= 'name: ' . $theme->get( 'Name' ) . ' (' . $theme->get_stylesheet() . ")\n";
		$report            .= 'version: ' . $theme->get( 'Version' ) . "\n";
		$report            .= 'author: ' . $theme->get( 'Author' ) . "\n";
		$report            .= 'author_website: ' . $theme->get( 'AuthorURI' ) . "\n";
		$report            .= 'parent_theme: ' . ( $theme->parent() ? $theme->parent()->get( 'Name' ) : 'none' ) . "\n";
		$report            .= 'theme_features: ' . implode( ', ', array_keys( get_registered_theme_features() ) ) . "\n";
		$report            .= 'theme_path: ' . $theme->get_stylesheet_directory() . "\n";
		$auto_update_status = in_array( $theme->get_stylesheet(), $auto_update_themes, true ) ? 'Enabled' : 'Disabled';
		$report            .= "auto_update: {$auto_update_status}\n";

		$all_plugins      = get_plugins();
		$active_plugins   = get_option( 'active_plugins', array() );
		$inactive_plugins = array_diff( array_keys( $all_plugins ), $active_plugins );

		$themes  = wp_get_themes();
		$report .= "\n### wp-themes-inactive (" . ( count( $themes ) - 1 ) . ") ###\n\n"; // excluding the active one.
		foreach ( $themes as $slug => $t ) {
			if ( $slug !== $theme->get_stylesheet() ) {
				$update_status = in_array( $slug, $auto_update_themes, true ) ? 'enabled' : 'disabled';
				$report       .= $t->get( 'Name' ) . ': version: ' . $t->get( 'Version' ) . ', author: ' . $t->get( 'Author' ) . ", Auto-updates {$update_status}\n";
			}
		}

		$report .= "\n### wp-plugins-active (" . count( $active_plugins ) . ") ###\n\n";
		foreach ( $active_plugins as $plugin ) {
			$data          = $all_plugins[ $plugin ];
			$update_status = in_array( $plugin, $auto_update_plugins, true ) ? 'enabled' : 'disabled';
			$report       .= $data['Name'] . ': version: ' . $data['Version'] . ', author: ' . $data['Author'] . ", Auto-updates {$update_status}\n";
		}

		$report .= "\n### wp-plugins-inactive (" . count( $inactive_plugins ) . ") ###\n\n";
		foreach ( $inactive_plugins as $plugin ) {
			$data          = $all_plugins[ $plugin ];
			$update_status = in_array( $plugin, $auto_update_plugins, true ) ? 'enabled' : 'disabled';
			$report       .= $data['Name'] . ': version: ' . $data['Version'] . ', author: ' . $data['Author'] . ", Auto-updates {$update_status}\n";
		}

		$report .= "\n### wp-media ###\n\n";
		$report .= 'image_editor: ' . ( wp_image_editor_supports( array( 'methods' => array( 'resize' ) ) ) ? 'true' : 'false' ) . "\n";
		$report .= 'imagick_module_version: ' . ( extension_loaded( 'imagick' ) ? phpversion( 'imagick' ) : 'Not available' ) . "\n";
		$report .= 'file_uploads: ' . ini_get( 'file_uploads' ) . "\n";
		$report .= 'post_max_size: ' . ini_get( 'post_max_size' ) . "\n";
		$report .= 'upload_max_filesize: ' . ini_get( 'upload_max_filesize' ) . "\n";
		$report .= 'max_effective_size: ' . max( (int) ini_get( 'upload_max_filesize' ), (int) ini_get( 'post_max_size' ) ) . " MB\n";
		$report .= 'max_file_uploads: ' . ini_get( 'max_file_uploads' ) . "\n";
		$report .= 'gd_version: ' . ( function_exists( 'gd_info' ) ? gd_info()['GD Version'] : 'not available' ) . "\n";

		$ghostscript_version = 'not available';
		if ( function_exists( 'exec' ) && ! in_array( 'exec', array_map( 'trim', explode( ',', ini_get( 'disable_functions' ) ) ), true ) ) {
			// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.system_calls_exec -- Used safely after function_exists() and disabled-functions check
			exec( 'gs --version 2>&1', $gs_output, $gs_result );

			if ( 0 === $gs_result && ! empty( $gs_output[0] ) ) {
				$ghostscript_version = trim( $gs_output[0] );
			}
		}
		$report .= "ghostscript_version: {$ghostscript_version}\n";

		$report .= "\n### wp-server ###\n\n";
		$report .= 'server_architecture: ' . php_uname() . "\n";
		$report .= 'httpd_software: ' . ( isset( $_SERVER['SERVER_SOFTWARE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : 'unknown' ) . "\n";
		$report .= 'php_version: ' . PHP_VERSION . ( PHP_INT_SIZE === 8 ? ' 64bit' : ' 32bit' ) . "\n";
		$report .= 'php_sapi: ' . php_sapi_name() . "\n";
		$report .= 'max_input_variables: ' . ini_get( 'max_input_vars' ) . "\n";
		$report .= 'time_limit: ' . ini_get( 'max_execution_time' ) . "\n";
		$report .= 'memory_limit: ' . ini_get( 'memory_limit' ) . "\n";
		$report .= 'max_input_time: ' . ini_get( 'max_input_time' ) . "\n";
		$report .= 'upload_max_filesize: ' . ini_get( 'upload_max_filesize' ) . "\n";
		$report .= 'php_post_max_size: ' . ini_get( 'post_max_size' ) . "\n";
		$report .= 'curl_version: ' . curl_version()['version'] . ' ' . curl_version()['ssl_version'] . "\n";
		$report .= 'suhosin: ' . ( extension_loaded( 'suhosin' ) ? 'true' : 'false' ) . "\n";
		$report .= 'imagick_availability: ' . ( extension_loaded( 'imagick' ) ? 'true' : 'false' ) . "\n";
		$report .= 'pretty_permalinks: ' . ( got_url_rewrite() ? 'true' : 'false' ) . "\n";

		$htaccess_file   = ABSPATH . '.htaccess';
		$has_extra_rules = false;

		if ( $wp_filesystem->exists( $htaccess_file ) ) {
			$htaccess_contents = $wp_filesystem->get_contents( $htaccess_file );

			$begin = strpos( $htaccess_contents, '# BEGIN WordPress' );
			$end   = strpos( $htaccess_contents, '# END WordPress' );

			if ( false !== $begin && false !== $end && $end > $begin ) {
				$before      = substr( $htaccess_contents, 0, $begin );
				$after       = substr( $htaccess_contents, $end + strlen( '# END WordPress' ) );
				$extra_rules = trim( $before . $after );

				if ( ! empty( $extra_rules ) ) {
					$has_extra_rules = true;
				} else {
					// Optional: Check for common extra rule patterns if WordPress block exists but no other content detected.
					$extra_patterns = array( 'mod_pagespeed', 'ExpiresActive On', 'Header set', 'RedirectMatch', 'RewriteCond %{HTTP_USER_AGENT}' );
					foreach ( $extra_patterns as $pattern ) {
						if ( stripos( $htaccess_contents, $pattern ) !== false ) {
							$has_extra_rules = true;
							break;
						}
					}
				}
			} else {
				// If no WordPress block is found, assume custom rules exist.
				$has_extra_rules = true;
			}
		} else {
			// If .htaccess doesn't exist, assume no extra rules.
			$has_extra_rules = false;
		}

		$report .= 'htaccess_extra_rules: ' . ( $has_extra_rules ? 'true' : 'false' ) . "\n";

		$robots_txt_path       = ABSPATH . 'robots.txt';
		$static_robotstxt_file = file_exists( $robots_txt_path );
		$report               .= 'static_robotstxt_file: ' . ( $static_robotstxt_file ? 'true' : 'false' ) . "\n";
		$report               .= 'current: ' . current_time( 'c' ) . "\n";
		$report               .= 'utc-time: ' . gmdate( 'r' ) . "\n";
		$report               .= 'server-time: ' . self::get_date( 'c' ) . "\n";

		$report .= "\n### wp-database ###\n\n";
		$report .= 'extension: ' . $wpdb->use_mysqli ? 'mysqli' : 'mysql' . "\n";
		$report .= 'server_version: ' . $wpdb->db_version() . "\n";
		// phpcs:ignore WordPress.DB.RestrictedFunctions.mysql_mysqli_get_client_info
		$report .= 'client_version: ' . mysqli_get_client_info() . "\n";
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$report .= 'max_allowed_packet: ' . $wpdb->get_var( "SHOW VARIABLES LIKE 'max_allowed_packet'", 1 ) . "\n";
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$report .= 'max_connections: ' . $wpdb->get_var( "SHOW VARIABLES LIKE 'max_connections'", 1 ) . "\n";

		$report .= "\n### wp-constants ###\n\n";
		$consts  = array(
			'WP_HOME',
			'WP_SITEURL',
			'WP_CONTENT_DIR',
			'WP_PLUGIN_DIR',
			'WP_MEMORY_LIMIT',
			'WP_MAX_MEMORY_LIMIT',
			'WP_DEBUG',
			'WP_DEBUG_DISPLAY',
			'WP_DEBUG_LOG',
			'SCRIPT_DEBUG',
			'WP_CACHE',
			'CONCATENATE_SCRIPTS',
			'COMPRESS_SCRIPTS',
			'COMPRESS_CSS',
			'WP_ENVIRONMENT_TYPE',
			'WP_DEVELOPMENT_MODE',
			'DB_CHARSET',
			'DB_COLLATE',
		);
		foreach ( $consts as $const ) {
			$report .= "$const: " . ( defined( $const ) ? constant( $const ) : 'undefined' ) . "\n";
		}

		$report  .= "\n### wp-filesystem ###\n\n";
		$fs_paths = array(
			'wordpress'  => ABSPATH,
			'wp-content' => WP_CONTENT_DIR,
			'uploads'    => $uploads['basedir'],
			'plugins'    => WP_PLUGIN_DIR,
			'themes'     => get_theme_root(),
			'fonts'      => $fonts_path,
		);
		foreach ( $fs_paths as $name => $path ) {
			$status  = ( ! $wp_filesystem->exists( $path ) ? 'does not exist' : ( $wp_filesystem->is_writable( $path ) ? 'writable' : 'not writable' ) );
			$report .= "$name: $status\n";
		}

		return $report;
	}

	/**
	 * Downloads the system report as a plain text file.
	 *
	 * Outputs the report with appropriate headers and ends execution.
	 *
	 * @return void
	 */
	public static function download_system_report() {
		$report = self::get_system_report();
		header( 'Content-Type: text/plain' );
		header( 'Content-Disposition: attachment; filename="author-kit-system-report.txt"' );
		header( 'Content-Length: ' . strlen( $report ) );
		echo esc_html( $report );
		exit;
	}

	/**
	 * Converts a size string (e.g., 256M, 1G) to bytes.
	 *
	 * @param string $size_string Size with optional unit (K, M, G).
	 * @return float Size in bytes.
	 */
	public static function parse_size( $size_string ) {
		$unit = strtoupper( substr( trim( $size_string ), -1 ) );
		$size = (float) $size_string;

		switch ( $unit ) {
			case 'G':
				return $size * 1024 * 1024 * 1024;
			case 'M':
				return $size * 1024 * 1024;
			case 'K':
				return $size * 1024;
			default:
				return $size; // Already in bytes.
		}
	}
}

// Initialize logger at plugin load.
Author_Kit_Logger::init();
