<?php

namespace VulnTitan\Admin;

use Throwable;
use VulnTitan\Services\MalwareScanner;
use VulnTitan\Services\VulnerabilityScanner;
use VulnTitan\Services\FileIntegrityScanner;
use VulnTitan\Services\BaselineBuilder;
use VulnTitan\Services\FirewallService;
use VulnTitan\Services\VulnerabilityRiskService;

class Ajax
{
    protected const FILE_OPERATIONS_CAPABILITY_FILTER = 'vulntitan_file_operations_capability';

    public function register(): void
    {
        add_action('wp_ajax_vulntitan_vulnerability_scan_item', [$this, 'vulnerabilityScan']);
        add_action('wp_ajax_vulntitan_vulnerability_rescan_item', [$this, 'vulnerabilityRescan']);
        add_action('wp_ajax_vulntitan_vuln_risk_update', [$this, 'vulnRiskUpdate']);
        add_action('wp_ajax_vulntitan_vuln_risk_clear', [$this, 'vulnRiskClear']);
        add_action('wp_ajax_vulntitan_vuln_risk_audit', [$this, 'vulnRiskAudit']);
        add_action('wp_ajax_vulntitan_malware_scan_init', [$this, 'malwareScanInit']);
        add_action('wp_ajax_vulntitan_malware_scan_file', [$this, 'malwareScanFile']);
        add_action('wp_ajax_vulntitan_malware_scan_batch', [$this, 'malwareScanBatch']);
        add_action('wp_ajax_vulntitan_malware_fix_finding', [$this, 'malwareFixFinding']);
        add_action('wp_ajax_vulntitan_integrity_scan_init', [$this, 'integrityScanInit']);
        add_action('wp_ajax_vulntitan_integrity_scan_file', [$this, 'integrityScanFile']);
        add_action('wp_ajax_vulntitan_integrity_scan_batch', [$this, 'integrityScanBatch']);
        add_action('wp_ajax_vulntitan_firewall_get_data', [$this, 'firewallGetData']);
        add_action('wp_ajax_vulntitan_firewall_save_settings', [$this, 'firewallSaveSettings']);
        add_action('wp_ajax_vulntitan_firewall_clear_logs', [$this, 'firewallClearLogs']);
        add_action('wp_ajax_vulntitan_firewall_approve_request', [$this, 'firewallApproveRequest']);
        add_action('wp_ajax_vulntitan_firewall_dismiss_approval', [$this, 'firewallDismissApproval']);
        add_action('wp_ajax_vulntitan_firewall_unblock_ip', [$this, 'firewallUnblockIp']);
        add_action('wp_ajax_vulntitan_firewall_allowlist_ip', [$this, 'firewallAllowlistIp']);
        add_action('wp_ajax_vulntitan_firewall_get_learning', [$this, 'firewallGetLearning']);
        add_action('wp_ajax_vulntitan_firewall_apply_learning', [$this, 'firewallApplyLearning']);
        add_action('wp_ajax_vulntitan_firewall_dismiss_learning', [$this, 'firewallDismissLearning']);
    }

    public function vulnerabilityScan(): void
    {
        (new VulnerabilityScanner())->runScan();
    }

    public function vulnerabilityRescan(): void
    {
        check_ajax_referer('vulntitan_ajax_scan', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(['message' => esc_html__('Insufficient permissions.', 'vulntitan')], 403);
        }

        $type = isset($_POST['type']) ? sanitize_key((string) wp_unslash($_POST['type'])) : '';
        $pluginFile = isset($_POST['plugin_file']) ? sanitize_text_field((string) wp_unslash($_POST['plugin_file'])) : '';
        $themeSlug = isset($_POST['theme_slug']) ? sanitize_text_field((string) wp_unslash($_POST['theme_slug'])) : '';
        $slug = isset($_POST['slug']) ? sanitize_text_field((string) wp_unslash($_POST['slug'])) : '';

        if (!in_array($type, ['plugin', 'theme', 'core'], true)) {
            wp_send_json_error(['message' => esc_html__('Invalid scan item.', 'vulntitan')], 400);
        }

        if ($type === 'plugin' && $pluginFile === '') {
            wp_send_json_error(['message' => esc_html__('Invalid scan item.', 'vulntitan')], 400);
        }

        if ($type === 'theme' && $themeSlug === '') {
            wp_send_json_error(['message' => esc_html__('Invalid scan item.', 'vulntitan')], 400);
        }

        if (function_exists('wp_update_plugins')) {
            wp_update_plugins();
        }
        if (function_exists('wp_update_themes')) {
            wp_update_themes();
        }
        if (function_exists('wp_version_check')) {
            wp_version_check();
        }

        $item = $this->buildScanItem($type, $pluginFile, $themeSlug, $slug);
        if (!$item) {
            wp_send_json_error(['message' => esc_html__('Invalid scan item.', 'vulntitan')], 400);
        }

        $result = (new VulnerabilityScanner())->scanItem($item);
        if (!($result['success'] ?? false)) {
            wp_send_json_error(['message' => $result['message'] ?? esc_html__('Unknown error occurred.', 'vulntitan')]);
        }

        wp_send_json_success([
            'item' => $item,
            'vulnerabilities' => $result['vulnerabilities'] ?? [],
            'meta' => $result['meta'] ?? [],
        ]);
    }

