<?php
/**
 * SSH failure monitor, PHP error monitor, and PHP-FPM worker monitor.
 *
 * @package CloudScale_DevTools
 */

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

class CSDT_Monitor {

    /**
     * Central exception reporter: error_log + rate-limited Telegram critical alert.
     *
     * Safe to call from any context including shutdown.  Deduplicates within
     * the same request (static flag) and across requests (15-min transient).
     *
     * @param \Throwable $e   The caught exception or error.
     * @param string     $ctx Human-readable context string (usually __METHOD__).
     */
    public static function log_exception( \Throwable $e, string $ctx ): void {
        $msg = '[CSDT] ' . $ctx . ': ' . $e->getMessage()
            . ' in ' . $e->getFile() . ':' . $e->getLine();
        error_log( $msg ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log

        static $sent = array();
        $key         = 'csdt_ex_' . substr( md5( $ctx ), 0, 8 );
        if ( isset( $sent[ $key ] ) ) {
			return; }
        $sent[ $key ] = true;

        if ( ! function_exists( 'get_transient' ) || get_transient( $key ) ) {
			return; }
        if ( ! class_exists( 'CloudScale_Telegram' ) || ! CloudScale_Telegram::is_configured() ) {
			return; }
        if ( function_exists( 'set_transient' ) ) {
            set_transient( $key, 1, 15 * MINUTE_IN_SECONDS );
        }

        $site  = function_exists( 'get_bloginfo' ) ? ( wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ) ?: home_url() ) : gethostname();
        $ts    = function_exists( 'wp_date' ) ? wp_date( 'Y-m-d H:i:s T' ) : gmdate( 'Y-m-d H:i:s T' );
        $trace = implode( "\n", array_slice( explode( "\n", $e->getTraceAsString() ), 0, 8 ) );
        CloudScale_Telegram::send(
            "CSDT Exception — {$site}\nTime: {$ts}\nContext: {$ctx}\n\n"
            . get_class( $e ) . ': ' . $e->getMessage()
            . "\nFile: " . $e->getFile() . ':' . $e->getLine()
            . "\n\nTrace:\n" . $trace,
            'CSDT Error',
            'critical'
        );
    }

    public static function ajax_ssh_fix_permissions(): void {
        check_ajax_referer( CloudScale_DevTools::SECURITY_NONCE, 'nonce' );
        if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( 'Unauthorized', 403 ); }

        try {
            // Shell-level group management (usermod) cannot run from PHP without exec().
            // Return the manual commands and attempt a PHP-FPM signal reload where possible.
            $manual_cmds = array(
                'sudo usermod -a -G adm www-data',
                'sudo systemctl restart php-fpm   # or php8.2-fpm / php8.3-fpm',
            );

            // Attempt SIGUSR2 to reload PHP-FPM in case the group change was done manually.
            $pid_files = glob( '/var/run/php/php*-fpm.pid' );
            if ( $pid_files ) {
                $pid = (int) file_get_contents( $pid_files[0] ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- reading local OS PID file
                if ( $pid > 0 && function_exists( 'posix_kill' ) ) {
					posix_kill( $pid, SIGUSR2 ); }
            }

            // Re-check readability after attempting restart.
            clearstatcache( true );
            $readable = false;
            foreach ( array( '/var/log/auth.log', '/var/log/secure', '/var/log/messages' ) as $p ) {
                if ( is_readable( $p ) ) {
					$readable = true;
					break; }
            }

            wp_send_json_success(
                array(
					'readable'    => $readable,
					'manual_cmds' => $manual_cmds,
					'note'        => $readable
						? 'Auth log is readable. If it was not before, run the manual commands below and restart PHP-FPM.'
						: 'Auth log is not yet readable. Run the manual commands below in your terminal, then reload.',
                )
            );
        } catch ( \Throwable $e ) {
            self::log_exception( $e, __METHOD__ );
            wp_send_json_error( 'Internal error', 500 );
        }
    }

