<?php
/**
 * Plugin Name: Vulnity Security
 * Description: Security monitoring and SIEM integration for WordPress
 * Version: 1.2.1
 * Author: Vulnity
 * License: GPL-2.0-or-later
 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain: vulnity
 * Domain Path: /languages
 */

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

define('VULNITY_VERSION', '1.2.1');

$vulnity_plugin_dir = plugin_dir_path(__FILE__);

if (!defined('VULNITY_PLUGIN_DIR')) {
    define('VULNITY_PLUGIN_DIR', $vulnity_plugin_dir);
}

if (!function_exists('vulnity_plugin_path')) {
    function vulnity_plugin_path($relative = '') {
        $base = plugin_dir_path(__FILE__);
        if ($relative === '') {
            return $base;
        }

        return rtrim($base, '/\\') . '/' . ltrim($relative, '/\\');
    }
}

if (!function_exists('vulnity_get_secure_storage_paths')) {
    /**
     * Resolve secure Vulnity storage paths under wp-content.
     *
     * @param string $subdir   Optional subdirectory.
     * @param string $filename Optional filename.
     *
     * @return string[]
     */
    function vulnity_get_secure_storage_paths($subdir = '', $filename = '') {
        $roots = array();

        if (defined('WP_CONTENT_DIR')) {
            $roots[] = rtrim(WP_CONTENT_DIR, '/\\') . '/vulnity-data';
        }

        if (defined('ABSPATH')) {
            $roots[] = rtrim(ABSPATH, '/\\') . '/wp-content/vulnity-data';
        }

        $paths = array();
        foreach (array_values(array_unique($roots)) as $root) {
            if (!is_string($root) || $root === '') {
                continue;
            }

            $path = rtrim($root, '/\\');
            if ($subdir !== '') {
                $path .= '/' . trim($subdir, '/\\');
            }

            if ($filename !== '') {
                $path .= '/' . ltrim($filename, '/\\');
            }

            $paths[] = $path;
        }

        return array_values(array_unique($paths));
    }
}

if (!function_exists('vulnity_get_legacy_log_file_candidates')) {
    /**
     * Return previous public log file locations for migration.
     *
     * @return string[]
     */
    function vulnity_get_legacy_log_file_candidates() {
        $candidate_paths = array();

        if (function_exists('wp_upload_dir')) {
            $uploads = wp_upload_dir(null, false);
            if (is_array($uploads) && !empty($uploads['basedir']) && is_string($uploads['basedir'])) {
                $candidate_paths[] = rtrim($uploads['basedir'], '/\\') . '/vulnity-logs/vulnity.log';
            }
        }

        if (defined('WP_CONTENT_DIR')) {
            $candidate_paths[] = rtrim(WP_CONTENT_DIR, '/\\') . '/uploads/vulnity-logs/vulnity.log';
            $candidate_paths[] = rtrim(WP_CONTENT_DIR, '/\\') . '/vulnity-logs/vulnity.log';
        }

        if (defined('ABSPATH')) {
            $candidate_paths[] = rtrim(ABSPATH, '/\\') . '/wp-content/uploads/vulnity-logs/vulnity.log';
            $candidate_paths[] = rtrim(ABSPATH, '/\\') . '/wp-content/vulnity-logs/vulnity.log';
        }

        return array_values(array_unique(array_filter($candidate_paths, 'is_string')));
    }
}

if (!function_exists('vulnity_get_legacy_log_dir_candidates')) {
    /**
     * Return legacy log directories previously used by Vulnity.
     *
     * @return string[]
     */
    function vulnity_get_legacy_log_dir_candidates() {
        $dirs = array();

        foreach (vulnity_get_legacy_log_file_candidates() as $path) {
            if (is_string($path) && $path !== '') {
                $dirs[] = dirname($path);
            }
        }

        return array_values(array_unique(array_filter($dirs, 'is_string')));
    }
}

if (!function_exists('vulnity_cleanup_legacy_runtime_directory')) {
    /**
     * Remove plugin-owned legacy files and delete the directory if it becomes empty.
     *
     * @param string $dir          Directory path.
     * @param array  $owned_files  Known plugin-owned filenames.
     *
     * @return void
     */
    function vulnity_cleanup_legacy_runtime_directory($dir, $owned_files = array()) {
        if (!is_string($dir) || $dir === '' || !is_dir($dir)) {
            return;
        }

        $owned_files = is_array($owned_files) ? $owned_files : array();
        foreach ($owned_files as $filename) {
            if (!is_string($filename) || $filename === '') {
                continue;
            }

            $file = rtrim($dir, '/\\') . '/' . ltrim($filename, '/\\');
            if (file_exists($file) && is_file($file)) {
                vulnity_delete_file($file);
            }
        }

        $remaining_entries = @scandir($dir); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
        if (!is_array($remaining_entries)) {
            return;
        }

        $remaining_entries = array_values(array_diff($remaining_entries, array('.', '..')));
        if (!empty($remaining_entries)) {
            return;
        }

        $filesystem = vulnity_get_wp_filesystem();
        if ($filesystem) {
            if (method_exists($filesystem, 'rmdir') && $filesystem->rmdir($dir, false)) {
                return;
            }

            if (method_exists($filesystem, 'delete')) {
                $filesystem->delete($dir, false, 'd');
            }
        }
    }
}

if (!function_exists('vulnity_get_log_archive_path')) {
    /**
     * Build a rotated archive path that keeps the .php extension last.
     *
     * @param string $base_path Base log file path.
     * @param int    $index     Archive index.
     *
     * @return string
     */
    function vulnity_get_log_archive_path($base_path, $index) {
        $base_path = (string) $base_path;
        $index = max(1, (int) $index);

        $dir = dirname($base_path);
        $filename = basename($base_path);
        $extension = pathinfo($filename, PATHINFO_EXTENSION);

        if ($extension !== '') {
            $basename = substr($filename, 0, -1 * (strlen($extension) + 1));
            return rtrim($dir, '/\\') . '/' . $basename . '.' . $index . '.' . $extension;
        }

        return rtrim($dir, '/\\') . '/' . $filename . '.' . $index;
    }
}

if (!function_exists('vulnity_get_log_file_header')) {
    /**
     * Header that prevents direct access to log contents over HTTP.
     *
     * @return string
     */
    function vulnity_get_log_file_header() {
        return "<?php exit; ?>\n";
    }
}

if (!function_exists('vulnity_prepare_guarded_log_contents')) {
    /**
     * Normalize log file contents so they always start with the PHP guard.
     *
     * @param string $contents Raw file contents.
     *
     * @return string
     */
    function vulnity_prepare_guarded_log_contents($contents) {
        $header = vulnity_get_log_file_header();
        $contents = is_string($contents) ? $contents : '';

        if (strpos($contents, $header) === 0) {
            return $contents;
        }

        if (strpos($contents, '<?php exit; ?>') === 0) {
            $newline_pos = strpos($contents, "\n");
            if ($newline_pos !== false) {
                $contents = substr($contents, $newline_pos + 1);
            } else {
                $contents = '';
            }
        }

        return $header . ltrim($contents, "\r\n");
    }
}

if (!function_exists('vulnity_ensure_guarded_log_file')) {
    /**
     * Ensure a Vulnity log file exists and is protected by a PHP header.
     *
     * @param string $path Log file path.
     *
     * @return bool
     */
    function vulnity_ensure_guarded_log_file($path) {
        if (!is_string($path) || $path === '') {
            return false;
        }

        if (!file_exists($path)) {
            return vulnity_write_file_contents($path, vulnity_get_log_file_header());
        }

        $header = vulnity_get_log_file_header();
        $first_line = '';

        $handle = @fopen($path, 'r'); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
        if ($handle !== false) {
            $first_line = fgets($handle);
            fclose($handle); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
        }

        if (is_string($first_line) && strpos($first_line, trim($header)) === 0) {
            return true;
        }

        $contents = file_get_contents($path);
        if ($contents === false) {
            return false;
        }

        $guarded = vulnity_prepare_guarded_log_contents($contents);
        if ($guarded === $contents) {
            return true;
        }

        return vulnity_write_file_contents($path, $guarded);
    }
}