    protected function buildScanItem(string $type, string $pluginFile, string $themeSlug, string $slug): ?array
    {
        if ($type === 'plugin') {
            require_once ABSPATH . 'wp-admin/includes/plugin.php';
            $plugins = get_plugins();
            if (!isset($plugins[$pluginFile])) {
                return null;
            }

            $pluginData = $plugins[$pluginFile];
            $slugValue = $this->normalizePluginSlug($pluginFile, $pluginData);
            $isActive = is_plugin_active($pluginFile);
            $pluginUpdates = get_site_transient('update_plugins');
            $pluginUpdateResponse = is_object($pluginUpdates) && isset($pluginUpdates->response) && is_array($pluginUpdates->response)
                ? $pluginUpdates->response
                : [];
            $updateInfo = $pluginUpdateResponse[$pluginFile] ?? null;
            $hasFix = is_object($updateInfo) && !empty($updateInfo->new_version);
            $updateUrl = $hasFix
                ? wp_nonce_url(
                    self_admin_url('update.php?action=upgrade-plugin&plugin=' . rawurlencode($pluginFile)),
                    'upgrade-plugin_' . $pluginFile
                )
                : '';
            $deactivateUrl = $isActive
                ? wp_nonce_url(
                    self_admin_url('plugins.php?action=deactivate&plugin=' . rawurlencode($pluginFile)),
                    'deactivate-plugin_' . $pluginFile
                )
                : '';
            $openUrl = self_admin_url('plugins.php?plugin_status=all&s=' . rawurlencode((string) ($pluginData['Name'] ?? '')));

            return [
                'type' => 'plugin',
                'slug' => $slugValue,
                'plugin_file' => $pluginFile,
                'version' => $pluginData['Version'] ?? '',
                'label' => 'Plugin: ' . ($pluginData['Name'] ?? $pluginFile),
                'active' => $isActive,
                'has_fix' => $hasFix,
                'actions' => [
                    'update' => $updateUrl,
                    'deactivate' => $deactivateUrl,
                    'open' => $openUrl,
                ],
            ];
        }

        if ($type === 'theme') {
            $theme = wp_get_theme($themeSlug);
            if (!$theme || !$theme->exists()) {
                return null;
            }

            $version = $theme->get('Version');
            $name = $theme->get('Name');
            $themeUpdates = get_site_transient('update_themes');
            $themeUpdateResponse = is_object($themeUpdates) && isset($themeUpdates->response) && is_array($themeUpdates->response)
                ? $themeUpdates->response
                : [];
            $themeUpdate = $themeUpdateResponse[$themeSlug] ?? null;
            $hasFix = is_array($themeUpdate) && !empty($themeUpdate['new_version']);
            $updateUrl = $hasFix
                ? wp_nonce_url(
                    self_admin_url('update.php?action=upgrade-theme&theme=' . rawurlencode($themeSlug)),
                    'upgrade-theme_' . $themeSlug
                )
                : '';
            $openUrl = self_admin_url('themes.php?search=' . rawurlencode((string) $name));

            return [
                'type' => 'theme',
                'slug' => $themeSlug,
                'theme_slug' => $themeSlug,
                'version' => $version,
                'label' => 'Theme: ' . $name,
                'active' => (string) wp_get_theme()->get_stylesheet() === $themeSlug,
                'has_fix' => $hasFix,
                'actions' => [
                    'update' => $updateUrl,
                    'open' => $openUrl,
                ],
            ];
        }

        $coreUpdates = get_site_transient('update_core');
        $coreHasFix = false;
        if (is_object($coreUpdates) && !empty($coreUpdates->updates) && is_array($coreUpdates->updates)) {
            foreach ($coreUpdates->updates as $update) {
                if (is_object($update) && ($update->response ?? '') === 'upgrade') {
                    $coreHasFix = true;
                    break;
                }
            }
        }

        return [
            'type' => 'core',
            'slug' => $slug !== '' ? $slug : 'wordpress',
            'version' => get_bloginfo('version'),
            'label' => 'WordPress Core',
            'active' => true,
            'has_fix' => $coreHasFix,
            'actions' => [
                'update' => $coreHasFix ? self_admin_url('update-core.php') : '',
            ],
        ];
    }

    protected function normalizePluginSlug(string $pluginFile, array $pluginData): string
    {
        $slug = dirname($pluginFile);

        if ($slug === '.' || $slug === '') {
            $textDomain = trim((string) ($pluginData['TextDomain'] ?? ''));
            if ($textDomain !== '') {
                $slug = $textDomain;
            } else {
                $slug = basename($pluginFile, '.php');
            }
        }

        return $slug;
    }

    public function vulnRiskUpdate(): void
    {
        check_ajax_referer('vulntitan_ajax_scan', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(['message' => esc_html__('Insufficient permissions.', 'vulntitan')], 403);
        }

        // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
        $item = isset($_POST['item']) ? (array) wp_unslash($_POST['item']) : [];
        $vuln = isset($_POST['vulnerability']) ? (array) wp_unslash($_POST['vulnerability']) : [];
        $decision = isset($_POST['decision']) ? sanitize_key((string) wp_unslash($_POST['decision'])) : '';
        $expiresIn = isset($_POST['expires_in']) ? (int) wp_unslash($_POST['expires_in']) : 0;
        $note = isset($_POST['note']) ? sanitize_text_field((string) wp_unslash($_POST['note'])) : '';
        $itemType = sanitize_key($item['type'] ?? '');
        $itemVersion = sanitize_text_field($item['version'] ?? '');

        if (!in_array($decision, ['accept', 'ignore'], true) || $expiresIn <= 0 || $itemType === '' || $itemVersion === '') {
            wp_send_json_error(['message' => esc_html__('Invalid risk decision request.', 'vulntitan')], 400);
        }

        $result = VulnerabilityRiskService::setDecision($item, $vuln, $decision, $expiresIn, $note);

        if (!$result) {
            wp_send_json_error(['message' => esc_html__('Unable to save risk decision.', 'vulntitan')], 500);
        }

        wp_send_json_success([
            'decision' => $result,
        ]);
    }

    public function vulnRiskClear(): void
    {
        check_ajax_referer('vulntitan_ajax_scan', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(['message' => esc_html__('Insufficient permissions.', 'vulntitan')], 403);
        }

        // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
        $item = isset($_POST['item']) ? (array) wp_unslash($_POST['item']) : [];
        $vuln = isset($_POST['vulnerability']) ? (array) wp_unslash($_POST['vulnerability']) : [];
        $note = isset($_POST['note']) ? sanitize_text_field((string) wp_unslash($_POST['note'])) : '';
        $itemType = sanitize_key($item['type'] ?? '');
        $itemVersion = sanitize_text_field($item['version'] ?? '');

        if ($itemType === '' || $itemVersion === '') {
            wp_send_json_error(['message' => esc_html__('Invalid risk decision request.', 'vulntitan')], 400);
        }

        if (!VulnerabilityRiskService::clearDecision($item, $vuln, $note)) {
            wp_send_json_error(['message' => esc_html__('Unable to clear risk decision.', 'vulntitan')], 500);
        }

        wp_send_json_success(['cleared' => true]);
    }

