<?php
if ( ! defined( 'ABSPATH' ) ) { exit; }

class Vulnity_Firewall_Manager {
    private static $instance = null;

    private $data_dir;
    private $data_file;
    private $data_dir_candidates = array();
    private $legacy_data_dir_candidates = array();
    private $htaccess_path;
    private $bootstrap_file;
    private $last_state_hash = null;
    private $filesystem = null;

    const HTACCESS_MARKER = 'Vulnity Firewall';
    const HTACCESS_START = '# BEGIN Vulnity Firewall';
    const HTACCESS_END = '# END Vulnity Firewall';
    const HTACCESS_SPACING_BLANK_LINES = 3;

    public static function get_instance() {
        if (self::$instance === null) {
            self::$instance = new self();
        }

        return self::$instance;
    }

    private function __construct() {
        $uploads = function_exists('wp_upload_dir') ? wp_upload_dir(null, false) : null;
        $uploads_dir = isset($uploads['basedir']) && $uploads['basedir'] ? $uploads['basedir'] : null;
        $content_root = defined('WP_CONTENT_DIR') ? WP_CONTENT_DIR : trailingslashit(ABSPATH) . 'wp-content';
        $content_root = $this->path_with_trailing_slash($content_root);

        if ($uploads_dir) {
            $uploads_dir = $this->path_with_trailing_slash($uploads_dir);
        } else {
            $uploads_dir = $content_root . 'uploads/';
        }

        if (function_exists('vulnity_get_secure_storage_paths')) {
            $this->data_dir_candidates = array_values(array_unique(vulnity_get_secure_storage_paths('firewall')));
        } else {
            $this->data_dir_candidates = array($content_root . 'vulnity-data/firewall');
        }

        $this->legacy_data_dir_candidates = array_values(array_unique(array(
            $content_root . 'vulnity-firewall',
            $uploads_dir . 'vulnity-firewall',
        )));

        $this->ensure_file_functions_loaded();
        $this->resolve_data_dir(true);

        $home_path = function_exists('get_home_path') ? get_home_path() : ABSPATH;
        $this->htaccess_path = trailingslashit($home_path) . '.htaccess';
        $this->bootstrap_file = vulnity_plugin_path('firewall/bootstrap.php');

        $this->migrate_legacy_data_if_needed();

        if (
            function_exists('get_option') &&
            is_string($this->data_file) &&
            $this->data_file !== '' &&
            !file_exists($this->data_file) &&
            is_string($this->data_dir) &&
            $this->data_dir !== '' &&
            $this->ensure_directory_writable($this->data_dir)
        ) {
            $this->sync_from_options();
        }
    }

    public function install_firewall() {
        $this->ensure_directories();
        $this->sync_from_options();
    }

    public function remove_firewall() {
        $this->remove_htaccess_rules();
        $this->remove_data_files();
        $this->last_state_hash = null;
    }

    public function sync_from_options() {
        if (!function_exists('get_option')) {
            return;
        }

        $blocked_ips = get_option('vulnity_blocked_ips', array());
        $whitelist = get_option('vulnity_ip_whitelist', array());

        $this->sync_firewall_state($blocked_ips, $whitelist);
    }

    public function sync_firewall_state($blocked_ips, $whitelist) {
        $this->ensure_directories();

        $state = $this->build_state($blocked_ips, $whitelist);

        $this->write_data_file($state);
        $this->write_htaccess_rules($state['active_ips']);
    }

    public function refresh_storage_notice() {
        $this->ensure_directories();
    }

    private function ensure_directories() {
        $this->resolve_data_dir(true);

        $status = 'unknown';
        if (!$this->ensure_directory_writable($this->data_dir, $status)) {
            $this->update_storage_notice($status);
            return;
        }

        $this->update_storage_notice('writable');
    }