    public static function ajax_ssh_log_clear(): void {
        check_ajax_referer( CloudScale_DevTools::SECURITY_NONCE, 'nonce' );
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_send_json_error( 'Unauthorized', 403 );
        }
        try {
            delete_option( 'csdt_ssh_monitor_alert_log' );
            wp_send_json_success();
        } catch ( \Throwable $e ) {
            self::log_exception( $e, __METHOD__ );
            wp_send_json_error( 'Internal error', 500 );
        }
    }

    public static function ajax_ssh_monitor_save(): void {
        check_ajax_referer( CloudScale_DevTools::SECURITY_NONCE, 'nonce' );
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_send_json_error( 'Unauthorized', 403 );
        }
        try {
            $enabled   = ( sanitize_key( wp_unslash( $_POST['enabled'] ?? '0' ) ) ) === '1' ? '1' : '0';
            $threshold = max( 1, min( 1000, absint( wp_unslash( $_POST['threshold'] ?? 10 ) ) ) );
            update_option( 'csdt_ssh_monitor_enabled', $enabled, true );
            update_option( 'csdt_ssh_monitor_threshold', $threshold, true );

            if ( $enabled === '1' ) {
                if ( ! wp_next_scheduled( 'csdt_ssh_monitor' ) ) {
                    wp_schedule_event( time() + 60, 'csdt_every_1min', 'csdt_ssh_monitor' );
                }
            } else {
                wp_clear_scheduled_hook( 'csdt_ssh_monitor' );
            }
            wp_send_json_success();
        } catch ( \Throwable $e ) {
            self::log_exception( $e, __METHOD__ );
            wp_send_json_error( 'Internal error', 500 );
        }
    }

    public static function monitor_ssh_failures(): void {
        try {
            if ( get_option( 'csdt_ssh_monitor_enabled', '1' ) !== '1' ) {
                return;
            }

            // Find a readable auth log
            $auth_log = '';
            foreach ( array( '/var/log/auth.log', '/var/log/secure', '/var/log/messages' ) as $p ) {
                if ( is_readable( $p ) ) {
					$auth_log = $p;
					break; }
            }
            if ( ! $auth_log ) {
                return; // log not accessible, silently skip
            }

            // Read the last 256 KB (enough for several minutes of auth activity)
            // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- streaming local OS log file; WP Filesystem API lacks partial-read/seek support
            $handle = is_readable( $auth_log ) ? fopen( $auth_log, 'r' ) : false;
            if ( ! $handle ) {
                return;
            }
            fseek( $handle, 0, SEEK_END );
            $size = ftell( $handle );
            if ( $size === 0 ) {
                fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- WP_Filesystem has no equivalent; used in server-management Quick Fix features (admin-only, nonce-protected)
                return;
            }
            $read = min( 262144, $size );
            fseek( $handle, $size - $read );
            // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread -- partial read of local OS log file
            $chunk = fread( $handle, $read ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread -- WP_Filesystem has no equivalent; used in server-management Quick Fix features (admin-only, nonce-protected)
            fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- paired with fopen above

            if ( ! $chunk ) {
                return;
            }

            $window    = 60; // seconds, check the last 60 seconds on each 1-minute cron tick
            $threshold = (int) get_option( 'csdt_ssh_monitor_threshold', '10' );
            $now       = time();
            $failures  = array();

            // Match: "Failed password", "Invalid user", "Connection closed by invalid user", "authentication failure"
            $pattern = '/^(\w{3}\s+\d+\s[\d:]+)\s+\S+\s+sshd\[\d+\]:\s+(?:Failed password|Invalid user|Connection closed by invalid user|authentication failure).*/m';

            foreach ( explode( "\n", $chunk ) as $line ) {
                if ( ! preg_match( $pattern, $line, $m ) ) {
                    continue;
                }
                // Parse syslog timestamp (no year, assume current year, roll back if in future)
                $ts = strtotime( $m[1] );
                if ( $ts === false ) {
                    continue;
                }
                // Syslog has no year, if timestamp is in the future, it's last year
                if ( $ts > $now + 60 ) {
                    $ts = strtotime( $m[1] . ' ' . ( (int) gmdate( 'Y' ) - 1 ) );
                }
                if ( $ts !== false && ( $now - $ts ) <= $window ) {
                    $failures[] = trim( $line );
                }
            }

            $count = count( $failures );

            // Extract targeted usernames from failure lines
            $user_counts = array();
            foreach ( $failures as $line ) {
                $username = '';
                if ( preg_match( '/Failed password for (?:invalid user )?(\S+)\s+from/i', $line, $um ) ) {
                    $username = $um[1];
                } elseif ( preg_match( '/Invalid user (\S+)\s+from/i', $line, $um ) ) {
                    $username = $um[1];
                } elseif ( preg_match( '/Connection closed by invalid user (\S+)\s/i', $line, $um ) ) {
                    $username = $um[1];
                } elseif ( preg_match( '/\buser=(\S+)/', $line, $um ) ) {
                    $username = $um[1];
                }
                if ( $username && $username !== 'for' && $username !== 'by' ) {
                    $user_counts[ $username ] = ( $user_counts[ $username ] ?? 0 ) + 1;
                }
            }

            // Store recent failure data for the Quick Fixes panel display
            update_option(
                'csdt_ssh_monitor_last_check',
                array(
					'ts'    => $now,
					'count' => $count,
					'lines' => array_slice( $failures, -20 ), // keep last 20 for display
				),
				false
            );

            if ( $count < $threshold ) {
                return;
            }

            // Throttle: don't alert more than once per 5 minutes (attacks move fast)
            $last_alert = (int) get_option( 'csdt_ssh_monitor_last_alert', 0 );
            if ( ( $now - $last_alert ) < 300 ) {
                return;
            }
            update_option( 'csdt_ssh_monitor_last_alert', $now, false );

            // Append to alert log (keep last 50 events)
            arsort( $user_counts );
            $alert_log   = get_option( 'csdt_ssh_monitor_alert_log', array() );
            $alert_log[] = array(
				'ts'    => $now,
				'count' => $count,
				'users' => $user_counts,
			);
            if ( count( $alert_log ) > 50 ) {
                $alert_log = array_slice( $alert_log, -50 );
            }
            update_option( 'csdt_ssh_monitor_alert_log', $alert_log, false );

            // Build alert message
            $site      = wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ) ?: home_url();
            $admin_url = admin_url( 'tools.php?page=' . CloudScale_DevTools::TOOLS_SLUG . '&tab=security' );
            $subject   = sprintf( 'CSDT: 🚨 SSH Brute-Force, %d failures (%s)', $count, $site );
            $body      = sprintf(
                "SSH brute-force attack detected on %s\n\n%d failed SSH login attempts in the last 60 seconds.\n\nRecent failures:\n%s\n\nInstall fail2ban immediately to block attacking IPs automatically.\nQuick Fixes: %s",
                $site,
                $count,
                implode( "\n", array_slice( $failures, -5 ) ),
                $admin_url
            );

            // Email alert
            if ( get_option( 'csdt_scan_schedule_email', '1' ) === '1' ) {
                $to = get_option( 'csdt_notify_email', '' ) ?: get_option( 'admin_email' );
                wp_mail( $to, $subject, $body );
            }

            if ( get_option( 'csdt_telegram_php_errors', '1' ) === '1' && get_option( 'csdt_notify_telegram_enabled', '1' ) === '1' ) {
                CloudScale_Telegram::send( $subject . "\n\n" . $body, 'CloudScale Cyber Devtools', 'critical' );
            }
        } catch ( \Throwable $e ) {
            self::log_exception( $e, __METHOD__ );
        }
    }

    /**
     * Registers a PHP shutdown function that fires immediately on any fatal error
     * (E_ERROR, E_PARSE, etc.) and sends an instant Telegram alert — no cron delay.
     * Also writes the error to the monitored log so the 5-min cron doesn't re-alert.
     */
    public static function register_shutdown_handler(): void {
        register_shutdown_function(
            function () {
                try {
                        $err = error_get_last();
                    if ( ! $err || ! in_array( $err['type'], array( E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR ), true ) ) {
                        return;
                    }

                    $sapi    = PHP_SAPI;
                    $context = $sapi === 'cli'
                    ? 'WP-CLI argv: ' . sanitize_text_field( implode( ' ', array_slice( wp_unslash( $_SERVER['argv'] ?? array() ), 0, 10 ) ) ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- argv elements sanitized via sanitize_text_field on the joined string; WP-CLI context only
                    : sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ?? 'GET' ) ) . ' ' . sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ?? '' ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated

                    $msg = sprintf(
                        '[%s] PHP Fatal (shutdown): %s in %s on line %d | sapi:%s | %s',
                        gmdate( 'd-M-Y H:i:s T' ),
                        $err['message'],
                        $err['file'],
                        $err['line'],
                        $sapi,
                        $context
                    );

                        // Write to the monitored log immediately so the cron run doesn't re-alert.
                    if ( function_exists( 'CloudScale_DevTools::get_log_sources' ) || class_exists( 'CloudScale_DevTools' ) ) {
                        $sources  = CloudScale_DevTools::get_log_sources();
                        $log_path = $sources['wp_debug']['path'] ?? '';
                    }
                    if ( empty( $log_path ) ) {
                        $log_path = (string) ini_get( 'error_log' );
                    }
                    if ( $log_path ) {
                     // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
                        file_put_contents( $log_path, $msg . "\n", FILE_APPEND | LOCK_EX ); // phpcs:ignore WordPress.WP.AlternativeFunctions
                    }

                    // Immediate Telegram — bypass the 5-minute cron.
                    if ( function_exists( 'get_option' )
                    && get_option( 'csdt_telegram_php_errors', '1' ) === '1'
                    && get_option( 'csdt_notify_telegram_enabled', '1' ) === '1'
                    && class_exists( 'CloudScale_Telegram' )
                        ) {
                            $site      = function_exists( 'get_bloginfo' ) ? ( wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ) ?: home_url() ) : gethostname();
                            $debug_url = function_exists( 'admin_url' ) ? admin_url( 'tools.php?page=' . CloudScale_DevTools::TOOLS_SLUG . '&tab=debug' ) : '';
                            $subject   = sprintf( 'CSDT: PHP Fatal (instant), %s', $site );
                            CloudScale_Telegram::send(
                                $subject . "\n\n" . $msg . ( $debug_url ? "\n\nDiagnostics: " . $debug_url : '' ),
                                'CloudScale Cyber Devtools',
                                'critical'
                            );
                    }
                } catch ( \Throwable $e ) {
                   // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
                    error_log( '[CSDT] Shutdown handler: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine() );
                }
            }
        );
    }

    public static function monitor_php_errors(): void {
        try {
			if ( get_option( 'csdt_php_error_monitor_enabled', '1' ) !== '1' ) {
				return;
			}

			$sources      = CloudScale_DevTools::get_log_sources();
			$watch_keys   = array( 'php_error', 'wp_debug' );
			$last_pos     = get_option( 'csdt_php_error_last_pos', array() );
			$new_pos      = $last_pos;
			$new_lines    = array();
			$fatal_lines  = array();
			$now          = time();
			$is_first_run = empty( $last_pos );

			foreach ( $watch_keys as $key ) {
				if ( empty( $sources[ $key ]['path'] ) ) {
					continue;
				}
				$path = $sources[ $key ]['path'];
				if ( ! file_exists( $path ) || ! is_readable( $path ) ) {
					continue;
				}

				$size            = filesize( $path );
				$saved           = isset( $last_pos[ $key ] ) ? (int) $last_pos[ $key ] : null;
				$new_pos[ $key ] = $size;

				// First run or file truncated, just record position, don't alert
				if ( $saved === null || $saved > $size ) {
					continue;
				}

				$unread = $size - $saved;
				if ( $unread <= 0 ) {
					continue;
				}

				// Cap at 128 KB of new content per source to avoid memory issues
				$read_bytes = min( $unread, 131072 );
				// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- streaming local log file; WP Filesystem API lacks seek/partial-read support
				$handle = is_readable( $path ) ? fopen( $path, 'rb' ) : false;
				if ( ! $handle ) {
					continue;
				}
				fseek( $handle, $size - $read_bytes );
				// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread -- partial read of local log file
				$chunk = fread( $handle, $read_bytes ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread -- WP_Filesystem has no equivalent; used in server-management Quick Fix features (admin-only, nonce-protected)
				fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- paired with fopen above

				if ( ! $chunk ) {
					continue;
				}

				foreach ( explode( "\n", $chunk ) as $line ) {
					$line = trim( $line );
					if ( ! $line ) {
						continue;
					}
					$lower = strtolower( $line );
					if ( strpos( $lower, 'fatal' ) !== false || strpos( $lower, 'critical' ) !== false ) {
						$fatal_lines[] = $line;
					} elseif ( strpos( $lower, 'php error' ) !== false || preg_match( '/\bPHP (?:Warning|Parse error|Error)\b/i', $line ) ) {
						$new_lines[] = $line;
					}
				}
			}

			update_option( 'csdt_php_error_last_pos', $new_pos, false );

			if ( $is_first_run || ( empty( $fatal_lines ) && empty( $new_lines ) ) ) {
				return;
			}

			$threshold = (int) get_option( 'csdt_php_error_monitor_threshold', '1' );
			$has_alert = ! empty( $fatal_lines ) || count( $new_lines ) >= $threshold;

			if ( ! $has_alert ) {
				return;
			}

			// Throttle: max one alert per 15 minutes
			$last_alert = (int) get_option( 'csdt_php_error_monitor_last_alert', 0 );
			if ( ( $now - $last_alert ) < 900 ) {
				return;
			}
			update_option( 'csdt_php_error_monitor_last_alert', $now, false );

			$site      = wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ) ?: home_url();
			$debug_url = admin_url( 'tools.php?page=' . CloudScale_DevTools::TOOLS_SLUG . '&tab=debug' );
			$is_fatal  = ! empty( $fatal_lines );
			$all_new   = array_merge( $fatal_lines, $new_lines );
			$sapi_ctx  = PHP_SAPI === 'cli'
            ? 'sapi:cli argv: ' . sanitize_text_field( implode( ' ', array_slice( wp_unslash( $_SERVER['argv'] ?? array() ), 0, 8 ) ) ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- argv elements sanitized via sanitize_text_field on the joined string; WP-CLI context only
            : 'sapi:' . PHP_SAPI . ' ' . sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ?? '' ) ) . ' ' . sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ?? '' ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
			$excerpt   = implode( "\n", array_slice( $all_new, 0, 20 ) );

			if ( $is_fatal ) {
				$subject  = sprintf( 'CSDT: PHP Fatal, %s', $site );
				$priority = 'urgent';
				$tags     = 'rotating_light,computer';
			} else {
				$subject  = sprintf( 'CSDT: %d PHP error%s, %s', count( $new_lines ), count( $new_lines ) === 1 ? '' : 's', $site );
				$priority = 'high';
				$tags     = 'warning,computer';
			}

			$body = sprintf(
                "%s on %s\n\n%s\n\nRecent entries:\n%s\n\nOpen Diagnostics to analyze: %s",
                $subject,
                home_url(),
                $sapi_ctx,
                $excerpt,
                $debug_url
			);

			// Email
			add_filter( 'wp_mail_content_type', array( 'CSDT_Login', 'email_content_type_html' ) );
			wp_mail( get_option( 'admin_email' ), $subject, nl2br( esc_html( $body ) ) );
			remove_filter( 'wp_mail_content_type', array( 'CSDT_Login', 'email_content_type_html' ) );

			if ( get_option( 'csdt_telegram_php_errors', '1' ) === '1' && get_option( 'csdt_notify_telegram_enabled', '1' ) === '1' ) {
				$level = $is_fatal ? 'critical' : 'error';
				CloudScale_Telegram::send( $subject . "\n\n" . $sapi_ctx . "\n\n" . $excerpt . "\n\nDiagnostics: " . $debug_url, 'CloudScale Cyber Devtools', $level );
			}

			update_option(
                'csdt_php_error_monitor_last_trigger',
                array(
					'ts'      => $now,
					'fatal'   => count( $fatal_lines ),
					'errors'  => count( $new_lines ),
					'excerpt' => $excerpt,
                ),
                false
            );

        } catch ( \Throwable $e ) {
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
            error_log( 'CSDT monitor_php_errors exception: ' . $e->getMessage() );
        }
    }

    public static function ajax_php_error_monitor_save(): void {
        check_ajax_referer( CloudScale_DevTools::DEBUG_NONCE, 'nonce' );
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_send_json_error( 'Unauthorized', 403 );
        }
        try {
            $enabled   = isset( $_POST['enabled'] ) && sanitize_key( wp_unslash( $_POST['enabled'] ) ) === '1' ? '1' : '0';
            $threshold = max( 1, min( 50, absint( wp_unslash( $_POST['threshold'] ?? 1 ) ) ) );
            update_option( 'csdt_php_error_monitor_enabled', $enabled, true );
            update_option( 'csdt_php_error_monitor_threshold', (string) $threshold, true );
            if ( $enabled === '1' ) {
                if ( ! wp_next_scheduled( 'csdt_php_error_monitor' ) ) {
                    wp_schedule_event( time() + 300, 'csdt_every_5min', 'csdt_php_error_monitor' );
                }
                // Reset position so first run doesn't re-scan old log
                delete_option( 'csdt_php_error_last_pos' );
            } else {
                wp_clear_scheduled_hook( 'csdt_php_error_monitor' );
            }
            wp_send_json_success(
                array(
					'enabled'   => $enabled,
					'threshold' => $threshold,
                )
            );
        } catch ( \Throwable $e ) {
            self::log_exception( $e, __METHOD__ );
            wp_send_json_error( 'Internal error', 500 );
        }
    }

    public static function ajax_fpm_monitor_save(): void {
        check_ajax_referer( CloudScale_DevTools::FPM_NONCE, 'nonce' );
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_send_json_error( 'Unauthorized', 403 );
        }
        try {
            $enabled          = isset( $_POST['enabled'] ) && sanitize_key( wp_unslash( $_POST['enabled'] ) ) === '1' ? '1' : '0';
            $threshold        = max( 1, min( 30, absint( wp_unslash( $_POST['threshold'] ?? 3 ) ) ) );
            $cooldown         = max( 60, min( 86400, absint( wp_unslash( $_POST['cooldown'] ?? 1800 ) ) ) );
            $timeout          = max( 1, min( 30, absint( wp_unslash( $_POST['probe_timeout'] ?? 5 ) ) ) );
            $auto_restart     = isset( $_POST['auto_restart'] ) && sanitize_key( wp_unslash( $_POST['auto_restart'] ) ) === '1' ? '1' : '0';
            $restart_cooldown = max( 60, min( 86400, absint( wp_unslash( $_POST['restart_cooldown'] ?? 1200 ) ) ) );
            update_option( 'csdt_fpm_enabled', $enabled, false );
            update_option( 'csdt_fpm_threshold', (string) $threshold, false );
            update_option( 'csdt_fpm_cooldown', (string) $cooldown, false );
            update_option( 'csdt_fpm_probe_url', esc_url_raw( wp_unslash( (string) ( $_POST['probe_url'] ?? 'http://localhost:8082/' ) ) ), false );
            update_option( 'csdt_fpm_probe_timeout', (string) $timeout, false );
            update_option( 'csdt_fpm_wp_container', sanitize_text_field( wp_unslash( (string) ( $_POST['wp_container'] ?? 'pi_wordpress' ) ) ), false );
            update_option( 'csdt_fpm_db_container', sanitize_text_field( wp_unslash( (string) ( $_POST['db_container'] ?? 'pi_mariadb' ) ) ), false );
            update_option( 'csdt_fpm_auto_restart', $auto_restart, false );
            update_option( 'csdt_fpm_restart_cooldown', (string) $restart_cooldown, false );
            wp_send_json_success();
        } catch ( \Throwable $e ) {
            self::log_exception( $e, __METHOD__ );
            wp_send_json_error( 'Internal error', 500 );
        }
    }

    public static function ajax_fpm_worker_status(): void {
        check_ajax_referer( CloudScale_DevTools::FPM_NONCE, 'nonce' );
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_send_json_error( 'Unauthorized', 403 );
        }
        try {
            $probe_url  = rtrim( get_option( 'csdt_fpm_probe_url', 'http://localhost:8082/' ), '/' );
            $status_url = $probe_url . '/fpm-status';
            $response   = wp_remote_get(
                $status_url,
                array(
					'timeout'   => 5,
					'sslverify' => false,
                )
            );
            if ( is_wp_error( $response ) ) {
                wp_send_json_error( array( 'message' => 'Could not reach ' . $status_url . ': ' . $response->get_error_message() . '. Ensure pm.status_path = /fpm-status in www.conf and a matching nginx location.' ) );
            }
            $body = wp_remote_retrieve_body( $response );
            $code = wp_remote_retrieve_response_code( $response );
            if ( (int) $code !== 200 || empty( $body ) ) {
                wp_send_json_error( array( 'message' => 'FPM status returned HTTP ' . $code . '. Enable pm.status_path = /fpm-status in www.conf and add a nginx location block for /fpm-status.' ) );
            }
            $parse = static function ( string $key ) use ( $body ): ?int {
                if ( preg_match( '/^' . preg_quote( $key, '/' ) . ':\s*(\d+)/m', $body, $m ) ) {
                    return (int) $m[1];
                }
                return null;
            };
            wp_send_json_success(
                array(
					'active' => $parse( 'active processes' ),
					'idle'   => $parse( 'idle processes' ),
					'total'  => $parse( 'total processes' ),
                )
            );
        } catch ( \Throwable $e ) {
            self::log_exception( $e, __METHOD__ );
            wp_send_json_error( 'Internal error', 500 );
        }
    }

    public static function ajax_fpm_worker_detail(): void {
        check_ajax_referer( CloudScale_DevTools::FPM_NONCE, 'nonce' );
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_send_json_error( 'Unauthorized', 403 );
        }
        try {
            $probe_url  = rtrim( get_option( 'csdt_fpm_probe_url', 'http://localhost:8082/' ), '/' );
            $status_url = $probe_url . '/fpm-status?full&json';
            $response   = wp_remote_get(
                $status_url,
                array(
					'timeout'   => 5,
					'sslverify' => false,
                )
            );
            if ( is_wp_error( $response ) ) {
                wp_send_json_error( array( 'message' => $response->get_error_message() ) );
            }
            $code = (int) wp_remote_retrieve_response_code( $response );
            $body = wp_remote_retrieve_body( $response );
            if ( $code !== 200 || empty( $body ) ) {
                wp_send_json_error( array( 'message' => 'HTTP ' . $code ) );
            }

            // Try JSON first (supported since PHP-FPM 7.x with ?json)
            $json = json_decode( $body, true );
            if ( $json && isset( $json['processes'] ) ) {
                wp_send_json_success(
                    array(
						'pool'     => $json['pool'] ?? '',
						'pm'       => $json['process manager'] ?? '',
						'accepted' => $json['accepted conn'] ?? 0,
						'workers'  => array_map(
                            static function ( array $p ): array {
								return array(
									'pid'      => $p['pid'] ?? 0,
									'state'    => $p['state'] ?? '',
									'reqs'     => $p['requests'] ?? 0,
									'since'    => $p['start since'] ?? 0,
									'duration' => $p['request duration'] ?? 0,
									'method'   => $p['request method'] ?? '',
									'uri'      => ( $p['request uri'] ?? '' ) . ( ! empty( $p['query string'] ) ? '?' . $p['query string'] : '' ),
									'script'   => basename( $p['script'] ?? '' ),
									'cpu'      => $p['last request cpu'] ?? 0,
									'mem'      => $p['last request memory'] ?? 0,
									'user'     => $p['user'] ?? '-',
								);
							},
							$json['processes']
                        ),
                    )
                );
            }

            // Fall back to text parsing
            $sections  = preg_split( '/\*{8,}/', $body );
            $pool_info = array();
            $workers   = array();
            if ( ! empty( $sections[0] ) ) {
                foreach ( explode( "\n", $sections[0] ) as $line ) {
                    if ( preg_match( '/^([^:]+):\s*(.+)$/', trim( $line ), $m ) ) {
                        $pool_info[ trim( $m[1] ) ] = trim( $m[2] );
                    }
                }
            }
            foreach ( array_slice( $sections, 1 ) as $section ) {
                $w = array();
                foreach ( explode( "\n", $section ) as $line ) {
                    if ( preg_match( '/^([^:]+):\s*(.*)$/', trim( $line ), $m ) ) {
                        $w[ trim( $m[1] ) ] = trim( $m[2] );
                    }
                }
                if ( ! empty( $w['pid'] ) ) {
                    $uri       = $w['request URI'] ?? '';
                    $qs        = $w['query string'] ?? '';
                    $workers[] = array(
                        'pid'      => (int) $w['pid'],
                        'state'    => $w['state'] ?? '',
                        'reqs'     => (int) ( $w['requests'] ?? 0 ),
                        'since'    => (int) ( $w['start since'] ?? 0 ),
                        'duration' => (int) ( $w['request duration'] ?? 0 ),
                        'method'   => $w['request method'] ?? '',
                        'uri'      => $uri . ( $qs ? '?' . $qs : '' ),
                        'script'   => basename( $w['script'] ?? '' ),
                        'cpu'      => (float) ( $w['last request cpu'] ?? 0 ),
                        'mem'      => (int) ( $w['last request memory'] ?? 0 ),
                        'user'     => $w['user'] ?? '-',
                    );
                }
            }
            wp_send_json_success(
                array(
					'pool'     => $pool_info['pool'] ?? '',
					'pm'       => $pool_info['process manager'] ?? '',
					'accepted' => (int) ( $pool_info['accepted conn'] ?? 0 ),
					'workers'  => $workers,
                )
            );
        } catch ( \Throwable $e ) {
            self::log_exception( $e, __METHOD__ );
            wp_send_json_error( 'Internal error', 500 );
        }
    }

    public static function ajax_fpm_setup_detect(): void {
        check_ajax_referer( CloudScale_DevTools::FPM_NONCE, 'nonce' );
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_send_json_error( 'Unauthorized', 403 );
        }

        try {
            // Locate www.conf
            $conf_candidates   = array(
                '/usr/local/etc/php-fpm.d/www.conf',
                '/etc/php-fpm.d/www.conf',
                '/etc/php/8.4/fpm/pool.d/www.conf',
                '/etc/php/8.3/fpm/pool.d/www.conf',
                '/etc/php/8.2/fpm/pool.d/www.conf',
                '/etc/php/8.1/fpm/pool.d/www.conf',
                '/etc/php/8.0/fpm/pool.d/www.conf',
                '/etc/php/7.4/fpm/pool.d/www.conf',
            );
            $www_conf          = null;
            $www_conf_writable = false;
            $status_path_set   = false;
            foreach ( $conf_candidates as $p ) {
                if ( file_exists( $p ) && is_readable( $p ) ) {
                    $www_conf          = $p;
                    $www_conf_writable = is_writable( $p ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable -- WP_Filesystem has no equivalent; used in server-management Quick Fix features (admin-only, nonce-protected)
                    $content           = (string) file_get_contents( $p ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- reading local PHP-FPM config file
                    $status_path_set   = (bool) preg_match( '/^\s*pm\.status_path\s*=/m', $content );
                    break;
                }
            }

            // Probe nginx candidates
            $stored           = rtrim( get_option( 'csdt_fpm_probe_url', 'http://localhost:8082/' ), '/' );
            $probes           = array_unique(
                array(
					$stored,
					'http://localhost',
					'http://localhost:80',
					'http://localhost:8080',
					'http://localhost:8082',
					'http://127.0.0.1',
                )
            );
            $nginx_url        = null;
            $fpm_status_works = false;
            foreach ( $probes as $base ) {
                $base = rtrim( $base, '/' );
                $r    = wp_remote_get(
                    $base . '/fpm-status',
                    array(
						'timeout'   => 2,
						'sslverify' => false,
                    )
                );
                if ( ! is_wp_error( $r ) && (int) wp_remote_retrieve_response_code( $r ) === 200 ) {
                    $body = wp_remote_retrieve_body( $r );
                    if ( str_contains( $body, 'active processes' ) || str_contains( $body, 'pool:' ) ) {
                        $nginx_url        = $base . '/';
                        $fpm_status_works = true;
                        break;
                    }
                }
                if ( $nginx_url === null ) {
                    $r2 = wp_remote_get(
                        $base . '/',
                        array(
							'timeout'   => 2,
							'sslverify' => false,
                        )
                    );
                    if ( ! is_wp_error( $r2 ) && (int) wp_remote_retrieve_response_code( $r2 ) > 0 ) {
                        $nginx_url = $base . '/';
                    }
                }
            }

            // Try to read the fastcgi_pass upstream from nginx config (same container only)
            $fastcgi_pass = 'php:9000'; // default guess
            foreach ( array( '/etc/nginx/sites-enabled', '/etc/nginx/conf.d', '/etc/nginx' ) as $dir ) {
                if ( ! is_dir( $dir ) ) {
					continue;
                }
                foreach ( (array) glob( $dir . '/*.conf' ) as $cf ) {
                    $nc = is_readable( $cf ) ? (string) file_get_contents( $cf ) : ''; // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- reading local nginx config file
                    if ( preg_match( '/fastcgi_pass\s+([^\s;]+)/i', $nc, $m ) ) {
                        $fastcgi_pass = $m[1];
                        break 2;
                    }
                }
            }

            wp_send_json_success(
                array(
					'www_conf'          => $www_conf,
					'www_conf_writable' => $www_conf_writable,
					'status_path_set'   => $status_path_set,
					'nginx_url'         => $nginx_url,
					'fpm_status_works'  => $fpm_status_works,
					'fastcgi_pass'      => $fastcgi_pass,
                )
            );
        } catch ( \Throwable $e ) {
            self::log_exception( $e, __METHOD__ );
            wp_send_json_error( 'Internal error', 500 );
        }
    }

    public static function ajax_fpm_setup_patch(): void {
        check_ajax_referer( CloudScale_DevTools::FPM_NONCE, 'nonce' );
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_send_json_error( 'Unauthorized', 403 );
        }

        try {
            $www_conf  = sanitize_text_field( wp_unslash( (string) ( $_POST['www_conf'] ?? '' ) ) );
            $nginx_url = esc_url_raw( wp_unslash( (string) ( $_POST['nginx_url'] ?? '' ) ) );

            $safe_paths = array(
                '/usr/local/etc/php-fpm.d/www.conf',
                '/etc/php-fpm.d/www.conf',
                '/etc/php/8.4/fpm/pool.d/www.conf',
                '/etc/php/8.3/fpm/pool.d/www.conf',
                '/etc/php/8.2/fpm/pool.d/www.conf',
                '/etc/php/8.1/fpm/pool.d/www.conf',
                '/etc/php/8.0/fpm/pool.d/www.conf',
                '/etc/php/7.4/fpm/pool.d/www.conf',
            );
            if ( ! in_array( $www_conf, $safe_paths, true ) ) {
                wp_send_json_error( array( 'message' => 'Invalid config path.' ) );
            }
            if ( ! file_exists( $www_conf ) || ! is_writable( $www_conf ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable -- WP_Filesystem has no equivalent; used in server-management Quick Fix features (admin-only, nonce-protected)
                wp_send_json_error( array( 'message' => 'www.conf not writable: ' . $www_conf ) );
            }

            $content = (string) file_get_contents( $www_conf ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- reading local PHP-FPM www.conf (path validated against allowlist)
            $patched = false;
            if ( ! preg_match( '/^\s*pm\.status_path\s*=/m', $content ) ) {
                // Insert after pm.max_spare_servers if present, else append
                if ( preg_match( '/^(pm\.max_spare_servers\s*=.*)/m', $content ) ) {
                    $content = preg_replace(
                        '/^(pm\.max_spare_servers\s*=.*)/m',
                        "$1\npm.status_path = /fpm-status",
                        $content,
                        1
                    );
                } else {
                    $content .= "\npm.status_path = /fpm-status\n";
                }
                if ( file_put_contents( $www_conf, $content ) === false ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- writing PHP-FPM www.conf (path validated against allowlist)
                    wp_send_json_error( array( 'message' => 'Could not write to ' . $www_conf ) );
                }
                $patched = true;
            }

            if ( $nginx_url ) {
                update_option( 'csdt_fpm_probe_url', rtrim( $nginx_url, '/' ) . '/', false );
            }

            // Reload php-fpm master, try PID file, then /proc scan
            $reloaded     = false;
            $reload_msg   = '';
            $reload_error = '';

            foreach ( array( '/var/run/php-fpm.pid', '/run/php-fpm.pid', '/run/php/php-fpm.pid' ) as $pid_file ) {
                if ( file_exists( $pid_file ) ) {
                    $pid = (int) trim( (string) file_get_contents( $pid_file ) ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- reading local OS PID file
                    if ( $pid > 1 && function_exists( 'posix_kill' ) && posix_kill( $pid, SIGUSR2 ) ) {
                        $reloaded   = true;
                        $reload_msg = 'Sent SIGUSR2 to php-fpm master (PID ' . $pid . ')';
                        break;
                    }
                }
            }

            if ( ! $reloaded && is_dir( '/proc' ) && function_exists( 'posix_kill' ) ) {
                $fpm_pids = array();
                foreach ( (array) glob( '/proc/[0-9]*', GLOB_ONLYDIR ) as $d ) {
                    $comm = is_readable( $d . '/comm' ) ? (string) file_get_contents( $d . '/comm' ) : ''; // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- reading /proc process name
                    if ( str_contains( trim( $comm ), 'php-fpm' ) ) {
                        $fpm_pids[] = (int) basename( $d );
                    }
                }
                if ( $fpm_pids ) {
                    sort( $fpm_pids );
                    if ( posix_kill( $fpm_pids[0], SIGUSR2 ) ) {
                        $reloaded   = true;
                        $reload_msg = 'Sent SIGUSR2 to php-fpm master (PID ' . $fpm_pids[0] . ')';
                    }
                }
            }

            if ( ! $reloaded ) {
                $reload_error = 'Auto-reload failed, run manually: kill -USR2 $(pgrep -o php-fpm)';
            }

            wp_send_json_success(
                array(
					'patched'      => $patched,
					'reloaded'     => $reloaded,
					'reload_msg'   => $reload_msg,
					'reload_error' => $reload_error,
                )
            );
        } catch ( \Throwable $e ) {
            self::log_exception( $e, __METHOD__ );
            wp_send_json_error( 'Internal error', 500 );
        }
    }

    // ── OPcache ──────────────────────────────────────────────────────────────

    public static function ajax_opcache_stats(): void {
        check_ajax_referer( CloudScale_DevTools::FPM_NONCE, 'nonce' );
        if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( 'Unauthorized', 403 ); }
        try {
            wp_send_json_success( self::get_opcache_stats() );
        } catch ( \Throwable $e ) {
            self::log_exception( $e, __METHOD__ );
            wp_send_json_error( 'Internal error', 500 );
        }
    }

    public static function ajax_opcache_flush(): void {
        // Accepts either a logged-in admin with nonce OR the stored deploy token (for deploy script).
        $token  = sanitize_text_field( wp_unslash( (string) ( $_POST['token'] ?? '' ) ) );
        $stored = get_option( 'csdt_opcache_token', '' );

        $by_token = $stored && $token && hash_equals( $stored, $token );
        if ( ! $by_token ) {
            check_ajax_referer( CloudScale_DevTools::FPM_NONCE, 'nonce' );
            if ( ! current_user_can( 'manage_options' ) ) {
				wp_send_json_error( 'Unauthorized', 403 ); }
        }

        try {
            // Use per-file invalidation instead of opcache_reset() to avoid acquiring the
            // global OPcache write-lock, which blocks all workers simultaneously and causes
            // a brief upstream timeout visible to users as ERR_CONNECTION_FAILED.
            $ok = false;
            if ( function_exists( 'opcache_invalidate' ) && function_exists( 'opcache_get_status' ) ) {
                $status = opcache_get_status( false );
                if ( ! empty( $status['scripts'] ) ) {
                    foreach ( array_keys( $status['scripts'] ) as $cached_file ) {
                        opcache_invalidate( $cached_file, true );
                    }
                    $ok = true;
                }
            }
            if ( ! $ok ) {
                $ok = function_exists( 'opcache_reset' ) && opcache_reset();
            }
            update_option( 'csdt_opcache_last_flush', time(), false );

            wp_send_json_success(
                array(
					'flushed'    => $ok,
					'stats'      => self::get_opcache_stats(),
					'flushed_at' => human_time_diff( time() ) . ' ago',
                )
            );
        } catch ( \Throwable $e ) {
            self::log_exception( $e, __METHOD__ );
            wp_send_json_error( 'Internal error', 500 );
        }
    }

    public static function ajax_lscache_purge(): void {
        $token    = sanitize_text_field( wp_unslash( (string) ( $_POST['token'] ?? '' ) ) );
        $stored   = get_option( 'csdt_opcache_token', '' );
        $by_token = $stored && $token && hash_equals( $stored, $token );
        if ( ! $by_token ) {
            check_ajax_referer( CloudScale_DevTools::FPM_NONCE, 'nonce' );
            if ( ! current_user_can( 'manage_options' ) ) {
				wp_send_json_error( 'Unauthorized', 403 ); }
        }
        try {
            do_action( 'litespeed_purge_all' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- third-party LiteSpeed hook
            wp_send_json_success( array( 'purged' => true ) );
        } catch ( \Throwable $e ) {
            self::log_exception( $e, __METHOD__ );
            wp_send_json_error( 'Internal error', 500 );
        }
    }

    private static function get_opcache_stats(): array {
        if ( ! function_exists( 'opcache_get_status' ) ) {
            return array( 'available' => false );
        }
        $s = opcache_get_status( false );
        if ( ! $s ) {
            return array( 'available' => false );
        }
        $mem   = $s['memory_usage'];
        $total = $mem['used_memory'] + $mem['free_memory'] + $mem['wasted_memory'];
        $stats = $s['opcache_statistics'];
        $last  = (int) get_option( 'csdt_opcache_last_flush', 0 );
        return array(
            'available'      => true,
            'enabled'        => $s['opcache_enabled'],
            'mem_used'       => $mem['used_memory'],
            'mem_free'       => $mem['free_memory'],
            'mem_wasted'     => $mem['wasted_memory'],
            'mem_total'      => $total,
            'cached_scripts' => $stats['num_cached_scripts'],
            'hits'           => $stats['hits'],
            'misses'         => $stats['misses'],
            'hit_rate'       => $total > 0 ? round( $stats['hits'] / max( 1, $stats['hits'] + $stats['misses'] ) * 100, 1 ) : 0,
            'last_flush'     => $last ? human_time_diff( $last ) . ' ago' : 'never',
            'last_flush_ts'  => $last,
        );
    }

    public static function register_fpm_report_route(): void {
        try {
            register_rest_route(
                'csdt/v1',
                '/fpm-report',
                array(
					'methods'             => 'POST',
					'callback'            => array( __CLASS__, 'rest_fpm_report' ),
					'permission_callback' => static function ( \WP_REST_Request $request ): bool {
						$stored = (string) get_option( 'csdt_fpm_token', '' );
						if ( $stored === '' ) {
							return false; }
						return hash_equals( $stored, sanitize_text_field( (string) $request->get_param( 'token' ) ) );
					},
				)
            );
        } catch ( \Throwable $e ) {
            self::log_exception( $e, __METHOD__ );
        }
    }

    public static function rest_fpm_report( \WP_REST_Request $request ): \WP_REST_Response {
        try {
            $type = sanitize_text_field( (string) $request->get_param( 'type' ) );
            if ( ! in_array( $type, array( 'saturated', 'recovered', 'restarted' ), true ) ) {
                $type = 'saturated';
            }
            $msg   = sanitize_text_field( (string) $request->get_param( 'msg' ) );
            $event = array(
                'ts'   => time(),
                'type' => $type,
                'msg'  => substr( $msg, 0, 200 ),
            );
            update_option( 'csdt_fpm_last_event', $event, false );
            $log = get_option( 'csdt_fpm_event_log', array() );
            if ( ! is_array( $log ) ) {
                $log = array();
            }
            array_unshift( $log, $event );
            $log = array_slice( $log, 0, 50 );
            update_option( 'csdt_fpm_event_log', $log, false );
            return new \WP_REST_Response( array( 'ok' => true ) );
        } catch ( \Throwable $e ) {
            self::log_exception( $e, __METHOD__ );
            return new \WP_Error( 'internal_error', 'Internal server error', array( 'status' => 500 ) );
        }
    }
}