    public function vulnRiskAudit(): void
    {
        check_ajax_referer('vulntitan_ajax_scan', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(['message' => esc_html__('Insufficient permissions.', 'vulntitan')], 403);
        }

        $uid = isset($_POST['risk_uid']) ? sanitize_text_field((string) wp_unslash($_POST['risk_uid'])) : '';

        if ($uid === '') {
            wp_send_json_error(['message' => esc_html__('Invalid audit request.', 'vulntitan')], 400);
        }

        $audit = VulnerabilityRiskService::getAudit($uid, 30);

        wp_send_json_success([
            'audit' => $audit,
        ]);
    }

    public function firewallGetData(): void
    {
        check_ajax_referer('vulntitan_ajax_scan', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(['message' => esc_html__('Insufficient permissions.', 'vulntitan')], 403);
        }

        FirewallService::ensureTable();

        wp_send_json_success([
            'settings' => FirewallService::getSettings(),
            'summary' => FirewallService::getSummary(24),
            'logs' => FirewallService::getRecentLogs(120),
            'approvals' => FirewallService::getPendingApprovals(80),
            'login_access' => FirewallService::getLoginAccessData(),
            'mu_loader' => FirewallService::getMuLoaderStatus(),
            'proxy_status' => FirewallService::getProxyStatus(),
        ]);
    }

    public function firewallSaveSettings(): void
    {
        check_ajax_referer('vulntitan_ajax_scan', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(['message' => esc_html__('Insufficient permissions.', 'vulntitan')], 403);
        }

        $customLoginValidation = FirewallService::validateCustomLoginSlug(
            isset($_POST['custom_login_slug']) ? (string) wp_unslash($_POST['custom_login_slug']) : ''
        );

        $input = [
            'enabled' => isset($_POST['enabled']) ? (int) wp_unslash($_POST['enabled']) : 1,
            'login_protection_enabled' => isset($_POST['login_protection_enabled']) ? (int) wp_unslash($_POST['login_protection_enabled']) : 1,
            'custom_login_slug' => $customLoginValidation['slug'],
            'two_factor_enabled' => isset($_POST['two_factor_enabled']) ? (int) wp_unslash($_POST['two_factor_enabled']) : 0,
            'two_factor_roles' => isset($_POST['two_factor_roles']) ? (array) wp_unslash($_POST['two_factor_roles']) : [],
            'trusted_device_days' => isset($_POST['trusted_device_days']) ? (int) wp_unslash($_POST['trusted_device_days']) : 30,
            'captcha_provider' => isset($_POST['captcha_provider']) ? (string) wp_unslash($_POST['captcha_provider']) : 'none',
            'captcha_site_key' => isset($_POST['captcha_site_key']) ? (string) wp_unslash($_POST['captcha_site_key']) : '',
            'captcha_secret_key' => isset($_POST['captcha_secret_key']) ? (string) wp_unslash($_POST['captcha_secret_key']) : '',
            'captcha_login_enabled' => isset($_POST['captcha_login_enabled']) ? (int) wp_unslash($_POST['captcha_login_enabled']) : 0,
            'captcha_register_enabled' => isset($_POST['captcha_register_enabled']) ? (int) wp_unslash($_POST['captcha_register_enabled']) : 0,
            'captcha_lostpassword_enabled' => isset($_POST['captcha_lostpassword_enabled']) ? (int) wp_unslash($_POST['captcha_lostpassword_enabled']) : 0,
            'captcha_comment_enabled' => isset($_POST['captcha_comment_enabled']) ? (int) wp_unslash($_POST['captcha_comment_enabled']) : 0,
            'xmlrpc_policy' => isset($_POST['xmlrpc_policy']) ? (string) wp_unslash($_POST['xmlrpc_policy']) : 'allow',
            'xmlrpc_allowlist_ips' => isset($_POST['xmlrpc_allowlist_ips']) ? (string) wp_unslash($_POST['xmlrpc_allowlist_ips']) : '',
            'lockout_allowlist_ips' => isset($_POST['lockout_allowlist_ips']) ? (string) wp_unslash($_POST['lockout_allowlist_ips']) : '',
            'xmlrpc_rate_limit_attempts' => isset($_POST['xmlrpc_rate_limit_attempts']) ? (int) wp_unslash($_POST['xmlrpc_rate_limit_attempts']) : 20,
            'xmlrpc_rate_limit_window_minutes' => isset($_POST['xmlrpc_rate_limit_window_minutes']) ? (int) wp_unslash($_POST['xmlrpc_rate_limit_window_minutes']) : 10,
            'weak_password_blocking_enabled' => isset($_POST['weak_password_blocking_enabled']) ? (int) wp_unslash($_POST['weak_password_blocking_enabled']) : 0,
            'waf_sqli_enabled' => isset($_POST['waf_sqli_enabled']) ? (int) wp_unslash($_POST['waf_sqli_enabled']) : 1,
            'waf_command_injection_enabled' => isset($_POST['waf_command_injection_enabled']) ? (int) wp_unslash($_POST['waf_command_injection_enabled']) : 1,
            'waf_whitelist_paths' => isset($_POST['waf_whitelist_paths']) ? (string) wp_unslash($_POST['waf_whitelist_paths']) : '',
            'learning_mode_enabled' => isset($_POST['learning_mode_enabled']) ? (int) wp_unslash($_POST['learning_mode_enabled']) : 0,
            'learning_suggestion_threshold' => isset($_POST['learning_suggestion_threshold']) ? (int) wp_unslash($_POST['learning_suggestion_threshold']) : 3,
            'learning_suggestion_window_days' => isset($_POST['learning_suggestion_window_days']) ? (int) wp_unslash($_POST['learning_suggestion_window_days']) : 7,
            'trust_cloudflare' => isset($_POST['trust_cloudflare']) ? (int) wp_unslash($_POST['trust_cloudflare']) : 0,
            'trusted_proxies' => isset($_POST['trusted_proxies']) ? (string) wp_unslash($_POST['trusted_proxies']) : '',
            'max_attempts' => isset($_POST['max_attempts']) ? (int) wp_unslash($_POST['max_attempts']) : 5,
            'lockout_minutes' => isset($_POST['lockout_minutes']) ? (int) wp_unslash($_POST['lockout_minutes']) : 15,
            'comment_shield_enabled' => isset($_POST['comment_shield_enabled']) ? (int) wp_unslash($_POST['comment_shield_enabled']) : 0,
            'form_shield_enabled' => isset($_POST['form_shield_enabled']) ? (int) wp_unslash($_POST['form_shield_enabled']) : 0,
            'form_provider_cf7_enabled' => isset($_POST['form_provider_cf7_enabled']) ? (int) wp_unslash($_POST['form_provider_cf7_enabled']) : 0,
            'form_provider_fluentforms_enabled' => isset($_POST['form_provider_fluentforms_enabled']) ? (int) wp_unslash($_POST['form_provider_fluentforms_enabled']) : 0,
            'cf7_shield_enabled' => isset($_POST['form_provider_cf7_enabled']) ? (int) wp_unslash($_POST['form_provider_cf7_enabled']) : (isset($_POST['cf7_shield_enabled']) ? (int) wp_unslash($_POST['cf7_shield_enabled']) : 0),
            'submission_min_submit_seconds' => isset($_POST['submission_min_submit_seconds']) ? (int) wp_unslash($_POST['submission_min_submit_seconds']) : 3,
            'submission_max_links' => isset($_POST['submission_max_links']) ? (int) wp_unslash($_POST['submission_max_links']) : 2,
            'submission_rate_limit_attempts' => isset($_POST['submission_rate_limit_attempts']) ? (int) wp_unslash($_POST['submission_rate_limit_attempts']) : 3,
            'submission_rate_limit_window_minutes' => isset($_POST['submission_rate_limit_window_minutes']) ? (int) wp_unslash($_POST['submission_rate_limit_window_minutes']) : 10,
            'comment_min_submit_seconds' => isset($_POST['submission_min_submit_seconds']) ? (int) wp_unslash($_POST['submission_min_submit_seconds']) : (isset($_POST['comment_min_submit_seconds']) ? (int) wp_unslash($_POST['comment_min_submit_seconds']) : 3),
            'comment_max_links' => isset($_POST['submission_max_links']) ? (int) wp_unslash($_POST['submission_max_links']) : (isset($_POST['comment_max_links']) ? (int) wp_unslash($_POST['comment_max_links']) : 2),
            'comment_rate_limit_attempts' => isset($_POST['submission_rate_limit_attempts']) ? (int) wp_unslash($_POST['submission_rate_limit_attempts']) : (isset($_POST['comment_rate_limit_attempts']) ? (int) wp_unslash($_POST['comment_rate_limit_attempts']) : 3),
            'comment_rate_limit_window_minutes' => isset($_POST['submission_rate_limit_window_minutes']) ? (int) wp_unslash($_POST['submission_rate_limit_window_minutes']) : (isset($_POST['comment_rate_limit_window_minutes']) ? (int) wp_unslash($_POST['comment_rate_limit_window_minutes']) : 10),
            'comment_suspicious_action' => isset($_POST['comment_suspicious_action']) ? (string) wp_unslash($_POST['comment_suspicious_action']) : 'hold',
            'log_retention_days' => isset($_POST['log_retention_days']) ? (int) wp_unslash($_POST['log_retention_days']) : 30,
            'weekly_summary_email_enabled' => isset($_POST['weekly_summary_email_enabled']) ? (int) wp_unslash($_POST['weekly_summary_email_enabled']) : 1,
            'weekly_summary_email_recipient' => isset($_POST['weekly_summary_email_recipient']) ? (string) wp_unslash($_POST['weekly_summary_email_recipient']) : '',
        ];

        $settings = FirewallService::saveSettings($input);
        $muLoaderResult = FirewallService::installMuLoader();
        $notice = '';

        if (($customLoginValidation['error'] ?? '') === 'invalid') {
            $notice = esc_html__('Custom login slug was not saved because it is invalid or reserved.', 'vulntitan');
        } elseif (($customLoginValidation['error'] ?? '') === 'conflict') {
            $notice = esc_html__('Custom login slug was not saved because that path is already used by existing WordPress content.', 'vulntitan');
        }

        wp_send_json_success([
            'settings' => $settings,
            'summary' => FirewallService::getSummary(24),
            'logs' => FirewallService::getRecentLogs(120),
            'approvals' => FirewallService::getPendingApprovals(80),
            'login_access' => FirewallService::getLoginAccessData(),
            'mu_loader' => FirewallService::getMuLoaderStatus(),
            'mu_loader_install' => $muLoaderResult,
            'notice' => $notice,
        ]);
    }