if (!function_exists('vulnity_maybe_migrate_legacy_log_files')) {
    /**
     * Best-effort migration from legacy public log paths to guarded PHP files.
     *
     * @param string $target_path New secure log file path.
     *
     * @return void
     */
    function vulnity_maybe_migrate_legacy_log_files($target_path) {
        static $processed_targets = array();

        if (!is_string($target_path) || $target_path === '') {
            return;
        }

        if (isset($processed_targets[$target_path])) {
            return;
        }
        $processed_targets[$target_path] = true;

        if (!vulnity_ensure_guarded_log_file($target_path)) {
            return;
        }

        $max_files = max(2, (int) apply_filters('vulnity_log_max_files', 3));
        $owned_legacy_files = array('.htaccess', 'index.php');
        for ($index = 0; $index < $max_files; $index++) {
            $owned_legacy_files[] = $index === 0 ? 'vulnity.log' : 'vulnity.log.' . $index;
        }

        foreach (vulnity_get_legacy_log_file_candidates() as $legacy_base_path) {
            if ($legacy_base_path === '' || $legacy_base_path === $target_path) {
                continue;
            }

            for ($index = 0; $index < $max_files; $index++) {
                $source_path = ($index === 0) ? $legacy_base_path : $legacy_base_path . '.' . $index;
                $target_archive_path = ($index === 0) ? $target_path : vulnity_get_log_archive_path($target_path, $index);

                if (!file_exists($source_path)) {
                    continue;
                }

                if (file_exists($target_archive_path)) {
                    vulnity_delete_file($source_path);
                    continue;
                }

                $contents = file_get_contents($source_path);
                if ($contents === false) {
                    continue;
                }

                if (!vulnity_write_file_contents($target_archive_path, vulnity_prepare_guarded_log_contents($contents))) {
                    continue;
                }

                vulnity_delete_file($source_path);
            }

            vulnity_cleanup_legacy_runtime_directory(dirname($legacy_base_path), $owned_legacy_files);
        }
    }
}

if (!function_exists('vulnity_append_log_entry')) {
    /**
     * Append a line to the guarded Vulnity log file.
     *
     * @param string $path  Log file path.
     * @param string $entry Line to append.
     *
     * @return bool
     */
    function vulnity_append_log_entry($path, $entry) {
        if (!vulnity_ensure_guarded_log_file($path)) {
            return false;
        }

        $result = @file_put_contents($path, $entry, FILE_APPEND | LOCK_EX); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
        return $result !== false;
    }
}

if (!function_exists('vulnity_get_log_path')) {
    /**
     * Resolve the secure Vulnity log path under wp-content.
     *
     * @return string
     */
    function vulnity_get_log_path() {
        static $resolved_path = null;

        if (is_string($resolved_path) && $resolved_path !== '') {
            return $resolved_path;
        }

        $candidate_paths = vulnity_get_secure_storage_paths('logs', 'vulnity-log.php');

        $last_status = 'unknown';
        foreach ($candidate_paths as $candidate_path) {
            $status = 'unknown';
            if (!vulnity_ensure_log_dir_with_status($candidate_path, $status)) {
                $last_status = $status;
                continue;
            }

            vulnity_maybe_migrate_legacy_log_files($candidate_path);
            if (!vulnity_ensure_guarded_log_file($candidate_path)) {
                continue;
            }

            $max_files = max(2, (int) apply_filters('vulnity_log_max_files', 3));
            $owned_legacy_files = array('.htaccess', 'index.php');
            for ($index = 0; $index < $max_files; $index++) {
                $owned_legacy_files[] = $index === 0 ? 'vulnity.log' : 'vulnity.log.' . $index;
            }
            foreach (vulnity_get_legacy_log_dir_candidates() as $legacy_dir) {
                vulnity_cleanup_legacy_runtime_directory($legacy_dir, $owned_legacy_files);
            }

            $resolved_path = $candidate_path;
            return $resolved_path;
        }

        if (!empty($candidate_paths)) {
            $dirs = array_map('dirname', $candidate_paths);
            vulnity_set_log_access_notice(
                vulnity_get_log_storage_notice_message($last_status),
                $dirs,
                $last_status
            );
            $resolved_path = $candidate_paths[0];
            return $resolved_path;
        }

        return '';
    }
}

if (!function_exists('vulnity_ensure_log_dir')) {
    /**
     * Ensure log directory exists and is writable.
     *
     * @param string $path Log file path.
     *
     * @return bool
     */
    function vulnity_ensure_log_dir($path) {
        return vulnity_ensure_log_dir_with_status($path);
    }
}

if (!function_exists('vulnity_set_log_access_notice')) {
    /**
     * Store a short-lived admin notice about log directory protection.
     *
     * @param string $message Notice message.
     * @param array  $paths   Relevant paths.
     *
     * @return void
     */
    function vulnity_set_log_access_notice($message, $paths = array(), $status = 'unknown') {
        if (!function_exists('set_transient')) {
            return;
        }

        $message = is_string($message) ? trim($message) : '';
        if ($message === '') {
            return;
        }

        $safe_paths = array();
        if (is_array($paths)) {
            foreach ($paths as $path) {
                if (is_string($path) && $path !== '') {
                    $safe_paths[] = $path;
                }
            }
        }

        $expiration = defined('HOUR_IN_SECONDS') ? HOUR_IN_SECONDS : 3600;
        set_transient(
            'vulnity_log_access_notice',
            array(
                'message' => $message,
                'status'  => is_string($status) ? $status : 'unknown',
                'paths'   => array_values(array_unique($safe_paths)),
                'time'    => time(),
            ),
            $expiration
        );
    }
}

if (!function_exists('vulnity_get_log_storage_notice_message')) {
    /**
     * Build a user-facing log storage notice for the detected failure mode.
     *
     * @param string $status Failure status code.
     *
     * @return string
     */
    function vulnity_get_log_storage_notice_message($status) {
        switch ((string) $status) {
            case 'cannot_create_directory':
                return 'Vulnity could not create its secure log storage directory. Allow the web server or PHP process to create the path below inside wp-content to enable local logging.';
            case 'directory_not_writable':
                return 'Vulnity found its secure log storage directory, but it could not write files inside it. Allow the web server or PHP process to write to the path below to enable local logging.';
            default:
                return 'Vulnity could not create its secure log storage. Allow the web server or PHP process to write to one of the paths below to enable local logging.';
        }
    }
}

if (!function_exists('vulnity_get_secure_storage_notice_doc_url')) {
    /**
     * Resolve the public documentation URL for a storage warning.
     *
     * @param string $status Failure status code.
     *
     * @return string
     */
    function vulnity_get_secure_storage_notice_doc_url($status) {
        switch ((string) $status) {
            case 'directory_not_writable':
                return 'https://vulnity.gitbook.io/vulnity-docs/warnings/la-carpeta-existe-pero-no-se-puede-escribir-dentro';
            case 'cannot_create_directory':
                return 'https://vulnity.gitbook.io/vulnity-docs/warnings/no-se-puede-crear-la-carpeta-segura';
            default:
                return '';
        }
    }
}

if (!function_exists('vulnity_ensure_log_dir_with_status')) {
    /**
     * Ensure the secure log directory exists and report why it failed.
     *
     * @param string      $path   Log file path.
     * @param string|null $status Output status code.
     *
     * @return bool
     */
    function vulnity_ensure_log_dir_with_status($path, &$status = null) {
        $status = 'unknown';

        if (!is_string($path) || $path === '') {
            return false;
        }

        $dir = dirname($path);
        if ($dir === '' || $dir === '.' || $dir === DIRECTORY_SEPARATOR) {
            return false;
        }

        $dir_existed = is_dir($dir);

        if (!$dir_existed) {
            if (!function_exists('wp_mkdir_p') || !wp_mkdir_p($dir)) {
                $filesystem = vulnity_get_wp_filesystem();
                $chmod = defined('FS_CHMOD_DIR') ? FS_CHMOD_DIR : 0755;
                if (
                    !$filesystem ||
                    !method_exists($filesystem, 'mkdir') ||
                    !$filesystem->mkdir($dir, $chmod)
                ) {
                    $status = 'cannot_create_directory';
                    return false;
                }
            }

            if (!is_dir($dir)) {
                $status = 'cannot_create_directory';
                return false;
            }
        }

        if (!vulnity_path_is_writable($dir)) {
            $filesystem = vulnity_get_wp_filesystem();
            if ($filesystem && method_exists($filesystem, 'chmod')) {
                $mode = defined('FS_CHMOD_DIR') ? FS_CHMOD_DIR : 0755;
                $filesystem->chmod($dir, $mode);
            }
        }

        if (!vulnity_path_is_writable($dir)) {
            $status = 'directory_not_writable';
            return false;
        }

        vulnity_ensure_directory_index($dir);
        vulnity_ensure_log_access_protection($dir);

        $status = 'writable';
        return true;
    }
}