    private function resolve_data_dir($prefer_existing = true) {
        if (empty($this->data_dir_candidates)) {
            return false;
        }

        $primary_dir = $this->data_dir_candidates[0];
        $this->set_data_paths($primary_dir);

        if ($prefer_existing) {
            foreach (array('blocks.php', 'blocks.json') as $filename) {
                $candidate_file = $this->path_with_trailing_slash($primary_dir) . $filename;
                if (file_exists($candidate_file)) {
                    return true;
                }
            }
        }

        if ($this->ensure_directory_writable($primary_dir)) {
            $this->set_data_paths($primary_dir);
            return true;
        }

        return false;
    }

    private function switch_to_fallback_data_dir() {
        return false;
    }

    private function ensure_directory_writable($dir, &$status = null) {
        $status = 'unknown';

        $dir_existed = is_dir($dir);
        if (!$dir_existed) {
            if (!function_exists('wp_mkdir_p') || !wp_mkdir_p($dir)) {
                $this->filesystem_mkdir($dir, 0755, true);
            }
        }

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

        if ($this->filesystem_is_writable($dir)) {
            $this->ensure_directory_guard($dir);
            $this->ensure_directory_htaccess($dir);
            $status = 'writable';
            return true;
        }

        $this->filesystem_chmod($dir, 0755);

        $writable = $this->filesystem_is_writable($dir);
        if ($writable) {
            $this->ensure_directory_guard($dir);
            $this->ensure_directory_htaccess($dir);
            $status = 'writable';
            return true;
        }

        $status = 'directory_not_writable';
        return false;
    }

    private function ensure_directory_guard($dir) {
        if (!is_string($dir) || $dir === '' || !is_dir($dir)) {
            return;
        }

        if (!$this->filesystem_is_writable($dir)) {
            return;
        }

        $index_file = $this->path_with_trailing_slash($dir) . 'index.php';
        if (file_exists($index_file)) {
            return;
        }

        $contents = "<?php\n// Silence is golden.\n";
        $this->safe_file_put_contents($index_file, $contents);
    }

    private function ensure_directory_htaccess($dir) {
        if (!is_string($dir) || $dir === '' || !is_dir($dir)) {
            return;
        }

        if (!$this->filesystem_is_writable($dir)) {
            return;
        }

        $htaccess = $this->path_with_trailing_slash($dir) . '.htaccess';
        if (file_exists($htaccess)) {
            return;
        }

        $rules = array(
            '# Vulnity Firewall',
            '<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>',
        );

        $this->safe_file_put_contents($htaccess, implode("\n", $rules) . "\n");
    }

    private function update_storage_notice($status) {
        if (!function_exists('set_transient') || !function_exists('delete_transient')) {
            return;
        }

        $key = 'vulnity_firewall_storage_unwritable';
        if ($status === 'writable') {
            delete_transient($key);
            return;
        }

        $paths = array();
        foreach ($this->data_dir_candidates as $candidate) {
            if (is_string($candidate) && $candidate !== '') {
                $paths[] = $candidate;
            }
        }

        $message = 'Vulnity could not write secure firewall storage. To keep persistent IP blocking enabled, allow the web server or PHP process to write to one of the paths below (preferred: wp-content/vulnity-data/firewall).';
        if ($status === 'cannot_create_directory') {
            $message = 'Vulnity could not create the secure firewall storage directory. Allow the web server or PHP process to create the path below inside wp-content to keep persistent IP blocking enabled.';
        } elseif ($status === 'directory_not_writable') {
            $message = 'Vulnity found the secure firewall storage directory, but it could not write files inside it. Allow the web server or PHP process to write to the path below to keep persistent IP blocking enabled.';
        }

        $payload = array(
            'message' => $message,
            'status'  => $status,
            'paths'   => array_values(array_unique($paths)),
            'time'    => time(),
        );

        $expiration = defined('HOUR_IN_SECONDS') ? HOUR_IN_SECONDS : 3600;
        set_transient($key, $payload, $expiration);
    }