    public function firewallClearLogs(): void
    {
        check_ajax_referer('vulntitan_ajax_scan', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(['message' => esc_html__('Insufficient permissions.', 'vulntitan')], 403);
        }

        FirewallService::ensureTable();
        $deleted = FirewallService::clearLogs();

        wp_send_json_success([
            'deleted' => $deleted,
            'summary' => FirewallService::getSummary(24),
            'logs' => FirewallService::getRecentLogs(120),
            'approvals' => FirewallService::getPendingApprovals(80),
        ]);
    }

    public function firewallApproveRequest(): void
    {
        check_ajax_referer('vulntitan_ajax_scan', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(['message' => esc_html__('Insufficient permissions.', 'vulntitan')], 403);
        }

        $logId = isset($_POST['log_id']) ? (int) wp_unslash($_POST['log_id']) : 0;
        if ($logId <= 0) {
            wp_send_json_error(['message' => esc_html__('Invalid approval request.', 'vulntitan')], 400);
        }

        $approval = FirewallService::getApprovalCandidateById($logId);
        if (!$approval) {
            wp_send_json_error(['message' => esc_html__('Approval request not found or already handled.', 'vulntitan')], 404);
        }

        $pattern = (string)($approval['approval_pattern'] ?? '');
        if ($pattern === '') {
            wp_send_json_error(['message' => esc_html__('No safe whitelist pattern could be generated for this request.', 'vulntitan')], 400);
        }

        $settings = FirewallService::getSettings();
        $whitelist = $settings['waf_whitelist_paths'] ?? [];
        if (!is_array($whitelist)) {
            $whitelist = [];
        }
        $whitelist[] = $pattern;

        $settings = FirewallService::saveSettings([
            'waf_whitelist_paths' => $whitelist,
        ]);

        FirewallService::updateApprovalStatus($logId, 'approved', get_current_user_id(), $pattern);

        wp_send_json_success([
            'settings' => $settings,
            'approvals' => FirewallService::getPendingApprovals(80),
            'pattern' => $pattern,
        ]);
    }