if (!function_exists('vulnity_refresh_log_storage_notice')) {
    /**
     * Refresh the admin notice for secure log storage availability.
     *
     * @return bool True when a writable secure log directory exists.
     */
    function vulnity_refresh_log_storage_notice() {
        if (!function_exists('delete_transient')) {
            return false;
        }

        $candidate_paths = vulnity_get_secure_storage_paths('logs', 'vulnity-log.php');
        if (empty($candidate_paths)) {
            return false;
        }

        $last_status = 'unknown';
        foreach ($candidate_paths as $candidate_path) {
            $status = 'unknown';
            if (vulnity_ensure_log_dir_with_status($candidate_path, $status)) {
                delete_transient('vulnity_log_access_notice');
                return true;
            }
            $last_status = $status;
        }

        $dirs = array_values(array_unique(array_filter(array_map('dirname', $candidate_paths), 'is_string')));
        vulnity_set_log_access_notice(
            vulnity_get_log_storage_notice_message($last_status),
            $dirs,
            $last_status
        );

        return false;
    }
}

if (!function_exists('vulnity_write_file_contents')) {
    /**
     * Write file contents using WordPress filesystem API.
     *
     * @param string $path     File path.
     * @param string $contents Contents.
     *
     * @return bool
     */
    function vulnity_write_file_contents($path, $contents) {
        if (!is_string($path) || $path === '') {
            return false;
        }

        if (!is_string($contents)) {
            $contents = (string) $contents;
        }

        $filesystem = vulnity_get_wp_filesystem();
        if (!$filesystem || !method_exists($filesystem, 'put_contents')) {
            $result = @file_put_contents($path, $contents, LOCK_EX); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
            return $result !== false;
        }

        $mode = defined('FS_CHMOD_FILE') ? FS_CHMOD_FILE : false;
        $result = $filesystem->put_contents($path, $contents, $mode);
        if ($result !== false) {
            return true;
        }

        $fallback = @file_put_contents($path, $contents, LOCK_EX); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
        return $fallback !== false;
    }
}

if (!function_exists('vulnity_ensure_directory_index')) {
    /**
     * Ensure an index.php guard exists in the target directory.
     *
     * @param string $dir Directory path.
     *
     * @return void
     */
    function vulnity_ensure_directory_index($dir) {
        if (!is_string($dir) || $dir === '' || !is_dir($dir)) {
            return;
        }

        if (!vulnity_path_is_writable($dir)) {
            return;
        }

        $index_file = rtrim($dir, '/\\') . '/index.php';
        if (file_exists($index_file)) {
            return;
        }

        vulnity_write_file_contents($index_file, "<?php\n// Silence is golden.\n");
    }
}

if (!function_exists('vulnity_ensure_log_access_protection')) {
    /**
     * Ensure log directory has Apache deny rules and provide Nginx guidance.
     *
     * @param string $dir Log directory.
     *
     * @return void
     */
    function vulnity_ensure_log_access_protection($dir) {
        if (!is_string($dir) || $dir === '' || !is_dir($dir)) {
            return;
        }

        if (!vulnity_path_is_writable($dir)) {
            vulnity_set_log_access_notice(
                'Vulnity could not write log protection rules. Restrict access to the Vulnity log directory in your server configuration.',
                array($dir)
            );
            return;
        }

        $htaccess_path = rtrim($dir, '/\\') . '/.htaccess';
        if (!file_exists($htaccess_path)) {
            $rules = array(
                '# Vulnity Logs',
                '<IfModule mod_authz_core.c>',
                'Require all denied',
                '</IfModule>',
                '<IfModule !mod_authz_core.c>',
                'Deny from all',
                '</IfModule>',
                '<IfModule mod_autoindex.c>',
                'Options -Indexes',
                '</IfModule>',
            );

            $written = vulnity_write_file_contents($htaccess_path, implode("\n", $rules) . "\n");
            if (!$written) {
                vulnity_set_log_access_notice(
                    'Vulnity could not write log protection rules. Restrict access to the Vulnity log directory in your server configuration.',
                    array($dir)
                );
            }
        }

    }
}

if (!function_exists('vulnity_get_wp_filesystem')) {
    /**
     * Resolve WordPress filesystem API instance.
     *
     * @return object|null
     */
    function vulnity_get_wp_filesystem() {
        global $wp_filesystem;

        if (is_object($wp_filesystem) && isset($wp_filesystem->method) && $wp_filesystem->method === 'direct') {
            return $wp_filesystem;
        }

        $file_include = ABSPATH . 'wp-admin/includes/file.php';
        if (file_exists($file_include)) {
            require_once $file_include;
        } else {
            return null;
        }

        $base_include = ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php';
        if (!class_exists('WP_Filesystem_Base') && file_exists($base_include)) {
            require_once $base_include;
        }

        $direct_include = ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php';
        if (!class_exists('WP_Filesystem_Direct') && file_exists($direct_include)) {
            require_once $direct_include;
        }

        if (class_exists('WP_Filesystem_Direct')) {
            return new WP_Filesystem_Direct(false);
        }

        if (!function_exists('WP_Filesystem')) {
            return null;
        }

        if (!WP_Filesystem()) {
            return null;
        }

        return is_object($wp_filesystem) ? $wp_filesystem : null;
    }
}

if (!function_exists('vulnity_path_is_writable')) {
    /**
     * Determine if a path is writable using WordPress filesystem helpers.
     *
     * @param string $path Path to validate.
     *
     * @return bool
     */
    function vulnity_path_is_writable($path) {
        if (!is_string($path) || $path === '') {
            return false;
        }

        if (function_exists('wp_is_writable')) {
            return wp_is_writable($path);
        }

        return false;
    }
}

if (!function_exists('vulnity_move_file')) {
    /**
     * Move file using WordPress filesystem API.
     *
     * @param string $source      Source file path.
     * @param string $destination Destination file path.
     *
     * @return bool
     */
    function vulnity_move_file($source, $destination) {
        if (
            !is_string($source) || $source === '' ||
            !is_string($destination) || $destination === ''
        ) {
            return false;
        }

        $filesystem = vulnity_get_wp_filesystem();
        if ($filesystem && method_exists($filesystem, 'move')) {
            return (bool) $filesystem->move($source, $destination, true);
        }

        return false;
    }
}

if (!function_exists('vulnity_delete_file')) {
    /**
     * Delete a file using WordPress API.
     *
     * @param string $path File path.
     *
     * @return bool
     */
    function vulnity_delete_file($path) {
        if (!is_string($path) || $path === '') {
            return false;
        }

        if (!file_exists($path)) {
            return true;
        }

        if (function_exists('wp_delete_file')) {
            wp_delete_file($path);
            return !file_exists($path);
        }

        $filesystem = vulnity_get_wp_filesystem();
        if ($filesystem && method_exists($filesystem, 'delete')) {
            return (bool) $filesystem->delete($path, false, 'f');
        }

        return false;
    }
}

if (!function_exists('vulnity_log_line_count_exceeds')) {
    /**
     * Check if line count exceeds a limit without loading full file in memory.
     *
     * @param string $path  File path.
     * @param int    $limit Max lines allowed.
     *
 * @return bool
 */
    function vulnity_log_line_count_exceeds($path, $limit) {
        if (!is_string($path) || $path === '' || !file_exists($path)) {
            return false;
        }

        $limit = max(1, (int) $limit);

        // Use streaming to avoid memory exhaustion with large files
        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- Required for streaming large files without memory exhaustion
        $fp = fopen($path, 'r');
        if (!$fp) {
            // Conservative fallback: assume limit not exceeded when file is unreadable.
            return false;
        }

        $line_count = 0;
        while (!feof($fp) && $line_count < $limit) {
            if (fgets($fp) !== false) {
                $line_count++;
            }
        }
        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Closing stream opened above
        fclose($fp);

        return $line_count >= $limit;
    }
}