    private function set_data_paths($dir) {
        $this->data_dir = $dir;
        $this->data_file = $this->path_with_trailing_slash($this->data_dir) . 'blocks.php';
    }

    private function build_state($blocked_ips, $whitelist) {
        $active_ips = array();
        $blocked_map = array();
        $now = time();
        $trusted_proxies = array();

        if (function_exists('vulnity_get_trusted_proxy_entries')) {
            $trusted_proxies = vulnity_get_trusted_proxy_entries();
        } elseif (function_exists('get_option')) {
            $trusted_proxies = get_option('vulnity_trusted_proxies', array());
        }

        if (!is_array($trusted_proxies)) {
            $trusted_proxies = array();
        }

        $trusted_proxies = array_values(array_unique(array_filter(array_map(function ($entry) {
            if (!is_string($entry)) {
                return '';
            }

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

            if (function_exists('vulnity_normalize_ip_value')) {
                $normalized = vulnity_normalize_ip_value($entry);
                if ($normalized !== '') {
                    return $normalized;
                }
            }

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

                if ($subnet !== '' && $prefix !== '' && is_numeric($prefix)) {
                    return $subnet . '/' . (int) $prefix;
                }
            }

            return '';
        }, $trusted_proxies))));

        $whitelist = is_array($whitelist) ? array_values(array_unique(array_filter($whitelist, function ($ip) {
            return filter_var($ip, FILTER_VALIDATE_IP);
        }))) : array();

        if (!empty($whitelist)) {
            $whitelist = array_map(array($this, 'normalize_ip'), $whitelist);
        }

        if (!is_array($blocked_ips)) {
            $blocked_ips = array();
        }

        foreach ($blocked_ips as $ip => $data) {
            if (!filter_var($ip, FILTER_VALIDATE_IP)) {
                continue;
            }

            $normalized_ip = $this->normalize_ip($ip);

            if (in_array($normalized_ip, $whitelist, true)) {
                continue;
            }

            $expires_at = $this->determine_expiration($data, $now);

            if ($expires_at !== 0 && $expires_at !== null && $expires_at <= $now) {
                continue;
            }

            $blocked_map[$normalized_ip] = array(
                'reason' => isset($data['reason']) ? $data['reason'] : 'Security block',
                'blocked_at' => isset($data['blocked_at']) ? $data['blocked_at'] : gmdate('Y-m-d H:i:s'),
                'permanent' => $expires_at === 0,
                'expires_at' => $expires_at === null ? 0 : (int) $expires_at,
                'display_expires' => ($expires_at && $expires_at > 0) ? gmdate('c', $expires_at) : null,
            );

            $active_ips[] = $normalized_ip;
        }

        ksort($blocked_map, SORT_STRING);
        sort($whitelist, SORT_STRING);