    public function firewallDismissApproval(): void
    {
        check_ajax_referer('vulntitan_ajax_scan', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(['message' => esc_html__('Insufficient permissions.', 'vulntitan')], 403);
        }

        $logId = isset($_POST['log_id']) ? (int) wp_unslash($_POST['log_id']) : 0;
        if ($logId <= 0) {
            wp_send_json_error(['message' => esc_html__('Invalid approval request.', 'vulntitan')], 400);
        }

        $approval = FirewallService::getApprovalCandidateById($logId);
        if (!$approval) {
            wp_send_json_error(['message' => esc_html__('Approval request not found or already handled.', 'vulntitan')], 404);
        }

        FirewallService::updateApprovalStatus($logId, 'dismissed', get_current_user_id());

        wp_send_json_success([
            'approvals' => FirewallService::getPendingApprovals(80),
        ]);
    }

    public function firewallUnblockIp(): void
    {
        check_ajax_referer('vulntitan_ajax_scan', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(['message' => esc_html__('Insufficient permissions.', 'vulntitan')], 403);
        }

        $ipAddress = isset($_POST['ip']) ? trim((string) wp_unslash($_POST['ip'])) : '';
        if ($ipAddress === '' || !filter_var($ipAddress, FILTER_VALIDATE_IP)) {
            wp_send_json_error(['message' => esc_html__('Invalid IP address.', 'vulntitan')], 400);
        }

        $cleared = FirewallService::clearLoginLockout($ipAddress);

        wp_send_json_success([
            'ip' => $ipAddress,
            'cleared' => $cleared,
        ]);
    }

    public function firewallAllowlistIp(): void
    {
        check_ajax_referer('vulntitan_ajax_scan', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(['message' => esc_html__('Insufficient permissions.', 'vulntitan')], 403);
        }

        $ipAddress = isset($_POST['ip']) ? trim((string) wp_unslash($_POST['ip'])) : '';
        if ($ipAddress === '' || !filter_var($ipAddress, FILTER_VALIDATE_IP)) {
            wp_send_json_error(['message' => esc_html__('Invalid IP address.', 'vulntitan')], 400);
        }

        $settings = FirewallService::getSettings();
        $allowlist = $settings['lockout_allowlist_ips'] ?? [];

        if (!is_array($allowlist)) {
            $allowlist = [];
        }

        $alreadyAllowlisted = in_array($ipAddress, $allowlist, true);
        if (!$alreadyAllowlisted) {
            $allowlist[] = $ipAddress;
            $settings = FirewallService::saveSettings([
                'lockout_allowlist_ips' => $allowlist,
            ]);
        }

        FirewallService::clearLoginLockout($ipAddress);

        wp_send_json_success([
            'ip' => $ipAddress,
            'allowlisted' => !$alreadyAllowlisted,
            'settings' => $settings,
        ]);
    }

    public function firewallGetLearning(): void
    {
        check_ajax_referer('vulntitan_ajax_scan', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(['message' => esc_html__('Insufficient permissions.', 'vulntitan')], 403);
        }

        $settings = FirewallService::getSettings();
        $enabled = !empty($settings['learning_mode_enabled']);
        $suggestions = $enabled ? FirewallService::getLearningSuggestions(30) : [];

        wp_send_json_success([
            'enabled' => $enabled ? 1 : 0,
            'suggestions' => $suggestions,
        ]);
    }

    public function firewallApplyLearning(): void
    {
        check_ajax_referer('vulntitan_ajax_scan', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(['message' => esc_html__('Insufficient permissions.', 'vulntitan')], 403);
        }

        $pattern = isset($_POST['pattern']) ? trim((string) wp_unslash($_POST['pattern'])) : '';
        if ($pattern === '') {
            wp_send_json_error(['message' => esc_html__('Invalid learning suggestion.', 'vulntitan')], 400);
        }

        $result = FirewallService::applyLearningSuggestion($pattern);
        if (empty($result['success'])) {
            wp_send_json_error(['message' => esc_html__('Invalid learning suggestion.', 'vulntitan')], 400);
        }

        wp_send_json_success([
            'settings' => $result['settings'] ?? FirewallService::getSettings(),
            'suggestions' => FirewallService::getLearningSuggestions(30),
        ]);
    }

    public function firewallDismissLearning(): void
    {
        check_ajax_referer('vulntitan_ajax_scan', 'nonce');

        if (!current_user_can('manage_options')) {
            wp_send_json_error(['message' => esc_html__('Insufficient permissions.', 'vulntitan')], 403);
        }

        $pattern = isset($_POST['pattern']) ? trim((string) wp_unslash($_POST['pattern'])) : '';
        if ($pattern === '') {
            wp_send_json_error(['message' => esc_html__('Invalid learning suggestion.', 'vulntitan')], 400);
        }

        if (!FirewallService::dismissLearningSuggestion($pattern)) {
            wp_send_json_error(['message' => esc_html__('Invalid learning suggestion.', 'vulntitan')], 400);
        }

        wp_send_json_success([
            'suggestions' => FirewallService::getLearningSuggestions(30),
        ]);
    }

    public function malwareScanInit(): void
    {
        check_ajax_referer('vulntitan_ajax_scan', 'nonce');

        $this->requireFileOperationsCapability();

        $scanner = new MalwareScanner(get_option('vulntitan_malware_scan_scope', 'all'));

        wp_send_json_success([
            'files'     => $scanner->getFilesToScan(),
            'scope'     => $scanner->getScope(),
            'base_path' => $scanner->getBasePath(),
        ]);
    }

    public function malwareScanFile(): void
    {
        check_ajax_referer('vulntitan_ajax_scan', 'nonce');

        $this->requireFileOperationsCapability();

        $resolved = $this->resolveFilePathWithinWordPress((string) wp_unslash($_POST['file_path'] ?? ''));
        if ($resolved === null) {
            wp_send_json_error(['message' => esc_html__('Invalid or missing file.', 'vulntitan')]);
        }

        try {
            $scanner = new MalwareScanner(get_option('vulntitan_malware_scan_scope', 'all'));
            $result = $scanner->scanSingleFile($resolved['absolute']);

            wp_send_json_success([
                'findings'   => $result,
                'scope'      => $scanner->getScope(),
                'base_path'  => $scanner->getBasePath(),
            ]);
        } catch (Throwable $e) {
            wp_send_json_error([
                'message' => esc_html__('Scan failed.', 'vulntitan'),
                'error'   => $e->getMessage(),
            ]);
        }
    }