if (!function_exists('vulnity_rotate_log_files')) {
    /**
     * Rotate log files to keep a bounded number of line-limited files.
     *
     * @param string $base_path Base log path.
     * @param int    $max_files Total files including base file.
     * @param int    $max_lines Maximum lines in base file before rotation.
     */
    function vulnity_rotate_log_files($base_path, $max_files, $max_lines) {
        if (!is_string($base_path) || $base_path === '') {
            return;
        }

        $max_files = max(2, (int) $max_files);
        $max_lines = max(1, (int) $max_lines);

        if (!vulnity_log_line_count_exceeds($base_path, $max_lines)) {
            return;
        }

        // Use file locking to prevent race conditions
        $lock_file = $base_path . '.lock';
        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- Required for flock() atomic locking
        $lock_fp = fopen($lock_file, 'c');

        if (!$lock_fp) {
            vulnity_log('[Vulnity] Failed to create lock file for log rotation');
            return;
        }

        // Try to get exclusive lock with timeout
        $lock_acquired = flock($lock_fp, LOCK_EX | LOCK_NB);

        if (!$lock_acquired) {
            // Another process is rotating, skip
            // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Closing lock file
            fclose($lock_fp);
            return;
        }

        try {
            // Double-check line count after acquiring lock
            if (!vulnity_log_line_count_exceeds($base_path, $max_lines)) {
                return;
            }

            $max_archive = $max_files - 1;

            for ($i = $max_archive; $i >= 1; $i--) {
                $dst = vulnity_get_log_archive_path($base_path, $i);
                $src = ($i === 1) ? $base_path : vulnity_get_log_archive_path($base_path, $i - 1);

                if (file_exists($dst)) {
                    vulnity_delete_file($dst);
                }

                if (file_exists($src)) {
                    vulnity_move_file($src, $dst);
                }
            }
        } finally {
            // Always release lock and cleanup
            flock($lock_fp, LOCK_UN);
            // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Closing lock file in finally block
            fclose($lock_fp);

            if (file_exists($lock_file)) {
                vulnity_delete_file($lock_file);
            }
        }
    }
}

if (!function_exists('vulnity_log')) {
    /**
     * Write Vulnity logs to guarded rotating PHP files under wp-content.
     *
     * - Current file: vulnity-log.php
     * - Archives: vulnity-log.1.php, vulnity-log.2.php
     * - Default limits: 3 files total, 1000 lines per file.
     *
     * @param mixed  $message Message to log.
     * @param string $prefix  Optional prefix for the message.
     */
    function vulnity_log($message, $prefix = '') {
        if (is_array($message) || is_object($message)) {
            $message = wp_json_encode($message);
        }

        $message = (string) $message;
        if ($message === '') {
            return;
        }

        if ($prefix !== '') {
            $message = $prefix . $message;
        }

        // Keep one line per entry for deterministic line-based rotation.
        $message = str_replace(array("\r\n", "\r", "\n"), ' ', $message);

        $log_path = vulnity_get_log_path();
        if (!vulnity_ensure_log_dir($log_path)) {
            return;
        }

        $max_files = (int) apply_filters('vulnity_log_max_files', 3);
        $max_lines = (int) apply_filters('vulnity_log_max_lines', 1000);
        vulnity_rotate_log_files($log_path, $max_files, $max_lines);

        $timestamp = gmdate('Y-m-d H:i:s');
        $entry = '[' . $timestamp . ' UTC] ' . $message . PHP_EOL;

        vulnity_append_log_entry($log_path, $entry);
    }
}

if (!function_exists('vulnity_get_server_var')) {
    /**
     * Safely retrieve $_SERVER variable using filter_input with fallback.
     *
     * This function follows WordPress Plugin Check standards by using
     * filter_input() instead of direct $_SERVER access.
     *
     * @param string $key    The $_SERVER key to retrieve.
     * @param int    $filter The filter to apply (default: FILTER_SANITIZE_FULL_SPECIAL_CHARS).
     *
     * @return string The sanitized server variable value, or empty string if not found.
     */
    function vulnity_get_server_var($key, $filter = FILTER_SANITIZE_FULL_SPECIAL_CHARS) {
        if (!is_string($key) || $key === '') {
            return '';
        }

        // Use filter_input as primary method (Plugin Check compliant)
        $value = filter_input(INPUT_SERVER, $key, $filter);

        // Fallback for CLI/test environments where filter_input may return null
        if ($value === null || $value === false) {
            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Validation and sanitization handled below
            if (isset($_SERVER[$key])) {
                // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitization applied immediately below
                $raw_value = $_SERVER[$key];

                if (is_string($raw_value)) {
                    // Apply WordPress sanitization as fallback
                    if (function_exists('wp_unslash')) {
                        $raw_value = wp_unslash($raw_value);
                    }

                    if (function_exists('sanitize_text_field')) {
                        $value = sanitize_text_field($raw_value);
                    } else {
                        $value = $raw_value;
                    }
                } else {
                    $value = '';
                }
            } else {
                $value = '';
            }
        }

        return is_string($value) ? $value : '';
    }
}