        return array(
            'generated_at' => gmdate('c'),
            'blocked_ips' => $blocked_map,
            'whitelist' => $whitelist,
            'trusted_proxies' => $trusted_proxies,
            'active_ips' => $active_ips,
        );
    }

    private function parse_expiration_timestamp($value) {
        if (empty($value)) {
            return null;
        }

        if (is_numeric($value)) {
            return (int) $value;
        }

        if (!is_string($value)) {
            return null;
        }

        if (function_exists('wp_timezone')) {
            $timezone = wp_timezone();
            if ($timezone instanceof DateTimeZone) {
                $dt = date_create($value, $timezone);
                if ($dt instanceof DateTimeInterface) {
                    return $dt->getTimestamp();
                }
            }
        }

        $timestamp = strtotime($value);
        return $timestamp ? (int) $timestamp : null;
    }

    private function determine_expiration($data, $now) {
        if (isset($data['permanent']) && $data['permanent']) {
            return 0;
        }

        if (!empty($data['blocked_until'])) {
            $timestamp = $this->parse_expiration_timestamp($data['blocked_until']);
            if ($timestamp) {
                return $timestamp;
            }
        }

        if (!empty($data['expires_at'])) {
            $timestamp = $this->parse_expiration_timestamp($data['expires_at']);
            if ($timestamp) {
                return $timestamp;
            }
        }

        if (!empty($data['duration']) && !empty($data['blocked_at'])) {
            $duration_seconds = $this->parse_duration_to_seconds($data['duration']);
            if ($duration_seconds) {
                $blocked_at = $this->parse_expiration_timestamp($data['blocked_at']);
                if ($blocked_at) {
                    return $blocked_at + $duration_seconds;
                }
            }
        }

        if (!empty($data['block_duration'])) {
            $duration_seconds = is_numeric($data['block_duration']) ? (int) $data['block_duration'] : $this->parse_duration_to_seconds($data['block_duration']);
            if ($duration_seconds) {
                return $now + $duration_seconds;
            }
        }

        return 0;
    }

    private function parse_duration_to_seconds($duration) {
        if (is_numeric($duration)) {
            return (int) $duration;
        }

        if (!is_string($duration)) {
            return null;
        }

        $duration = trim(strtolower($duration));

        if ($duration === 'permanent' || $duration === 'forever') {
            return null;
        }

        $pattern = '/^(\d+)([smhdw])$/';
        if (preg_match($pattern, $duration, $matches)) {
            $value = (int) $matches[1];
            $unit = $matches[2];

            switch ($unit) {
                case 's':
                    return $value;
                case 'm':
                    return $value * 60;
                case 'h':
                    return $value * 3600;
                case 'd':
                    return $value * 86400;
                case 'w':
                    return $value * 604800;
            }
        }

        return null;
    }

    private function write_data_file($state) {
        $export_state = array(
            'generated_at' => $state['generated_at'],
            'blocked_ips' => $state['blocked_ips'],
            'whitelist' => $state['whitelist'],
            'trusted_proxies' => isset($state['trusted_proxies']) && is_array($state['trusted_proxies']) ? $state['trusted_proxies'] : array(),
        );

        $encoder = function_exists('wp_json_encode') ? 'wp_json_encode' : 'json_encode';
        $new_hash = md5((string) call_user_func($encoder, $export_state));

        if ($this->last_state_hash === null && file_exists($this->data_file)) {
            $existing = $this->load_firewall_state_file($this->data_file);
            if (is_array($existing)) {
                $this->last_state_hash = md5((string) call_user_func($encoder, $existing));
            }
        }

        if ($this->last_state_hash !== null && $this->last_state_hash === $new_hash) {
            return;
        }

        $this->last_state_hash = $new_hash;

        $result = $this->safe_file_put_contents($this->data_file, $this->build_php_data_file_contents($export_state));

        if (!$result && $this->switch_to_fallback_data_dir()) {
            $result = $this->safe_file_put_contents($this->data_file, $this->build_php_data_file_contents($export_state));
        }

        if ($result) {
            $this->remove_legacy_data_files();
        }
    }

    private function migrate_legacy_data_if_needed() {
        if (!is_string($this->data_file) || $this->data_file === '' || file_exists($this->data_file)) {
            return;
        }

        $this->ensure_directories();
        if (!$this->filesystem_is_writable($this->data_dir)) {
            return;
        }

        foreach ($this->legacy_data_dir_candidates as $legacy_dir) {
            foreach (array('blocks.php', 'blocks.json') as $filename) {
                $legacy_file = $this->path_with_trailing_slash($legacy_dir) . $filename;
                if (!file_exists($legacy_file)) {
                    continue;
                }

                $state = $this->load_firewall_state_file($legacy_file);
                if (!is_array($state)) {
                    continue;
                }

                if ($this->safe_file_put_contents($this->data_file, $this->build_php_data_file_contents($state))) {
                    $this->remove_legacy_data_files();
                }

                return;
            }
        }
    }

    private function load_firewall_state_file($file) {
        if (!is_string($file) || $file === '' || !file_exists($file)) {
            return null;
        }

        if (substr($file, -5) === '.json') {
            $raw = file_get_contents($file);
            if ($raw === false) {
                return null;
            }

            $decoded = json_decode($raw, true);
            return is_array($decoded) ? $decoded : null;
        }

        $loaded = @include $file; // phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.UsingVariable -- Internal plugin data file path.
        return is_array($loaded) ? $loaded : null;
    }

    private function build_php_data_file_contents($state) {
        if (!is_array($state)) {
            $state = array();
        }

        $json = wp_json_encode($state, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        if (!is_string($json) || $json === '') {
            $json = '[]';
        }

        return "<?php\nreturn json_decode('" . $this->escape_php_single_quoted_string($json) . "', true);\n";
    }

    private function escape_php_single_quoted_string($value) {
        return str_replace(
            array('\\', "'"),
            array('\\\\', "\\'"),
            (string) $value
        );
    }

    private function remove_legacy_data_files() {
        foreach ($this->legacy_data_dir_candidates as $candidate) {
            $this->cleanup_plugin_owned_data_dir($candidate);
        }
    }

    private function cleanup_plugin_owned_data_dir($dir) {
        if (!is_string($dir) || $dir === '' || !is_dir($dir)) {
            return;
        }

        foreach (array('blocks.json', 'blocks.php', 'blocks.tmp.json', 'blocks.tmp.php', '.htaccess', 'index.php') as $filename) {
            $candidate_file = $this->path_with_trailing_slash($dir) . $filename;
            if (file_exists($candidate_file) && is_file($candidate_file)) {
                $this->filesystem_unlink($candidate_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;
        }

        $this->filesystem_rmdir($dir);
    }

    private function write_htaccess_rules($active_ips) {
        $bootstrap_real_path = realpath($this->bootstrap_file);
        if ($bootstrap_real_path === false) {
            return;
        }

        $bootstrap_path = $this->normalize_path($bootstrap_real_path);

        $active_ips = array_unique($active_ips);
        sort($active_ips, SORT_STRING);

        $state_hash = md5(json_encode(array_values($active_ips)));

        $block_lines = array();
        $block_lines[] = '# Vulnity Firewall State: ' . $state_hash;
        $block_lines[] = '<IfModule mod_php.c>';
        $block_lines[] = "    php_value auto_prepend_file '" . $bootstrap_path . "'";
        $block_lines[] = '</IfModule>';
        $block_lines[] = '<IfModule mod_php7.c>';
        $block_lines[] = "    php_value auto_prepend_file '" . $bootstrap_path . "'";
        $block_lines[] = '</IfModule>';
        $block_lines[] = '<IfModule mod_php8.c>';
        $block_lines[] = "    php_value auto_prepend_file '" . $bootstrap_path . "'";
        $block_lines[] = '</IfModule>';

        // $active_ips already normalized and sorted above

        $use_apache_deny = apply_filters('vulnity_firewall_use_apache_deny', false);

        if ($use_apache_deny && !empty($active_ips)) {
            $block_lines[] = '<IfModule mod_authz_core.c>';
            $block_lines[] = '    <RequireAll>';
            $block_lines[] = '        Require all granted';
            foreach ($active_ips as $ip) {
                $block_lines[] = '        Require not ip ' . $ip;
            }
            $block_lines[] = '    </RequireAll>';
            $block_lines[] = '</IfModule>';
            $block_lines[] = '<IfModule !mod_authz_core.c>';
            $block_lines[] = '    Order Allow,Deny';
            $block_lines[] = '    Allow from all';
            foreach ($active_ips as $ip) {
                $block_lines[] = '    Deny from ' . $ip;
            }
            $block_lines[] = '</IfModule>';
        } else {
            $block_lines[] = '# IP blocks enforced by Vulnity PHP firewall';
        }

        $this->write_htaccess_contents($block_lines);
    }

    private function write_htaccess_contents($block_lines) {
        $htaccess_dir = dirname($this->htaccess_path);

        if (file_exists($this->htaccess_path)) {
            if (!$this->filesystem_is_writable($this->htaccess_path)) {
                return;
            }
        } elseif (!is_dir($htaccess_dir) || !$this->filesystem_is_writable($htaccess_dir)) {
            return;
        }

        if ($this->insert_block_with_markers($block_lines)) {
            return;
        }

        $block_with_markers = array_merge(
            array(self::HTACCESS_START),
            $block_lines,
            array(self::HTACCESS_END)
        );

        $new_block = implode(PHP_EOL, $block_with_markers);
        if ($new_block !== '') {
            $new_block = rtrim($new_block, "\r\n") . PHP_EOL;
        }

        $htaccess_contents = '';
        $existing_block = '';
        $pattern = '/' . preg_quote(self::HTACCESS_START, '/') . '.*?' . preg_quote(self::HTACCESS_END, '/') . '\s*/s';

        if (file_exists($this->htaccess_path)) {
            $htaccess_contents = file_get_contents($this->htaccess_path);
            if ($htaccess_contents === false) {
                return;
            }

            if (preg_match($pattern, $htaccess_contents, $matches)) {
                $existing_block = $matches[0];
            }
        }

        if ($existing_block !== '' && rtrim($existing_block, "\r\n") === rtrim($new_block, "\r\n")) {
            return;
        }

        if ($existing_block !== '') {
            $updated_contents = preg_replace($pattern, $new_block, $htaccess_contents, 1);
            if ($updated_contents === null) {
                return;
            }

            $htaccess_contents = $updated_contents;
        } else {
            if ($htaccess_contents !== '' && !preg_match('/\r?\n$/', $htaccess_contents)) {
                $htaccess_contents .= PHP_EOL;
            }

            $htaccess_contents .= $new_block;
        }

        if ($htaccess_contents !== '') {
            $htaccess_contents = rtrim($htaccess_contents, "\r\n") . PHP_EOL;
        }

        $this->safe_file_put_contents($this->htaccess_path, $htaccess_contents);
        $this->normalize_marker_spacing();
    }

    private function insert_block_with_markers($block_lines) {
        $this->ensure_marker_functions_loaded();

        if (function_exists('insert_with_markers')) {
            $result = insert_with_markers($this->htaccess_path, self::HTACCESS_MARKER, $block_lines);
            if ($result !== false) {
                $this->normalize_marker_spacing();
                return true;
            }
        }

        return false;
    }

    private function remove_block_with_markers() {
        $this->ensure_marker_functions_loaded();

        if (!function_exists('insert_with_markers')) {
            return false;
        }

        $result = insert_with_markers($this->htaccess_path, self::HTACCESS_MARKER, array());

        return $result !== false;
    }

    private function ensure_marker_functions_loaded() {
        if (function_exists('insert_with_markers')) {
            return;
        }

        $misc_path = ABSPATH . 'wp-admin/includes/misc.php';

        if (file_exists($misc_path)) {
            require_once $misc_path;
            if (!function_exists('insert_with_markers')) {
                return;
            }
        }
    }

    private function normalize_marker_spacing() {
        if (!file_exists($this->htaccess_path) || !is_readable($this->htaccess_path)) {
            return;
        }

        $contents = file_get_contents($this->htaccess_path);
        if ($contents === false || $contents === '') {
            return;
        }

        $pattern = '/' . preg_quote(self::HTACCESS_START, '/') . '.*?' . preg_quote(self::HTACCESS_END, '/') . '/s';
        if (!preg_match($pattern, $contents, $matches, PREG_OFFSET_CAPTURE)) {
            return;
        }

        $block = $matches[0][0];
        $start = $matches[0][1];
        $end = $start + strlen($block);

        $prefix = substr($contents, 0, $start);
        $suffix = substr($contents, $end);

        $prefix = preg_replace('/(?:\r?\n[ \t]*)+$/', '', $prefix);
        $suffix = preg_replace('/^(?:[ \t]*\r?\n)+/', '', $suffix);
        $block = rtrim($block, "\r\n");

        $sections = array();

        if ($prefix !== '') {
            $sections[] = $prefix;
        }

        $sections[] = $block;

        if ($suffix !== '') {
            $sections[] = $suffix;
        }

        $spacing = max(1, self::HTACCESS_SPACING_BLANK_LINES + 1);
        $separator = str_repeat(PHP_EOL, $spacing);

        $normalized = implode($separator, $sections);
        $normalized = rtrim($normalized, "\r\n") . PHP_EOL;

        if ($normalized !== $contents) {
            $this->safe_file_put_contents($this->htaccess_path, $normalized);
        }
    }

    private function remove_htaccess_rules() {
        if (!file_exists($this->htaccess_path)) {
            return;
        }

        if (!$this->filesystem_is_writable($this->htaccess_path)) {
            return;
        }

        if ($this->remove_block_with_markers()) {
            return;
        }

        $contents = file_get_contents($this->htaccess_path);
        if ($contents === false) {
            return;
        }

        $pattern = '/' . preg_quote(self::HTACCESS_START, '/') . '.*?' . preg_quote(self::HTACCESS_END, '/') . '\n?/s';
        $updated = preg_replace($pattern, '', $contents);

        if ($updated !== null && $updated !== $contents) {
            $this->safe_file_put_contents($this->htaccess_path, trim($updated) === '' ? '' : $updated);
        }
    }

    private function remove_data_files() {
        $candidates = array_merge($this->data_dir_candidates, $this->legacy_data_dir_candidates);

        if (empty($candidates) && !empty($this->data_dir)) {
            $candidates = array($this->data_dir);
        }

        foreach ($candidates as $candidate) {
            $this->cleanup_plugin_owned_data_dir($candidate);
        }
    }

    private function safe_file_put_contents($file, $contents) {
        $dir = dirname($file);
        if (!is_dir($dir)) {
            if (!function_exists('wp_mkdir_p') || !wp_mkdir_p($dir)) {
                $this->filesystem_mkdir($dir, 0755, true);
            }
        }

        if (!$this->filesystem_is_writable($dir)) {
            $this->filesystem_chmod($dir, 0755);
        }

        $tmp_file = $this->get_temporary_file_path($file);
        $filesystem = $this->get_filesystem();
        $result = false;

        if ($filesystem && method_exists($filesystem, 'put_contents')) {
            $chmod = defined('FS_CHMOD_FILE') ? FS_CHMOD_FILE : 0644;
            $result = $filesystem->put_contents($tmp_file, $contents, $chmod);
        }

        if ($result === false) {
            $result = @file_put_contents($tmp_file, $contents, LOCK_EX);
        }

        if ($result === false) {
            vulnity_log('[Vulnity] Failed to write firewall file: ' . $file); 
            if (file_exists($tmp_file)) {
                $this->filesystem_unlink($tmp_file);
            }
            return false;
        }

        $this->filesystem_chmod($tmp_file, 0644);

        if (!$this->filesystem_move($tmp_file, $file, true)) {
            $fallback = @file_put_contents($file, $contents, LOCK_EX);
            if ($fallback !== false) {
                if (file_exists($tmp_file)) {
                    $this->filesystem_unlink($tmp_file);
                }
                return true;
            }

            if (file_exists($tmp_file)) {
                $this->filesystem_unlink($tmp_file);
            }
            vulnity_log('[Vulnity] Failed to finalize firewall file: ' . $file); 
            return false;
        }

        return true;
    }

    private function get_temporary_file_path($file) {
        $dir = dirname($file);
        $filename = basename($file);
        $extension = pathinfo($filename, PATHINFO_EXTENSION);

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

        return $file . '.tmp';
    }

    public function get_bootstrap_path() {
        return $this->bootstrap_file;
    }

    private function normalize_path($path) {
        return str_replace('\\', '/', $path);
    }

    private function normalize_ip($ip) {
        if (strpos($ip, ':') !== false) {
            return strtolower($ip);
        }

        return $ip;
    }

    private function path_with_trailing_slash($path) {
        return rtrim($path, '/\\') . '/';
    }

    private function get_filesystem() {
        if ($this->filesystem !== null) {
            return $this->filesystem ?: null;
        }

        $this->filesystem = false;

        if (!defined('ABSPATH')) {
            return null;
        }

        $this->ensure_file_functions_loaded();

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

        global $wp_filesystem;

        if (is_object($wp_filesystem) && !empty($wp_filesystem->method) && $wp_filesystem->method === 'direct') {
            $this->filesystem = $wp_filesystem;
            return $this->filesystem;
        }

        $this->ensure_filesystem_classes_loaded();

        if (class_exists('WP_Filesystem_Direct')) {
            $this->filesystem = new WP_Filesystem_Direct(null);
            return $this->filesystem;
        }

        if (function_exists('WP_Filesystem') && (!is_object($wp_filesystem) || empty($wp_filesystem->method))) {
            WP_Filesystem();
        }

        if (is_object($wp_filesystem)) {
            $this->filesystem = $wp_filesystem;
            return $this->filesystem;
        }

        return null;
    }

    private function filesystem_mkdir($path, $chmod = 0755, $recursive = false) {
        $filesystem = $this->get_filesystem();

        if ($filesystem && method_exists($filesystem, 'mkdir')) {
            return $filesystem->mkdir($path, $chmod, $recursive);
        }

        return @mkdir($path, $chmod, $recursive); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_mkdir
    }

    private function filesystem_is_writable($path) {
        $filesystem = $this->get_filesystem();

        if ($filesystem && method_exists($filesystem, 'is_writable')) {
            return $filesystem->is_writable($path);
        }

        return @is_writable($path); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable
    }

    private function filesystem_delete($path, $recursive = false) {
        $filesystem = $this->get_filesystem();

        if ($filesystem && method_exists($filesystem, 'delete')) {
            return $filesystem->delete($path, $recursive);
        }

        if (is_dir($path) && !$recursive) {
            return @rmdir($path); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir
        }

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

        return @unlink($path); // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink
    }

    private function filesystem_rmdir($path) {
        $filesystem = $this->get_filesystem();

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

        return @rmdir($path); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir
    }

    private function filesystem_chmod($path, $mode) {
        $filesystem = $this->get_filesystem();

        if ($filesystem && method_exists($filesystem, 'chmod')) {
            return $filesystem->chmod($path, $mode);
        }

        return @chmod($path, $mode); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_chmod
    }

    private function filesystem_move($source, $destination, $overwrite = false) {
        $filesystem = $this->get_filesystem();

        if ($filesystem && method_exists($filesystem, 'move')) {
            return $filesystem->move($source, $destination, $overwrite);
        }

        if ($overwrite && file_exists($destination)) {
            $this->filesystem_delete($destination);
        }

        return @rename($source, $destination); // phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename
    }

    private function filesystem_unlink($path) {
        $filesystem = $this->get_filesystem();

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

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

        return @unlink($path); // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink
    }

    private function ensure_file_functions_loaded() {
        if (function_exists('get_home_path') && function_exists('WP_Filesystem')) {
            return;
        }

        $file_include = ABSPATH . 'wp-admin/includes/file.php';

        if (file_exists($file_include)) {
            require_once $file_include;
        }
    }

    private function ensure_filesystem_classes_loaded() {
        if (class_exists('WP_Filesystem_Direct')) {
            return;
        }

        $base_include = ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php';
        $direct_include = ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php';

        if (file_exists($base_include)) {
            require_once $base_include;
        }

        if (file_exists($direct_include)) {
            require_once $direct_include;
        }
    }
}