    public function malwareScanBatch(): void
    {
        check_ajax_referer('vulntitan_ajax_scan', 'nonce');

        $this->requireFileOperationsCapability();

        $filePaths = $_POST['file_paths'] ?? [];

        if (!is_array($filePaths) || empty($filePaths)) {
            wp_send_json_error(['message' => esc_html__('No files provided.', 'vulntitan')]);
        }

        $scanner = new MalwareScanner(get_option('vulntitan_malware_scan_scope', 'all'));
        $results = [];

        foreach ($filePaths as $inputPath) {
            $resolved = $this->resolveFilePathWithinWordPress((string) $inputPath);

            if ($resolved === null) {
                $results[] = [
                    'file' => ltrim(str_replace('\\', '/', (string) $inputPath), '/'),
                    'status' => 'skipped',
                    'skipped' => true,
                    'reason' => esc_html__('Invalid or unsafe file path.', 'vulntitan'),
                ];
                continue;
            }

            try {
                $findings = $scanner->scanSingleFile($resolved['absolute']);
                $results[] = [
                    'file'      => $resolved['relative'],
                    'status'    => !empty($findings) ? 'infected' : 'clean',
                    'findings'  => $findings,
                ];
            } catch (Throwable $e) {
                $results[] = [
                    'file'  => $resolved['relative'],
                    'status' => 'error',
                    'error' => $e->getMessage(),
                ];
            }
        }

        wp_send_json_success([
            'results'   => $results,
            'scope'     => $scanner->getScope(),
            'base_path' => $scanner->getBasePath(),
        ]);
    }

    public function malwareFixFinding(): void
    {
        check_ajax_referer('vulntitan_ajax_scan', 'nonce');

        $this->requireFileOperationsCapability();

        $filePath = sanitize_text_field(wp_unslash($_POST['file_path'] ?? ''));
        $lineNumber = isset($_POST['line']) ? (int) $_POST['line'] : 0;

        if ($filePath === '' || $lineNumber < 1) {
            wp_send_json_error(['message' => esc_html__('Invalid fix request.', 'vulntitan')]);
        }

        $resolved = $this->resolveFilePathWithinWordPress($filePath);
        if ($resolved === null) {
            wp_send_json_error(['message' => esc_html__('Invalid or unsafe file path.', 'vulntitan')]);
        }

        $relativePath = $resolved['relative'];
        $realAbsolute = $resolved['absolute'];
        $finding = $this->getFixableMalwareFinding($realAbsolute, $lineNumber);
        if ($finding === null) {
            wp_send_json_error(['message' => esc_html__('Auto-fix requires a current high-risk finding on the selected line.', 'vulntitan')]);
        }

        $extension = strtolower(pathinfo($realAbsolute, PATHINFO_EXTENSION));
        if (!in_array($extension, ['php', 'js'], true)) {
            wp_send_json_error(['message' => esc_html__('Unsupported file type for auto-fix.', 'vulntitan')]);
        }

        $lines = file($realAbsolute);
        if (!is_array($lines) || !isset($lines[$lineNumber - 1])) {
            wp_send_json_error(['message' => esc_html__('Requested line does not exist in file.', 'vulntitan')]);
        }

        $backup = $this->createMalwareFixBackup($realAbsolute, $relativePath);
        if (!$backup['success']) {
            wp_send_json_error(['message' => esc_html__('Failed to create backup before fix.', 'vulntitan')]);
        }

        $index = $lineNumber - 1;
        $originalLine = (string) $lines[$index];
        $lineEnding = '';
        if (substr($originalLine, -2) === "\r\n") {
            $lineEnding = "\r\n";
        } elseif (substr($originalLine, -1) === "\n") {
            $lineEnding = "\n";
        } elseif (substr($originalLine, -1) === "\r") {
            $lineEnding = "\r";
        }

        $updatedLine = $originalLine;
        $appliedMode = 'quarantine_line';
        $expectedCode = trim(substr((string) ($finding['code'] ?? ''), 0, 2000));
        $patterns = $this->normalizeFixablePatternList($finding['patterns'] ?? []);
        $normalizedExpectedCode = str_replace(["\r", "\n"], '', $expectedCode);

        // Attempt targeted snippet removal only for short snippets.
        if ($normalizedExpectedCode !== '' && strlen($normalizedExpectedCode) < 500) {
            $position = strpos($updatedLine, $normalizedExpectedCode);
            if ($position !== false) {
                $updatedLine = substr_replace($updatedLine, '', $position, strlen($normalizedExpectedCode));
                if ($updatedLine !== $originalLine && trim($updatedLine) !== '') {
                    $appliedMode = 'remove_snippet';
                }
            }
        }

        if ($appliedMode !== 'remove_snippet') {
            $updatedLine = $this->buildMalwareFixComment($lineNumber, $patterns) . $lineEnding;
        }

        $lines[$index] = $updatedLine;
        $written = file_put_contents($realAbsolute, implode('', $lines), LOCK_EX);

        if ($written === false) {
            @copy($backup['path'], $realAbsolute);
            wp_send_json_error(['message' => esc_html__('Failed to write file fix. Original content was restored from backup.', 'vulntitan')]);
        }

        wp_send_json_success([
            'message' => esc_html__('Fix applied successfully. Re-run malware scan to verify.', 'vulntitan'),
            'mode' => $appliedMode,
            'line' => $lineNumber,
            'file' => $relativePath,
            'backup_file' => $backup['relative'],
        ]);
    }

    protected function createMalwareFixBackup(string $absolutePath, string $relativePath): array
    {
        $backupDir = $this->resolveMalwareBackupDirectory();
        if ($backupDir === '') {
            return ['success' => false, 'path' => '', 'relative' => ''];
        }

        $safeName = preg_replace('/[^a-zA-Z0-9._-]/', '_', $relativePath);
        if (!$safeName) {
            $safeName = 'file';
        }
        $safeName = trim((string) $safeName, '_');
        if ($safeName === '') {
            $safeName = 'file';
        }
        $backupFile = sprintf(
            '%s/%s.%s.%s.bak',
            rtrim($backupDir, '/'),
            $safeName,
            gmdate('Ymd-His'),
            wp_generate_password(6, false, false)
        );

        if (!@copy($absolutePath, $backupFile)) {
            return ['success' => false, 'path' => '', 'relative' => ''];
        }

        return [
            'success' => true,
            'path' => $backupFile,
            'relative' => $this->formatBackupLabel($backupFile),
        ];
    }