if (!function_exists('vulnity_normalize_ip_value')) {
    /**
     * Normalize a single IP value extracted from server headers.
     *
     * @param mixed $value Raw IP value.
     *
     * @return string Normalized IP or empty string.
     */
    function vulnity_normalize_ip_value($value) {
        if (is_array($value)) {
            $value = reset($value);
        }

        if (!is_scalar($value) && !(is_object($value) && method_exists($value, '__toString'))) {
            return '';
        }

        $value = trim((string) $value);
        if ($value === '') {
            return '';
        }

        $value = trim($value, "\"' \t\n\r\0\x0B");

        if (strlen($value) > 2 && $value[0] === '[' && substr($value, -1) === ']') {
            $value = substr($value, 1, -1);
        }

        if (filter_var($value, FILTER_VALIDATE_IP)) {
            return $value;
        }

        if (strpos($value, ':') !== false && substr_count($value, ':') === 1) {
            $parts = explode(':', $value, 2);
            if (isset($parts[0]) && filter_var($parts[0], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
                return $parts[0];
            }
        }

        return '';
    }
}

if (!function_exists('vulnity_is_private_or_reserved_ip')) {
    /**
     * Check whether an IP belongs to private or reserved ranges.
     *
     * @param string $ip IP address.
     *
     * @return bool
     */
    function vulnity_is_private_or_reserved_ip($ip) {
        if (!is_string($ip) || $ip === '' || !filter_var($ip, FILTER_VALIDATE_IP)) {
            return false;
        }

        return false === filter_var(
            $ip,
            FILTER_VALIDATE_IP,
            FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
        );
    }
}

if (!function_exists('vulnity_ip_matches_cidr')) {
    /**
     * Check whether an IP belongs to the given CIDR range.
     *
     * @param string $ip    IP address.
     * @param string $entry CIDR entry.
     *
     * @return bool
     */
    function vulnity_ip_matches_cidr($ip, $entry) {
        if (!is_string($ip) || !is_string($entry) || strpos($entry, '/') === false) {
            return false;
        }

        list($subnet, $prefix_length) = array_pad(explode('/', trim($entry), 2), 2, '');
        $subnet = vulnity_normalize_ip_value($subnet);

        if ($subnet === '' || $prefix_length === '' || !is_numeric($prefix_length)) {
            return false;
        }

        $ip_bin = @inet_pton($ip);
        $subnet_bin = @inet_pton($subnet);

        if ($ip_bin === false || $subnet_bin === false || strlen($ip_bin) !== strlen($subnet_bin)) {
            return false;
        }

        $prefix_length = (int) $prefix_length;
        $max_bits = strlen($ip_bin) * 8;

        if ($prefix_length < 0 || $prefix_length > $max_bits) {
            return false;
        }

        $full_bytes = intdiv($prefix_length, 8);
        $remaining_bits = $prefix_length % 8;

        if ($full_bytes > 0 && substr($ip_bin, 0, $full_bytes) !== substr($subnet_bin, 0, $full_bytes)) {
            return false;
        }

        if ($remaining_bits === 0) {
            return true;
        }

        $mask = (0xFF << (8 - $remaining_bits)) & 0xFF;

        return (ord($ip_bin[$full_bytes]) & $mask) === (ord($subnet_bin[$full_bytes]) & $mask);
    }
}

if (!function_exists('vulnity_ip_matches_proxy_entries')) {
    /**
     * Check whether an IP matches any trusted proxy entry.
     *
     * @param string $ip      IP address.
     * @param array  $entries Trusted proxy entries.
     *
     * @return bool
     */
    function vulnity_ip_matches_proxy_entries($ip, $entries) {
        $ip = vulnity_normalize_ip_value($ip);
        if ($ip === '' || !is_array($entries)) {
            return false;
        }

        foreach ($entries as $entry) {
            if (!is_string($entry)) {
                continue;
            }

            $entry = trim($entry);
            if ($entry === '') {
                continue;
            }

            $normalized_entry = vulnity_normalize_ip_value($entry);
            if ($normalized_entry !== '' && $normalized_entry === $ip) {
                return true;
            }

            if (strpos($entry, '/') !== false && vulnity_ip_matches_cidr($ip, $entry)) {
                return true;
            }
        }

        return false;
    }
}

if (!function_exists('vulnity_get_trusted_proxy_entries')) {
    /**
     * Retrieve sanitized trusted proxy entries from local configuration.
     *
     * @return array
     */
    function vulnity_get_trusted_proxy_entries() {
        static $cached_entries = null;

        if (is_array($cached_entries)) {
            return $cached_entries;
        }

        $stored_entries = function_exists('get_option')
            ? get_option('vulnity_trusted_proxies', array())
            : array();
        if (!is_array($stored_entries)) {
            $stored_entries = array();
        }

        $entries = array();
        foreach ($stored_entries as $entry) {
            if (!is_string($entry)) {
                continue;
            }

            $entry = trim($entry);
            if ($entry === '') {
                continue;
            }

            $normalized_entry = vulnity_normalize_ip_value($entry);
            if ($normalized_entry !== '') {
                $entries[] = $normalized_entry;
                continue;
            }

            if (strpos($entry, '/') !== false) {
                $parts = explode('/', $entry, 2);
                $subnet = vulnity_normalize_ip_value($parts[0]);
                $prefix = isset($parts[1]) ? trim($parts[1]) : '';

                if ($subnet !== '' && $prefix !== '' && is_numeric($prefix)) {
                    $prefix = (int) $prefix;
                    $max_bits = strlen((string) @inet_pton($subnet)) * 8;

                    if ($max_bits > 0 && $prefix >= 0 && $prefix <= $max_bits) {
                        $entries[] = $subnet . '/' . $prefix;
                    }
                }
            }
        }

        $entries = array_values(array_unique($entries));

        /**
         * Filter trusted proxy entries before resolving visitor IPs.
         *
         * @param array $entries Trusted proxy IPs/CIDRs.
         */
        $cached_entries = function_exists('apply_filters')
            ? apply_filters('vulnity_trusted_proxy_entries', $entries)
            : $entries;

        if (!is_array($cached_entries)) {
            $cached_entries = $entries;
        }

        return $cached_entries;
    }
}

if (!function_exists('vulnity_parse_forwarded_list_header')) {
    /**
     * Parse comma-separated forwarded header values into valid IPs.
     *
     * @param string $header_value Header value.
     *
     * @return array
     */
    function vulnity_parse_forwarded_list_header($header_value) {
        if (!is_string($header_value) || $header_value === '') {
            return array();
        }

        $ips = array();
        $parts = preg_split('/\s*,\s*/', $header_value, -1, PREG_SPLIT_NO_EMPTY);

        foreach ($parts as $part) {
            $ip = vulnity_normalize_ip_value($part);
            if ($ip !== '') {
                $ips[] = $ip;
            }
        }

        return $ips;
    }
}

if (!function_exists('vulnity_parse_forwarded_header_ips')) {
    /**
     * Parse RFC 7239 Forwarded headers into valid IPs.
     *
     * @param string $header_value Header value.
     *
     * @return array
     */
    function vulnity_parse_forwarded_header_ips($header_value) {
        if (!is_string($header_value) || $header_value === '') {
            return array();
        }

        $ips = array();
        if (preg_match_all('/for=(?:"?\[?)([A-Fa-f0-9:\.]+)(?:\]?)(?::\d+)?/i', $header_value, $matches)) {
            foreach ($matches[1] as $match) {
                $ip = vulnity_normalize_ip_value($match);
                if ($ip !== '') {
                    $ips[] = $ip;
                }
            }
        }

        return $ips;
    }
}

if (!function_exists('vulnity_resolve_forwarded_chain_ip')) {
    /**
     * Resolve the client IP from a proxy chain using right-to-left validation.
     *
     * @param array $chain                 IP chain from left (client) to right (nearest proxy).
     * @param array $trusted_proxy_entries Trusted proxy IPs/CIDRs.
     * @param bool  $trust_private_hops    Whether private/reserved proxy hops should be skipped.
     *
     * @return string
     */
    function vulnity_resolve_forwarded_chain_ip($chain, $trusted_proxy_entries, $trust_private_hops = false) {
        if (!is_array($chain) || empty($chain)) {
            return '';
        }

        $normalized_chain = array();
        foreach ($chain as $entry) {
            $ip = vulnity_normalize_ip_value($entry);
            if ($ip !== '') {
                $normalized_chain[] = $ip;
            }
        }

        if (empty($normalized_chain)) {
            return '';
        }

        $leftmost_ip = $normalized_chain[0];

        for ($index = count($normalized_chain) - 1; $index >= 0; $index--) {
            $candidate_ip = $normalized_chain[$index];
            $is_trusted_hop = vulnity_ip_matches_proxy_entries($candidate_ip, $trusted_proxy_entries);

            if (!$is_trusted_hop && $trust_private_hops && vulnity_is_private_or_reserved_ip($candidate_ip)) {
                $is_trusted_hop = true;
            }

            if (!$is_trusted_hop) {
                return $candidate_ip;
            }
        }

        return $leftmost_ip;
    }
}

if (!function_exists('vulnity_resolve_client_ip')) {
    /**
     * Resolve the most reliable client IP without trusting spoofable headers by default.
     *
     * @return string
     */
    function vulnity_resolve_client_ip() {
        $remote_addr = vulnity_normalize_ip_value(vulnity_get_server_var('REMOTE_ADDR'));
        if ($remote_addr === '') {
            return 'unknown';
        }

        $trusted_proxy_entries = vulnity_get_trusted_proxy_entries();
        $remote_is_private_proxy = vulnity_is_private_or_reserved_ip($remote_addr);
        $remote_is_trusted_proxy = $remote_is_private_proxy || vulnity_ip_matches_proxy_entries($remote_addr, $trusted_proxy_entries);

        if (!$remote_is_trusted_proxy) {
            return $remote_addr;
        }

        $chain_headers = array(
            'HTTP_X_FORWARDED_FOR',
            'HTTP_X_FORWARDED',
            'HTTP_X_CLUSTER_CLIENT_IP',
            'HTTP_FORWARDED_FOR',
        );

        foreach ($chain_headers as $header) {
            $chain = vulnity_parse_forwarded_list_header(vulnity_get_server_var($header));
            $resolved_ip = vulnity_resolve_forwarded_chain_ip($chain, $trusted_proxy_entries, $remote_is_private_proxy);
            if ($resolved_ip !== '') {
                return $resolved_ip;
            }
        }

        $forwarded_header = vulnity_get_server_var('HTTP_FORWARDED');
        $forwarded_chain = vulnity_parse_forwarded_header_ips($forwarded_header);
        $resolved_forwarded_ip = vulnity_resolve_forwarded_chain_ip(
            $forwarded_chain,
            $trusted_proxy_entries,
            $remote_is_private_proxy
        );

        if ($resolved_forwarded_ip !== '') {
            return $resolved_forwarded_ip;
        }

        if (vulnity_ip_matches_proxy_entries($remote_addr, $trusted_proxy_entries)) {
            foreach (array('HTTP_CF_CONNECTING_IP', 'HTTP_X_REAL_IP') as $header) {
                $candidate_ip = vulnity_normalize_ip_value(vulnity_get_server_var($header));
                if ($candidate_ip !== '') {
                    return $candidate_ip;
                }
            }
        }

        return $remote_addr;
    }
}

if (!defined('VULNITY_BASE_URL')) {
    define('VULNITY_BASE_URL', 'https://euxnoekqasvzwfcbybkg.supabase.co/functions/v1');
}

$vulnity_core_file = vulnity_plugin_path('includes/class-core.php');
$vulnity_anti_collapse_file = vulnity_plugin_path('includes/class-anti-collapse.php');

if (file_exists($vulnity_core_file)) {
    require_once $vulnity_core_file;
} else {
    vulnity_log('[Vulnity] Core file missing at: ' . $vulnity_core_file);
}

if (file_exists($vulnity_anti_collapse_file)) {
    require_once $vulnity_anti_collapse_file;
} else {
    vulnity_log('[Vulnity] Anti-collapse file missing at: ' . $vulnity_anti_collapse_file);
}

// Inicializar sistema anti-colapso automáticamente
add_action('init', function() {
    if (class_exists('Vulnity_Anti_Collapse_System')) {
        Vulnity_Anti_Collapse_System::get_instance();
    }
});

class Vulnity {
    private static $instance = null;
    private $core = null;
    
    public static function get_instance() {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }
    
    private function __construct() {
        register_activation_hook(__FILE__, array($this, 'activate'));
        register_deactivation_hook(__FILE__, array($this, 'deactivate'));

        // Final cron cleanup at very low priority — other hooks (e.g. alert
        // dispatchers) may re-schedule cron events after deactivate() runs,
        // so we clear them once more after everything else has fired.
        add_action('deactivated_plugin', array($this, 'final_deactivation_cleanup'), 9999);

        // Add cron action for retry queue processing
        add_action('vulnity_process_retry_queue', array($this, 'process_retry_queue'));

        // Add action for triggered inventory sync
        add_action('vulnity_triggered_inventory_sync', array($this, 'handle_triggered_inventory_sync'));

        // Add heartbeat check action
        add_action('vulnity_check_heartbeat', array($this, 'check_heartbeat'));

        // Register custom cron interval
        add_filter('cron_schedules', array($this, 'add_cron_intervals'));

        // Add admin notices for critical failures
        add_action('admin_notices', array($this, 'show_critical_alert_failures'));
        add_action('admin_init', array($this, 'refresh_runtime_storage_notices'));

        // Schedule retry queue processing if not already scheduled
        if (!wp_next_scheduled('vulnity_process_retry_queue')) {
            wp_schedule_event(time(), 'vulnity_1hour', 'vulnity_process_retry_queue');
        }

        // Ensure heartbeat check is scheduled every six hours
        $this->ensure_heartbeat_schedule();
        
        $this->core = Vulnity_Core::get_instance();
    }

    public function refresh_runtime_storage_notices() {
        if (!is_admin() || (function_exists('wp_doing_ajax') && wp_doing_ajax())) {
            return;
        }

        if (function_exists('current_user_can') && !current_user_can('manage_options')) {
            return;
        }

        if (class_exists('Vulnity_Firewall_Manager')) {
            Vulnity_Firewall_Manager::get_instance()->refresh_storage_notice();
        }

        vulnity_refresh_log_storage_notice();
    }

    public function activate() {
        if (!get_option('vulnity_config')) {
            update_option('vulnity_config', array(
                'site_id' => '',
                'token' => '',
                'signing_secret' => '',
                'paired_at' => '',
                'status' => 'inactive'
            ));
        }
        
        if (!get_option('vulnity_alerts')) {
            update_option('vulnity_alerts', array());
        }
        
        if (!get_option('vulnity_retry_queue')) {
            update_option('vulnity_retry_queue', array());
        }
        
        if (!get_option('vulnity_failed_alerts')) {
            update_option('vulnity_failed_alerts', array());
        }

        update_option('vulnity_alerts_unread', 0);

        if (!get_option('vulnity_heartbeat_status')) {
            update_option('vulnity_heartbeat_status', array(
                'failures' => 0,
                'last_success' => null
            ));
        }

        if (class_exists('Vulnity_Firewall_Manager')) {
            $firewall = Vulnity_Firewall_Manager::get_instance();
            $firewall->install_firewall();
        }

        // Schedule cron events
        if (!wp_next_scheduled('vulnity_process_retry_queue')) {
            wp_schedule_event(time(), 'vulnity_1hour', 'vulnity_process_retry_queue');
        }

        $this->ensure_heartbeat_schedule();

        // Process any pending alerts immediately after activation
        $this->process_unsent_alerts();
    }

    public function deactivate() {
        // Keep pairing and local data on deactivation.
        vulnity_log('[Vulnity] Plugin deactivated - preserving pairing and local data.');

        // Clean up runtime anti-collapse buffers/hooks.
        if (class_exists('Vulnity_Anti_Collapse_System')) {
            $anti_collapse = Vulnity_Anti_Collapse_System::get_instance();
            $anti_collapse->cleanup();
        }

        // Remove plugin-owned hardening marker rules from .htaccess when possible.
        if (class_exists('Vulnity_Static_Security')) {
            Vulnity_Static_Security::get_instance()->cleanup_on_deactivation();
        }

        // Remove runtime firewall integration.
        if (class_exists('Vulnity_Firewall_Manager')) {
            Vulnity_Firewall_Manager::get_instance()->remove_firewall();
        }

        // Always try to remove plugin-owned .htaccess marker blocks on deactivation.
        $this->remove_plugin_htaccess_markers();

        // Remove plugin rewrite rules from WordPress runtime.
        flush_rewrite_rules(false);

        delete_option('vulnity_heartbeat_status');

        // Clear ALL runtime schedules LAST — some cleanup steps above may
        // re-schedule cron events (e.g. alert sending triggers inventory sync),
        // so we clear them after everything else has finished.
        $vulnity_cron_hooks = array(
            'vulnity_sync_inventory',
            'vulnity_sync_inventory_deferred',
            'vulnity_process_retry_queue',
            'vulnity_flush_alert_buffer',
            'vulnity_check_heartbeat',
            'vulnity_sync_mitigation_config',
            'vulnity_cleanup_flood_data',
            'vulnity_process_brute_force_windows',
            'vulnity_triggered_inventory_sync',
        );
        foreach ($vulnity_cron_hooks as $hook) {
            wp_unschedule_hook($hook);
        }
    }

    /**
     * Final sweep after all deactivation hooks have fired.
     * Removes any cron events re-scheduled by late-firing hooks (e.g.
     * plugin-change alerts that trigger inventory syncs) and ensures
     * .htaccess marker blocks are fully removed.
     * Runs on 'deactivated_plugin' at priority 9999 — only for this plugin.
     */
    public function final_deactivation_cleanup($plugin) {
        if ($plugin !== plugin_basename(__FILE__)) {
            return;
        }

        // Final cron sweep — wp_unschedule_hook removes ALL events for a hook
        // regardless of arguments (unlike wp_clear_scheduled_hook).
        $vulnity_cron_hooks = array(
            'vulnity_sync_inventory',
            'vulnity_sync_inventory_deferred',
            'vulnity_process_retry_queue',
            'vulnity_flush_alert_buffer',
            'vulnity_check_heartbeat',
            'vulnity_sync_mitigation_config',
            'vulnity_cleanup_flood_data',
            'vulnity_process_brute_force_windows',
            'vulnity_triggered_inventory_sync',
        );
        foreach ($vulnity_cron_hooks as $hook) {
            wp_unschedule_hook($hook);
        }

        // Final .htaccess sweep — other deactivation hooks may have
        // re-written markers after our initial cleanup pass.
        $this->remove_plugin_htaccess_markers();
    }

    /**
     * Add custom cron intervals
     */
    public function add_cron_intervals($schedules) {
        $schedules['vulnity_every_minute'] = array(
            'interval' => 60, // 1 minute
            'display'  => $this->translate_label('Every Minute (Vulnity)')
        );

        $schedules['vulnity_5min'] = array(
            'interval' => 300, // 5 minutes
            'display'  => $this->translate_label('Every 5 Minutes (Vulnity)')
        );

        $schedules['vulnity_1hour'] = array(
            'interval' => 3600, // 1 hour
            'display'  => $this->translate_label('Every Hour (Vulnity)')
        );

        $schedules['vulnity_15min'] = array(
            'interval' => 900, // 15 minutes
            'display'  => $this->translate_label('Every 15 Minutes (Vulnity)')
        );

        $schedules['vulnity_6hours'] = array(
            'interval' => 21600, // 6 hours
            'display'  => $this->translate_label('Every 6 Hours (Vulnity)')
        );

        return $schedules;
    }

    /**
     * Avoid early translation loading notices before init.
     *
     * @param string $text Label text.
     *
     * @return string
     */
    private function translate_label($text) {
        if (!is_string($text) || $text === '') {
            return '';
        }

        if (did_action('init') > 0) {
            switch ($text) {
                case 'Every Minute (Vulnity)':
                    return __('Every Minute (Vulnity)', 'vulnity');
                case 'Every 5 Minutes (Vulnity)':
                    return __('Every 5 Minutes (Vulnity)', 'vulnity');
                case 'Every 15 Minutes (Vulnity)':
                    return __('Every 15 Minutes (Vulnity)', 'vulnity');
                case 'Every 6 Hours (Vulnity)':
                    return __('Every 6 Hours (Vulnity)', 'vulnity');
            }
        }

        return $text;
    }

    private function ensure_heartbeat_schedule() {
        $current_schedule = wp_get_schedule('vulnity_check_heartbeat');

        if ($current_schedule && $current_schedule !== 'vulnity_6hours') {
            wp_clear_scheduled_hook('vulnity_check_heartbeat');
        }

        if (!wp_next_scheduled('vulnity_check_heartbeat')) {
            wp_schedule_event(time(), 'vulnity_6hours', 'vulnity_check_heartbeat');
        }
    }

    private function remove_plugin_htaccess_markers() {
        $markers = array(
            'Vulnity Login URL',
            'Vulnity Common Paths',
            'Vulnity Firewall',
        );

        foreach ($markers as $marker) {
            $this->remove_htaccess_marker($marker);
        }
    }

    private function remove_htaccess_marker($marker) {
        if (!is_string($marker) || $marker === '') {
            return;
        }

        if (!function_exists('get_home_path')) {
            $file_include = ABSPATH . 'wp-admin/includes/file.php';
            if (file_exists($file_include)) {
                require_once $file_include;
            }
        }

        $home_path = function_exists('get_home_path') ? get_home_path() : ABSPATH;
        $htaccess_path = trailingslashit($home_path) . '.htaccess';

        if (!file_exists($htaccess_path)) {
            return;
        }

        if (!is_readable($htaccess_path)) {
            vulnity_log('[Vulnity] .htaccess not readable, cannot remove marker: ' . $marker);
            return;
        }

        // Try WP Filesystem first, fall back to native PHP functions.
        $contents = false;
        $use_native = false;

        $filesystem = vulnity_get_wp_filesystem();
        if ($filesystem && method_exists($filesystem, 'get_contents') && method_exists($filesystem, 'put_contents')) {
            $contents = $filesystem->get_contents($htaccess_path);
        }

        if ($contents === false) {
            // Fallback: read with native PHP if WP Filesystem is unavailable.
            $contents = @file_get_contents($htaccess_path);
            $use_native = true;
        }

        if ($contents === false) {
            vulnity_log('[Vulnity] Could not read .htaccess to remove marker: ' . $marker);
            return;
        }

        list($updated, $changed) = $this->strip_marker_blocks_from_contents($contents, $marker);
        if (!$changed) {
            return;
        }

        $written = false;

        if (!$use_native && $filesystem) {
            $chmod = defined('FS_CHMOD_FILE') ? FS_CHMOD_FILE : false;
            $written = $filesystem->put_contents($htaccess_path, $updated, $chmod);
        }

        if (!$written && vulnity_path_is_writable($htaccess_path)) {
            // Fallback: write with native PHP when WP_Filesystem is unavailable.
            $fallback_fs = new WP_Filesystem_Direct(false);
            $chmod = defined('FS_CHMOD_FILE') ? FS_CHMOD_FILE : false;
            $written = $fallback_fs->put_contents($htaccess_path, $updated, $chmod);
        }

        if ($written) {
            vulnity_log('[Vulnity] Removed .htaccess marker: ' . $marker);
        } else {
            vulnity_log('[Vulnity] Failed to write .htaccess after removing marker: ' . $marker);
        }
    }

    private function strip_marker_blocks_from_contents($contents, $marker) {
        if (!is_string($contents) || !is_string($marker) || $marker === '') {
            return array($contents, false);
        }

        $begin_marker = '# BEGIN ' . $marker;
        $end_marker = '# END ' . $marker;
        $changed = false;
        $offset = 0;

        while (($begin_pos = strpos($contents, $begin_marker, $offset)) !== false) {
            $prefix = substr($contents, 0, $begin_pos);
            $line_start = strrpos($prefix, "\n");
            $line_start = ($line_start === false) ? 0 : ($line_start + 1);

            $end_pos = strpos($contents, $end_marker, $begin_pos);
            if ($end_pos === false) {
                break;
            }

            $line_end = strpos($contents, "\n", $end_pos);
            $line_end = ($line_end === false) ? strlen($contents) : ($line_end + 1);

            $contents = substr($contents, 0, $line_start) . substr($contents, $line_end);
            $changed = true;
            $offset = $line_start;
        }

        return array($contents, $changed);
    }
    
    /**
     * Handle triggered inventory sync
     */
    public function handle_triggered_inventory_sync($trigger_type) {
        vulnity_log('[Vulnity] Handling triggered inventory sync from: ' . $trigger_type);
        
        if (class_exists('Vulnity_Inventory_Sync')) {
            $inventory_sync = Vulnity_Inventory_Sync::get_instance();
            $scan_type = 'alert_triggered_' . $trigger_type;
            
            $result = $inventory_sync->perform_sync($scan_type);
            
            if ($result['success']) {
                vulnity_log('[Vulnity] Scheduled inventory sync completed successfully');
            } else {
                vulnity_log('[Vulnity] Scheduled inventory sync failed: ' . $result['error']);
            }
        }
    }
    
    /**
     * Process retry queue via cron
     */
    public function process_retry_queue() {
        // Load the alert base class if needed
        if (!class_exists('Vulnity_Alert_Base')) {
            $alerts_dir = vulnity_plugin_path('includes/alerts/');
            if (file_exists($alerts_dir . 'class-alert-base.php')) {
                require_once $alerts_dir . 'class-alert-base.php';
            }
        }
        
        // Process the queue
        if (class_exists('Vulnity_Alert_Base')) {
            Vulnity_Alert_Base::process_retry_queue();
        }
        
        // Also process any unsent alerts
        $this->process_unsent_alerts();
    }
    
    /**
     * Process unsent alerts
     */
    private function process_unsent_alerts() {
        if (!class_exists('Vulnity_Alert_Base')) {
            $alerts_dir = vulnity_plugin_path('includes/alerts/');
            if (file_exists($alerts_dir . 'class-alert-base.php')) {
                require_once $alerts_dir . 'class-alert-base.php';
            }
        }

        $alerts = get_option('vulnity_alerts', array());
        $siem = Vulnity_SIEM_Connector::get_instance();
        $processed_count = 0;
        $max_to_process = 10; // Process max 10 alerts at a time to avoid timeout
        $sent_alerts = array();
        
        foreach ($alerts as &$alert) {
            if ($processed_count >= $max_to_process) {
                break;
            }
            
            if (!$alert['sent_to_siem'] && (!isset($alert['retry_count']) || $alert['retry_count'] < 3)) {
                $result = $siem->send_alert($alert);
                
                if ($result['success']) {
                    $alert['sent_to_siem'] = true;
                    $alert['siem_response'] = isset($result['data']) && is_array($result['data']) ? $result['data'] : array();
                    $alert['sent_at'] = current_time('mysql');
                    unset($alert['last_error']);
                    $sent_alerts[] = array(
                        'id' => $alert['id'],
                        'alert' => $alert,
                    );
                    vulnity_log('[Vulnity] Unsent alert processed successfully: ' . $alert['id']);
                } else {
                    $alert['retry_count'] = isset($alert['retry_count']) ? $alert['retry_count'] + 1 : 1;
                    $alert['last_retry'] = current_time('mysql');
                    $alert['last_error'] = $result['error'];
                }
                
                $processed_count++;
            }
        }
        
        if ($processed_count > 0) {
            update_option('vulnity_alerts', $alerts);
            vulnity_log('[Vulnity] Processed ' . $processed_count . ' unsent alerts');
        }

        if (class_exists('Vulnity_Alert_Base')) {
            foreach ($sent_alerts as $sent_entry) {
                Vulnity_Alert_Base::dispatch_alert_sent_static($sent_entry['id'], $sent_entry['alert']);
            }
        } else {
            foreach ($sent_alerts as $sent_entry) {
                do_action('vulnity_alert_sent', $sent_entry['id'], $sent_entry['alert']);
            }
        }
    }
    
    /**
     * Send all pending alerts before deactivation
     */
    private function send_all_pending_alerts() {
        $alerts = get_option('vulnity_alerts', array());
        $siem = Vulnity_SIEM_Connector::get_instance();
        $sent_count = 0;

        foreach ($alerts as &$alert) {
            if (!$alert['sent_to_siem']) {
                $result = $siem->send_alert($alert);

                if ($result['success']) {
                    $alert['sent_to_siem'] = true;
                    $alert['sent_at'] = current_time('mysql');
                    $sent_count++;
                }
            }
        }

        if ($sent_count > 0) {
            update_option('vulnity_alerts', $alerts);
            vulnity_log('[Vulnity] Sent ' . $sent_count . ' pending alerts before deactivation');
        }
    }

    public function check_heartbeat() {
        $config = get_option('vulnity_config');

        if (empty($config) || empty($config['site_id']) || empty($config['token']) || (isset($config['status']) && $config['status'] !== 'active')) {
            return;
        }

        $heartbeat_state = get_option('vulnity_heartbeat_status', array('failures' => 0));
        $heartbeat_state['last_checked'] = current_time('mysql');

        $endpoint = VULNITY_BASE_URL . '/heartbeat';

        $theme = function_exists('wp_get_theme') ? wp_get_theme() : null;
        $timezone_string = function_exists('wp_timezone_string') ? wp_timezone_string() : get_option('timezone_string');
        $latency_recorded_at = isset($heartbeat_state['last_latency_recorded_at']) ? $heartbeat_state['last_latency_recorded_at'] : null;

        $system_info = array(
            'site_id'          => $config['site_id'],
            'site_url'         => get_site_url(),
            'home_url'         => home_url(),
            'site_name'        => get_bloginfo('name'),
            'language'         => get_locale(),
            'timezone'         => $timezone_string ?: date_default_timezone_get(),
            'multisite'        => is_multisite(),
            'theme'            => $theme ? $theme->get('Name') : null,
            'theme_version'    => $theme ? $theme->get('Version') : null,
            'php_memory_limit' => ini_get('memory_limit'),
            'server'           => php_sapi_name(),
            'timestamp'        => current_time('c'),
            'latency_recorded_at' => $latency_recorded_at,
        );

        $body_latency = isset($heartbeat_state['last_latency']) ? (int) $heartbeat_state['last_latency'] : null;
        $body = array(
            'status'            => 'online',
            'plugin_version'    => VULNITY_VERSION,
            'wordpress_version' => get_bloginfo('version'),
            'php_version'       => PHP_VERSION,
            'latency_ms'        => $body_latency,
            'system_info'       => $system_info,
        );

        $headers = array(
            'Content-Type' => 'application/json',
            'x-vulnity-token' => $config['token'],
            'x-site-id' => $config['site_id']
        );

        $args = array(
            'method' => 'POST',
            'headers' => $headers,
            'timeout' => 15,
            'sslverify' => true
        );

        $start_time = microtime(true);
        $args['body'] = wp_json_encode($body);
        $response = wp_remote_post($endpoint, $args);
        $latency_ms = round((microtime(true) - $start_time) * 1000);
        $status_code = null;
        $error_message = '';

        if (is_wp_error($response)) {
            $error_message = $response->get_error_message();
        } else {
            $status_code = wp_remote_retrieve_response_code($response);

            if ($status_code >= 200 && $status_code < 300) {
                $heartbeat_state['failures'] = 0;
                $heartbeat_state['last_success'] = current_time('mysql');
                $heartbeat_state['last_status'] = $status_code;
                $heartbeat_state['last_latency'] = $latency_ms;
                $heartbeat_state['last_latency_recorded_at'] = current_time('mysql');
                unset($heartbeat_state['last_error']);
                update_option('vulnity_heartbeat_status', $heartbeat_state);
                return;
            }

            // 401 = SIEM invalidated the token (e.g. the site was deleted remotely).
            // Auto-disconnect the plugin so it mirrors the SIEM state.
            if ($status_code === 401) {
                $response_body = wp_remote_retrieve_body($response);
                $decoded       = json_decode($response_body, true);
                $server_reason = (is_array($decoded) && isset($decoded['error'])) ? $decoded['error'] : 'token_invalidated';

                vulnity_log('[Vulnity] Heartbeat 401 — token invalidated by SIEM, auto-disconnecting.');

                if (class_exists('Vulnity_Core')) {
                    Vulnity_Core::get_instance()->perform_remote_disconnect(array(
                        'reason'  => 'token_invalidated',
                        'message' => $server_reason,
                    ));
                }

                return;
            }

            $response_body = wp_remote_retrieve_body($response);
            $decoded = json_decode($response_body, true);
            if (is_array($decoded) && isset($decoded['error'])) {
                $error_message = $decoded['error'];
            } else {
                $error_message = sprintf('HTTP %d: %s', $status_code, wp_trim_words($response_body, 40));
            }
        }

        if (!empty($error_message)) {
            $heartbeat_state['last_error'] = $error_message;
        }

        $heartbeat_state['last_status'] = $status_code ?? 'error';
        $heartbeat_state['last_latency'] = $latency_ms;
        $heartbeat_state['last_latency_recorded_at'] = current_time('mysql');
        update_option('vulnity_heartbeat_status', $heartbeat_state);

        vulnity_log('[Vulnity] Heartbeat request failed: ' . ($error_message ?: 'Unknown error'));
    }
    
    /**
     * Show admin notices for critical alert failures
     */
    public function show_critical_alert_failures() {
        $failed_critical = get_transient('vulnity_critical_alert_failed');
        
        if ($failed_critical) {
            ?>
            <div class="notice notice-error is-dismissible">
                <p><strong>Vulnity Security Alert:</strong> <?php echo esc_html($failed_critical['message']); ?></p>
                <p>Alert ID: <?php echo esc_html($failed_critical['alert_id']); ?></p>
                <p><a href="<?php echo esc_url( admin_url( 'options-general.php?page=vulnity&tab=alerts' ) ); ?>">View Alerts</a></p>
            </div>
            <?php
            
            // Delete transient after showing
            delete_transient('vulnity_critical_alert_failed');
        }
        
        // Also check for failed queue
        $failed_alerts = get_option('vulnity_failed_alerts', array());
        $critical_failed = array_filter($failed_alerts, function($item) {
            return $item['alert']['severity'] === 'critical';
        });
        
        // Show anti-collapse panic mode notification if active
        if (class_exists('Vulnity_Anti_Collapse_System')) {
            $anti_collapse = Vulnity_Anti_Collapse_System::get_instance();
            $stats = $anti_collapse->get_stats();
            
            if ($stats['panic_mode']) {
                ?>
                <div class="notice notice-warning is-dismissible">
                    <p><strong>Vulnity Anti-Collapse:</strong> System is in PANIC MODE due to high alert volume (<?php echo esc_html( $stats['current_rate'] ); ?> alerts/min). Alerts are being aggregated to prevent SIEM overload.</p>
                </div>
                <?php
            }
        }

        $firewall_notice = get_transient('vulnity_firewall_storage_unwritable');
        if (is_array($firewall_notice) && !empty($firewall_notice['message'])) {
            $paths = '';
            $doc_url = vulnity_get_secure_storage_notice_doc_url(isset($firewall_notice['status']) ? $firewall_notice['status'] : '');
            if (!empty($firewall_notice['paths']) && is_array($firewall_notice['paths'])) {
                $paths = implode(', ', array_map('esc_html', $firewall_notice['paths']));
            }
            ?>
            <div class="notice notice-warning is-dismissible">
                <p><strong>Vulnity Firewall:</strong> <?php echo esc_html($firewall_notice['message']); ?></p>
                <?php if ($paths !== '') : ?>
                    <p><?php echo esc_html('Checked paths: ' . $paths); ?></p>
                <?php endif; ?>
                <?php if ($doc_url !== '') : ?>
                    <p><a href="<?php echo esc_url($doc_url); ?>" target="_blank" rel="noopener noreferrer"><?php echo esc_html__('View the fix guide', 'vulnity'); ?></a></p>
                <?php endif; ?>
            </div>
            <?php
        }

        $log_notice = get_transient('vulnity_log_access_notice');
        if (is_array($log_notice) && !empty($log_notice['message'])) {
            $doc_url = vulnity_get_secure_storage_notice_doc_url(isset($log_notice['status']) ? $log_notice['status'] : '');
            ?>
            <div class="notice notice-warning is-dismissible">
                <p><strong>Vulnity Logs:</strong> <?php echo esc_html($log_notice['message']); ?></p>
                <?php if (!empty($log_notice['paths']) && is_array($log_notice['paths'])) : ?>
                    <?php foreach ($log_notice['paths'] as $path) : ?>
                        <p><?php echo esc_html('Path: ' . $path); ?></p>
                    <?php endforeach; ?>
                <?php endif; ?>
                <p><?php echo esc_html('Allow the web server or PHP process to write to the listed path to restore local logging.'); ?></p>
                <?php if ($doc_url !== '') : ?>
                    <p><a href="<?php echo esc_url($doc_url); ?>" target="_blank" rel="noopener noreferrer"><?php echo esc_html__('View the fix guide', 'vulnity'); ?></a></p>
                <?php endif; ?>
            </div>
            <?php

            // Delete transient after showing so the notice only appears once
            delete_transient('vulnity_log_access_notice');
        }
    }
}

// Initialize the plugin
Vulnity::get_instance();

// Global function to manually trigger retry queue processing (for debugging)
if (!function_exists('vulnity_process_retry_queue_manual')) {
    function vulnity_process_retry_queue_manual() {
        $vulnity = Vulnity::get_instance();
        $vulnity->process_retry_queue();
        return 'Retry queue processed';
    }
}

// Global function to check anti-collapse status (for debugging)
if (!function_exists('vulnity_check_anti_collapse_status')) {
    function vulnity_check_anti_collapse_status() {
        if (class_exists('Vulnity_Anti_Collapse_System')) {
            $anti_collapse = Vulnity_Anti_Collapse_System::get_instance();
            return $anti_collapse->get_stats();
        }
        return 'Anti-collapse system not loaded';
    }
}