    protected function resolveMalwareBackupDirectory(): string
    {
        $wpRoot = realpath(ABSPATH);
        $outsideDir = '';

        if ($wpRoot) {
            $outsideDir = dirname($wpRoot) . '/vulntitan-secure-backups/malware-fixes';
        }

        if ($outsideDir !== '' && $this->ensureBackupDirectory($outsideDir, true)) {
            return $outsideDir;
        }

        // Strict by default: do not keep sensitive backups in web-accessible plugin paths.
        $allowWebrootFallback = defined('VULNTITAN_ALLOW_WEBROOT_BACKUPS')
            && VULNTITAN_ALLOW_WEBROOT_BACKUPS === true;
        if (!$allowWebrootFallback) {
            return '';
        }

        $fallbackDir = VULNTITAN_PLUGIN_DIR . '/storage/backups/malware-fixes';
        if ($this->ensureBackupDirectory($fallbackDir, true)) {
            return $fallbackDir;
        }

        return '';
    }

    protected function ensureBackupDirectory(string $dir, bool $writeWebProtection): bool
    {
        if (!is_dir($dir) && !wp_mkdir_p($dir)) {
            return false;
        }

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

        if ($writeWebProtection) {
            $this->writeBackupWebProtectionFiles($dir);
        }

        return true;
    }

    protected function writeBackupWebProtectionFiles(string $dir): void
    {
        $indexFile = rtrim($dir, '/\\') . '/index.php';
        if (!file_exists($indexFile)) {
            @file_put_contents($indexFile, "<?php\n// Silence is golden.\n");
        }

        $htaccessFile = rtrim($dir, '/\\') . '/.htaccess';
        if (!file_exists($htaccessFile)) {
            @file_put_contents(
                $htaccessFile,
                "<IfModule mod_authz_core.c>\nRequire all denied\n</IfModule>\n<IfModule !mod_authz_core.c>\nDeny from all\n</IfModule>\n"
            );
        }

        $webConfigFile = rtrim($dir, '/\\') . '/web.config';
        if (!file_exists($webConfigFile)) {
            @file_put_contents(
                $webConfigFile,
                "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<configuration>\n  <system.webServer>\n    <authorization>\n      <deny users=\"*\" />\n    </authorization>\n  </system.webServer>\n</configuration>\n"
            );
        }
    }

    protected function formatBackupLabel(string $backupPath): string
    {
        $normalizedPluginDir = str_replace('\\', '/', rtrim(VULNTITAN_PLUGIN_DIR, '/\\'));
        $normalizedBackupPath = str_replace('\\', '/', $backupPath);

        if (strpos($normalizedBackupPath, $normalizedPluginDir . '/') === 0) {
            return ltrim(substr($normalizedBackupPath, strlen($normalizedPluginDir)), '/');
        }

        return 'external-secure/' . basename($backupPath);
    }

    protected function buildMalwareFixComment(int $lineNumber, string $patterns): string
    {
        $normalizedPatterns = preg_replace('/[^a-zA-Z0-9_,\-\s]/', '', $patterns);
        $suffix = $normalizedPatterns ? ' [' . trim((string) $normalizedPatterns) . ']' : '';

        return sprintf('/* VulnTitan quarantine: suspicious code removed at line %d%s */', $lineNumber, $suffix);
    }

    protected function getFixableMalwareFinding(string $absolutePath, int $lineNumber): ?array
    {
        try {
            $scanner = new MalwareScanner(get_option('vulntitan_malware_scan_scope', 'all'));
            $findings = $scanner->scanSingleFile($absolutePath);
        } catch (Throwable $e) {
            return null;
        }

        foreach ($findings as $finding) {
            if (!is_array($finding) || (int) ($finding['line'] ?? 0) !== $lineNumber) {
                continue;
            }

            $patterns = $this->normalizeFixablePatternList($finding['patterns'] ?? []);
            if (!$this->isFixablePatternList($patterns)) {
                continue;
            }

            return $finding;
        }

        return null;
    }

    /**
     * @param mixed $patterns
     */
    protected function normalizeFixablePatternList($patterns): string
    {
        if (!is_array($patterns)) {
            return '';
        }

        $items = [];
        foreach ($patterns as $pattern) {
            $value = sanitize_key((string) $pattern);
            if ($value !== '') {
                $items[] = $value;
            }
        }

        return implode(', ', array_values(array_unique($items)));
    }

    protected function isFixablePatternList(string $patterns): bool
    {
        $items = array_filter(array_map('trim', explode(',', strtolower($patterns))));
        if (empty($items)) {
            return false;
        }

        $fixablePatterns = [
            'eval', 'assert', 'create_function', 'preg_replace_eval',
            'system', 'exec', 'shell_exec', 'passthru', 'popen', 'proc_open',
            'include_user_input', 'file_put_contents', 'fwrite', 'move_uploaded_file',
            'js_function_ctor', 'js_eval_accessor', 'js_timer_string',
            'eval_base64_chain', 'decode_execution_chain', 'user_input_execution_chain',
            'encoded_file_write_chain', 'upload_input_chain',
        ];

        foreach ($items as $pattern) {
            if (in_array($pattern, $fixablePatterns, true)) {
                return true;
            }
        }

        return false;
    }

    protected function requireFileOperationsCapability(): void
    {
        if ($this->currentUserCanManageFileOperations()) {
            return;
        }

        wp_send_json_error(['message' => esc_html__('Insufficient permissions.', 'vulntitan')], 403);
    }

    protected function currentUserCanManageFileOperations(): bool
    {
        $defaultCapability = is_multisite() ? 'manage_network_plugins' : 'update_plugins';
        $capability = apply_filters(self::FILE_OPERATIONS_CAPABILITY_FILTER, $defaultCapability);
        $capability = is_string($capability) && $capability !== '' ? $capability : $defaultCapability;

        return current_user_can($capability);
    }

    protected function resolveFilePathWithinWordPress(string $inputPath): ?array
    {
        $inputPath = trim($inputPath);
        if ($inputPath === '') {
            return null;
        }

        $basePath = realpath(ABSPATH);
        if (!$basePath) {
            return null;
        }

        $normalizedBase = untrailingslashit(wp_normalize_path($basePath));
        $normalizedInput = wp_normalize_path($inputPath);

        if ($normalizedInput === $normalizedBase || strpos($normalizedInput, $normalizedBase . '/') === 0) {
            $relativePath = ltrim(substr($normalizedInput, strlen($normalizedBase)), '/');
        } else {
            $relativePath = ltrim(str_replace('\\', '/', $inputPath), '/');
        }

        if ($relativePath === '') {
            return null;
        }

        $absolutePath = realpath(trailingslashit($basePath) . $relativePath);
        if (!$absolutePath || !is_file($absolutePath) || !$this->isPathInsideBase($absolutePath, $basePath)) {
            return null;
        }

        $normalizedAbsolute = untrailingslashit(wp_normalize_path($absolutePath));

        return [
            'absolute' => $absolutePath,
            'relative' => ltrim(substr($normalizedAbsolute, strlen($normalizedBase)), '/'),
        ];
    }

    protected function isPathInsideBase(string $path, string $basePath): bool
    {
        $normalizedPath = untrailingslashit(wp_normalize_path($path));
        $normalizedBase = untrailingslashit(wp_normalize_path($basePath));

        return $normalizedPath === $normalizedBase || strpos($normalizedPath, $normalizedBase . '/') === 0;
    }

    public function integrityScanInit(): void
    {
        check_ajax_referer('vulntitan_ajax_scan', 'nonce');

        $this->requireFileOperationsCapability();

        if (!BaselineBuilder::exists()) {
            if (!BaselineBuilder::save()) {
                wp_send_json_error(['message' => esc_html__('Baseline generation failed.', 'vulntitan')]);
            }
            update_option('vulntitan_baseline_generated_at', current_time('mysql'));
        }

        $scanner = new FileIntegrityScanner();
        $scanner->loadCoreHashes();
        $scanner->loadBaseline();
        $queueId = wp_generate_uuid4();
        $queueDir = VULNTITAN_PLUGIN_DIR . '/storage/queues';
        $queueFile = $queueDir . '/integrity-' . $queueId . '.queue';
        $total = $scanner->writeFileQueue($queueFile);

        if ($total <= 0) {
            @unlink($queueFile);
            wp_send_json_success([
                'queue_id' => null,
                'total'    => 0,
            ]);
        }

        set_transient('vulntitan_integrity_queue_' . $queueId, [
            'file'  => $queueFile,
            'total' => $total,
        ], 6 * HOUR_IN_SECONDS);

        wp_send_json_success([
            'queue_id' => $queueId,
            'total'    => $total,
        ]);
    }

    public function integrityScanFile(): void
    {
        check_ajax_referer('vulntitan_ajax_scan', 'nonce');

        $this->requireFileOperationsCapability();

        $resolved = $this->resolveFilePathWithinWordPress((string) wp_unslash($_POST['file_path'] ?? ''));
        if ($resolved === null) {
            wp_send_json_error(['message' => esc_html__('File not found.', 'vulntitan')]);
        }

        try {
            $scanner = new FileIntegrityScanner();
            $scanner->loadCoreHashes();
            $scanner->loadBaseline();

            $result = $scanner->scanSingleFile($resolved['relative']);

            wp_send_json_success([
                'result' => $result,
            ]);
        } catch (Throwable $e) {
            wp_send_json_error([
                'message' => esc_html__('Integrity scan failed.', 'vulntitan'),
                'error'   => $e->getMessage(),
            ]);
        }
    }

    public function integrityScanBatch(): void
    {
        check_ajax_referer('vulntitan_ajax_scan', 'nonce');
        $startedAt = microtime(true);

        $this->requireFileOperationsCapability();
    
        $queueId = sanitize_text_field( wp_unslash( $_POST['queue_id'] ?? '' ) );
        $offset  = isset($_POST['offset']) ? max(0, (int) $_POST['offset']) : 0;
        $limit   = isset($_POST['limit']) ? max(1, min(120, (int) $_POST['limit'])) : 20;
    
        if (!$queueId) {
            wp_send_json_error(['message' => esc_html__('Missing queue id.', 'vulntitan')]);
        }
    
        $queueState = get_transient('vulntitan_integrity_queue_' . $queueId);
        $queueFile = is_array($queueState) ? ($queueState['file'] ?? '') : '';
    
        if (!$queueFile || !file_exists($queueFile)) {
            wp_send_json_error(['message' => esc_html__('Invalid or expired scan queue.', 'vulntitan')]);
        }
    
        $handle = fopen($queueFile, 'rb');
        if (!$handle) {
            wp_send_json_error(['message' => esc_html__('Unable to read scan queue.', 'vulntitan')]);
        }
    
        if ($offset > 0) {
            fseek($handle, $offset);
        }
    
        $filePaths = [];
        while (!feof($handle) && count($filePaths) < $limit) {
            $line = fgets($handle);
            if ($line === false) {
                break;
            }
            $line = trim($line);
            if ($line === '') {
                continue;
            }
            $filePaths[] = $line;
        }
    
        $nextOffset = ftell($handle);
        $done = feof($handle) || empty($filePaths);
        fclose($handle);
    
        $scanner = new FileIntegrityScanner();
        $scanner->loadCoreHashes();
        $scanner->loadBaseline();
    
        $results = [];
    
        foreach ($filePaths as $inputPath) {
            $relativePath = ltrim(str_replace('\\', '/', str_replace(ABSPATH, '', $inputPath)), '/');
            $absolutePath = ABSPATH . $relativePath;
    
            if (!file_exists($absolutePath)) {
                $results[] = [
                    'file_path' => $relativePath,
                    'status'    => 'missing',
                ];
                continue;
            }
    
            try {
                $scanResult = $scanner->scanSingleFile($relativePath);
    
                $results[] = [
                    'file_path' => $relativePath,
                    'status'    => $scanResult['status'] ?? 'unknown',
                ];
            } catch (Throwable $e) {
                $results[] = [
                    'file_path' => $relativePath,
                    'status'    => 'error',
                    'error'     => $e->getMessage(),
                ];
            }
        }

        if ($done) {
            delete_transient('vulntitan_integrity_queue_' . $queueId);
            @unlink($queueFile);
        }

        wp_send_json_success([
            'results'     => $results,
            'next_offset' => $nextOffset,
            'done'        => $done,
            'total'       => is_array($queueState) ? ($queueState['total'] ?? null) : null,
            'processing_ms' => (int) round((microtime(true) - $startedAt) * 1000),
            'limit_used'  => $limit,
        ]);
    }    
}
