<?php

declare(strict_types=1);

namespace ShieldX\Scanner;

defined('ABSPATH') || exit;

final class ScanRuleEngine
{
    public const ENGINE_VERSION = 'shieldx-scanner-0.2.0';
    private const FILE_EVALUATION_BUDGET_SECONDS = 0.1;

    /**
     * @param array<string,mixed> $file
     * @return array<int,array<string,mixed>>
     */
    public function evaluateFile(array $file): array
    {
        $path = self::normalizePath((string) ($file['path'] ?? ''));
        $contents = (string) ($file['contents'] ?? '');
        $issues = [];
        $deadline = microtime(true) + self::FILE_EVALUATION_BUDGET_SECONDS;

        if (!empty($file['official_core_verified']) || !empty($file['official_plugin_verified'])) {
            return [];
        }

        if ($this->isSensitivePublicFile($path)) {
            $issues[] = $this->issue(
                'SX-SCAN-CONFIG-001',
                'sensitive_file',
                $this->sensitiveFileSeverity($path),
                'Sensitive configuration or backup file is present in a web-accessible tree.',
                $path,
                basename($path)
            );
        }

        if ($this->isDisclosureFile($path)) {
            $issues[] = $this->issue(
                'SX-SCAN-DISCLOSURE-001',
                'information_disclosure',
                'low',
                'Public metadata file can expose platform details to scanners.',
                $path,
                basename($path)
            );
        }

        if ($this->isExecutableUpload($path)) {
            $issues[] = $this->issue(
                'SX-SCAN-UPLOAD-001',
                'executable_upload',
                'high',
                'Executable PHP-like file was found inside uploads.',
                $path,
                pathinfo($path, PATHINFO_EXTENSION)
            );
        }

        if ($this->isUploadExtensionEvasion($path)) {
            $issues[] = $this->issue(
                'SX-SCAN-UPLOAD-002',
                'executable_upload',
                'high',
                'Upload filename uses double extensions or executable suffix evasion.',
                $path,
                basename($path)
            );
        }

        if ($contents === '') {
            return $issues;
        }

        $phpLike = $this->isPhpLikePath($path);
        $trustedCompatibilityFile = $this->isTrustedWordPressCompatibilityFile($path);

        if ($this->isSuspiciousHtaccess($path, $contents)) {
            $issues[] = $this->issue(
                'SX-SCAN-CONFIG-002',
                'suspicious_htaccess',
                'high',
                'A .htaccess file contains executable PHP handler, auto-prepend, or external rewrite directives.',
                $path,
                '.htaccess directive'
            );
        }

        if ($this->fileBudgetExceeded($deadline)) {
            return $issues;
        }

        if ($this->hasWpConfigCodeExecution($path, $contents)) {
            $issues[] = $this->issue(
                'SX-SCAN-CONFIG-003',
                'suspicious_config',
                'critical',
                'wp-config.php contains dynamic execution or obfuscation indicators.',
                $path,
                'wp-config code execution marker'
            );
        }

        if ($this->fileBudgetExceeded($deadline)) {
            return $issues;
        }

        if ($phpLike && $this->isUnexpectedAdminAssetPhp($path)) {
            $issues[] = $this->issue(
                'SX-SCAN-CORE-002',
                'unexpected_core_file',
                'high',
                'PHP-like file found inside a WordPress admin asset directory.',
                $path,
                'unexpected admin asset PHP'
            );
        }

        if ($this->fileBudgetExceeded($deadline)) {
            return $issues;
        }

        if ($phpLike && !$trustedCompatibilityFile && preg_match('~\b(?:eval|assert)\s*\(\s*(?:base64_decode|gzinflate|str_rot13)\s*\(~i', $contents)) {
            $issues[] = $this->issue(
                'SX-SCAN-MALWARE-001',
                'suspicious_php',
                'critical',
                'Encoded payload is passed into a dynamic code execution function.',
                $path,
                'encoded dynamic execution'
            );
        }

        if ($this->fileBudgetExceeded($deadline)) {
            return $issues;
        }

        if ($phpLike && !$trustedCompatibilityFile && $this->hasRequestDrivenCommandExecution($contents)) {
            $issues[] = $this->issue(
                'SX-SCAN-MALWARE-002',
                'suspicious_php',
                'high',
                'Request data appears to reach an operating-system command execution function.',
                $path,
                'request-controlled command sink'
            );
        }

        if ($this->fileBudgetExceeded($deadline)) {
            return $issues;
        }

        if ($phpLike && !$trustedCompatibilityFile && $this->hasObfuscationPattern($contents)) {
            $issues[] = $this->issue(
                'SX-SCAN-MALWARE-003',
                'suspicious_php',
                'medium',
                'Obfuscation pattern commonly used by webshells or malware was detected.',
                $path,
                'obfuscation heuristic'
            );
        }

        if ($this->fileBudgetExceeded($deadline)) {
            return $issues;
        }

        if ($phpLike && !$trustedCompatibilityFile && $this->hasWebshellIndicator($path, $contents)) {
            $issues[] = $this->issue(
                'SX-SCAN-MALWARE-004',
                'webshell_indicator',
                'critical',
                'Common webshell name, control parameter, or shell-management marker was detected.',
                $path,
                'webshell heuristic'
            );
        }

        if ($this->fileBudgetExceeded($deadline)) {
            return $issues;
        }

        if ($phpLike && !$trustedCompatibilityFile && $this->hasHiddenAdminCreation($contents)) {
            $issues[] = $this->issue(
                'SX-SCAN-MALWARE-005',
                'hidden_admin_creator',
                'critical',
                'PHP code appears to create or promote administrator accounts outside a normal onboarding flow.',
                $path,
                'administrator account creation marker'
            );
        }

        if ($this->fileBudgetExceeded($deadline)) {
            return $issues;
        }

        if ($phpLike && !$trustedCompatibilityFile && $this->hasSeoSpamInjection($contents)) {
            $issues[] = $this->issue(
                'SX-SCAN-MALWARE-006',
                'seo_spam_injector',
                'high',
                'PHP code appears to inject hidden SEO spam links or keyword content.',
                $path,
                'SEO spam injection marker'
            );
        }

        if ($this->fileBudgetExceeded($deadline)) {
            return $issues;
        }

        if ($phpLike && !$trustedCompatibilityFile && $this->hasSeoCloaking($contents)) {
            $issues[] = $this->issue(
                'SX-SCAN-MALWARE-007',
                'seo_cloaking',
                'high',
                'PHP code appears to serve different content or redirects to search crawlers.',
                $path,
                'crawler cloaking marker'
            );
        }

        if ($this->fileBudgetExceeded($deadline)) {
            return $issues;
        }

        if ($phpLike && !$trustedCompatibilityFile && $this->hasFakePluginHeader($path, $contents)) {
            $issues[] = $this->issue(
                'SX-SCAN-MALWARE-008',
                'fake_plugin_header',
                'medium',
                'A WordPress plugin header appears outside a normal plugin directory.',
                $path,
                'unexpected plugin header'
            );
        }

        if ($this->fileBudgetExceeded($deadline)) {
            return $issues;
        }

        if ($phpLike && !$trustedCompatibilityFile && $this->hasMaliciousCronRegistration($contents)) {
            $issues[] = $this->issue(
                'SX-SCAN-MALWARE-009',
                'malicious_cron',
                'high',
                'PHP code appears to register cron persistence with suspicious payload behavior.',
                $path,
                'suspicious cron persistence'
            );
        }

        if ($this->fileBudgetExceeded($deadline)) {
            return $issues;
        }

        if ($phpLike && !$trustedCompatibilityFile && $this->hasRemoteLoader($contents)) {
            $issues[] = $this->issue(
                'SX-SCAN-MALWARE-010',
                'remote_loader',
                'critical',
                'PHP code appears to fetch remote code or payloads and execute or include them.',
                $path,
                'remote payload loader'
            );
        }

        if ($this->fileBudgetExceeded($deadline)) {
            return $issues;
        }

        if ($phpLike && !$trustedCompatibilityFile && $this->hasVariableFunctionExecution($contents)) {
            $issues[] = $this->issue(
                'SX-SCAN-MALWARE-011',
                'variable_function_execution',
                'high',
                'Request-controlled data appears to select a PHP function for dynamic execution.',
                $path,
                'request-controlled function dispatch'
            );
        }

        if ($this->fileBudgetExceeded($deadline)) {
            return $issues;
        }

        if ($phpLike && !$trustedCompatibilityFile && $this->isPersistencePath($path) && $this->hasSuspiciousPersistenceContent($contents)) {
            $issues[] = $this->issue(
                'SX-SCAN-PERSISTENCE-001',
                'persistence_indicator',
                'high',
                'Must-use plugin or drop-in contains obfuscated, remote-load, or command-execution behavior.',
                $path,
                'persistent execution path'
            );
        }

        return $issues;
    }

    private function fileBudgetExceeded(float $deadline): bool
    {
        return microtime(true) >= $deadline;
    }

    /**
     * @param array<int,array<string,mixed>> $files
     * @return array<string,mixed>
     */
    public function scanFiles(array $files): array
    {
        $issues = [];
        $scanned = 0;

        foreach ($files as $file) {
            if (!is_array($file)) {
                continue;
            }

            $scanned++;
            foreach ($this->evaluateFile($file) as $issue) {
                $issues[] = $issue;
            }
        }

        return [
            'engine_version' => self::ENGINE_VERSION,
            'files_total' => count($files),
            'files_scanned' => $scanned,
            'issues' => $issues,
        ];
    }

    public static function normalizePath(string $path): string
    {
        $path = str_replace('\\', '/', $path);
        $path = preg_replace('~/+~', '/', $path) ?: $path;

        return ltrim($path, '/');
    }

    /**
     * @return array<int,string>
     */
    public function retiredFalsePositivePaths(): array
    {
        return [
            'wp-includes/js/plupload/wp-plupload.js',
            'wp-includes/class-snoopy.php',
        ];
    }

    private function isSensitivePublicFile(string $path): bool
    {
        $basename = strtolower(basename($path));

        if (strpos($path, '/.git/') !== false || strpos($path, '.git/') === 0) {
            return true;
        }

        if (preg_match('~^\.env(?:\..{1,80})?$~i', $basename)) {
            return true;
        }

        return (bool) preg_match(
            '~^(?:wp-config\.php\.(?:bak|old|orig|save|swp|txt|zip|tar|gz)|(?:config|database|db)\.(?:bak|old|sql|zip|tar|gz)|debug\.log)$~i',
            $basename
        );
    }

    private function sensitiveFileSeverity(string $path): string
    {
        $basename = strtolower(basename($path));

        if ($basename === '.env' || strpos($basename, 'wp-config.php.') === 0 || strpos($path, '/.git/') !== false) {
            return 'critical';
        }

        return 'high';
    }

    private function isDisclosureFile(string $path): bool
    {
        $basename = strtolower(basename($path));

        if (in_array($basename, ['phpinfo.php', 'info.php'], true)) {
            return true;
        }

        return strpos($path, '/') === false && in_array($basename, ['readme.html', 'license.txt'], true);
    }

    private function isExecutableUpload(string $path): bool
    {
        if (strpos('/' . $path, '/uploads/') === false) {
            return false;
        }

        return (bool) preg_match('~\.(?:php[0-9]?|phtml|phar|inc|shtml|cgi)$~i', $path);
    }

    private function isUploadExtensionEvasion(string $path): bool
    {
        if (strpos('/' . $path, '/uploads/') === false) {
            return false;
        }

        return (bool) preg_match('~\.(?:php[0-9]?|phtml|phar|shtml|cgi)\.(?:jpe?g|png|gif|webp|txt|pdf|zip)$~i', $path)
            || (bool) preg_match('~\.(?:jpe?g|png|gif|webp|txt|pdf)\.(?:php[0-9]?|phtml|phar|shtml|cgi)$~i', $path);
    }

    private function isSuspiciousHtaccess(string $path, string $contents): bool
    {
        if (strtolower(basename($path)) !== '.htaccess') {
            return false;
        }

        $contents = $this->withoutKnownGoodHtaccessAutoPrepend($contents);

        return (bool) preg_match('~(?:AddType|AddHandler|SetHandler)\s+application/x-httpd-php~i', $contents)
            || (bool) preg_match('~\b(?:auto_prepend_file|auto_append_file)\b~i', $contents)
            || (bool) preg_match('~RewriteRule\s+\S{1,512}\s+https?://~i', $contents)
            || (bool) preg_match('~\bbase64_decode\s*\(|FilesMatch\s+["]?\.\*\.(?:php|phtml|phar)~i', $contents);
    }

    private function withoutKnownGoodHtaccessAutoPrepend(string $contents): string
    {
        if (!preg_match('~^\s*(?:php_value|php_admin_value)\s+auto_prepend_file\s+["\']?[^"\'\r\n]*wordfence-waf\.php["\']?\s*$~im', $contents)) {
            return $contents;
        }

        $filtered = preg_replace(
            '~^\s*(?:php_value|php_admin_value)\s+auto_prepend_file\s+["\']?[^"\'\r\n]*wordfence-waf\.php["\']?\s*(?:\R|$)~im',
            '',
            $contents
        );

        return is_string($filtered) ? $filtered : $contents;
    }

    private function hasWpConfigCodeExecution(string $path, string $contents): bool
    {
        if (strtolower($path) !== 'wp-config.php') {
            return false;
        }

        return (bool) preg_match('~\b(?:eval|assert|base64_decode|gzinflate|str_rot13|shell_exec|system|passthru|proc_open|popen|exec)\s*\(~i', $contents);
    }

    private function isUnexpectedAdminAssetPhp(string $path): bool
    {
        return (bool) preg_match('~^wp-admin/(?:css|images|js|maint)/[^?]{1,240}\.(?:php[0-9]?|phtml|phar|inc)$~i', $path);
    }

    private function hasRequestDrivenCommandExecution(string $contents): bool
    {
        if (!preg_match('~\$_(?:GET|POST|REQUEST|COOKIE|FILES)\b~', $contents)) {
            return false;
        }

        return (bool) preg_match('~\b(?:shell_exec|system|passthru|proc_open|popen|exec)\s*\(~i', $contents);
    }

    private function hasObfuscationPattern(string $contents): bool
    {
        if (preg_match('~preg_replace\s*\([^;]{0,180}/[a-z]*e[a-z]*[\'"]~is', $contents)) {
            return true;
        }

        if (preg_match('~(?:str_rot13|gzinflate)\s*\(\s*base64_decode\s*\(~i', $contents)) {
            return true;
        }

        preg_match_all('~\bchr\s*\(\s*\d+\s*\)~i', $contents, $matches);

        return count($matches[0]) >= 8;
    }

    private function hasWebshellIndicator(string $path, string $contents): bool
    {
        if (preg_match('~\b(?:wso|c99|r57|b374k|indoxploit|filesman|alfacgiapi|minishell)\b~i', basename($path))) {
            return true;
        }

        if (preg_match('~\b(?:wso|c99|r57|b374k|indoxploit|filesman|alfacgiapi|minishell)\b~i', $contents)) {
            return true;
        }

        if (preg_match('~\$_(?:GET|POST|REQUEST|COOKIE)\s*\[\s*["](?:cmd|command|exec|shell|pass|pwd|0)["]\s*\]~i', $contents)
            && preg_match('~\b(?:shell_exec|system|passthru|proc_open|popen|exec)\s*\(~i', $contents)
        ) {
            return true;
        }

        return (bool) preg_match('~\b(?:filemanager|mass deface|safe_mode bypass|php shell|command shell)\b~i', $contents);
    }

    private function isPersistencePath(string $path): bool
    {
        $basename = strtolower(basename($path));

        return strpos($path, 'wp-content/mu-plugins/') === 0
            || in_array($basename, ['advanced-cache.php', 'db.php', 'object-cache.php', 'sunrise.php'], true);
    }

    private function hasSuspiciousPersistenceContent(string $contents): bool
    {
        return (bool) preg_match('~\b(?:eval|assert|base64_decode|gzinflate|str_rot13|shell_exec|system|passthru|curl_exec|file_get_contents)\s*\(~i', $contents);
    }

    private function hasHiddenAdminCreation(string $contents): bool
    {
        if (!preg_match('~\b(?:wp_create_user|wp_insert_user|wp_update_user|update_user_meta|add_user_to_blog|set_role)\s*\(~i', $contents)) {
            return false;
        }

        return (bool) preg_match('~\b(?:administrator|wp_capabilities|wp_user_level|manage_options|promote_user)\b~i', $contents)
            && (bool) preg_match('~\b(?:user_login|user_pass|user_email|admin_email|wp_generate_password|random_password)\b~i', $contents);
    }

    private function hasSeoSpamInjection(string $contents): bool
    {
        if (!preg_match('~\b(?:casino|viagra|cialis|levitra|payday\s+loan|payday|betting|poker|slots|pharma|porn|replica|essay|cheap\s+jerseys|seo\s+spam)\b~i', $contents)) {
            return false;
        }

        return (bool) preg_match('~\b(?:add_action|add_filter)\s*\(\s*["\'](?:wp_head|wp_footer|the_content|the_title|template_redirect)["\']~i', $contents)
            || (bool) preg_match('~\b(?:echo|print)\b[^;]{0,240}(?:<a\s+href=|display\s*:\s*none|visibility\s*:\s*hidden|left\s*:\s*-9999|font-size\s*:\s*0)~is', $contents)
            || (bool) preg_match('~(?:<a\s+href=|display\s*:\s*none|visibility\s*:\s*hidden|left\s*:\s*-9999|font-size\s*:\s*0)[^;]{0,240}\b(?:casino|viagra|cialis|payday|betting|poker|slots|pharma|porn|replica|essay)\b~is', $contents);
    }

    private function hasSeoCloaking(string $contents): bool
    {
        if (!preg_match('~\$_SERVER\s*\[\s*["\'](?:HTTP_USER_AGENT|HTTP_REFERER|REMOTE_ADDR)["\']\s*\]~i', $contents)) {
            return false;
        }

        return (bool) preg_match('~\b(?:googlebot|bingbot|slurp|baiduspider|yandexbot|duckduckbot|crawler|spider)\b~i', $contents)
            && (bool) preg_match('~\b(?:wp_redirect|header|include|include_once|require|require_once|readfile|file_get_contents|echo|print)\s*\(~i', $contents);
    }

    private function hasFakePluginHeader(string $path, string $contents): bool
    {
        if (strpos($path, 'wp-content/plugins/') === 0) {
            return false;
        }

        return (bool) preg_match('~(?:^|/\*)\s*Plugin\s+Name\s*:~im', $contents);
    }

    private function hasMaliciousCronRegistration(string $contents): bool
    {
        if (!preg_match('~\b(?:wp_schedule_event|wp_schedule_single_event|wp_schedule_recurring_event|add_action)\s*\(~i', $contents)) {
            return false;
        }

        if (preg_match('~\b(?:eval|assert|base64_decode|gzinflate|str_rot13|shell_exec|system|passthru|curl_exec)\s*\(~i', $contents)) {
            return true;
        }

        if (preg_match('~\b(?:wp_schedule_event|wp_schedule_single_event|wp_schedule_recurring_event|add_action)\s*\([^;]{0,160}\b(?:backdoor|webshell|payload|malware|inject)\b~isD', $contents)) {
            return true;
        }

        return $this->hasRemoteCodeExecutionFlow($contents);
    }

    private function hasRemoteLoader(string $contents): bool
    {
        if (!preg_match('~\b(?:file_get_contents|wp_remote_get|wp_remote_post|curl_exec|curl_setopt|fopen|readfile)\s*\(~i', $contents)) {
            return false;
        }

        if (!preg_match('~https?://~i', $contents)) {
            return false;
        }

        if (preg_match('~\b(?:eval|assert|include|include_once|require|require_once|create_function|call_user_func)\s*\(\s*(?:file_get_contents|wp_remote_get|wp_remote_post|curl_exec|fopen|readfile)\s*\(~is', $contents)) {
            return true;
        }

        if (preg_match('~\b(?:include|include_once|require|require_once)\s*\(\s*["\']https?://[^"\']{1,512}\.(?:php|txt|dat|inc|phtml)(?:\?[^"\']{0,256})?["\']~i', $contents)) {
            return true;
        }

        return $this->hasRemoteCodeExecutionFlow($contents);
    }

    private function hasRemoteCodeExecutionFlow(string $contents): bool
    {
        return (bool) preg_match('~\$\w+\s*=\s*(?:file_get_contents|wp_remote_get|wp_remote_post|curl_exec|fopen)\s*\(\s*["\']https?://[^;]{1,512};.{0,600}\b(?:eval|assert|include|include_once|require|require_once|create_function|call_user_func)\s*\(\s*\$\w+~is', $contents);
    }

    private function hasVariableFunctionExecution(string $contents): bool
    {
        if (!preg_match('~\$_(?:GET|POST|REQUEST|COOKIE)\b~', $contents)) {
            return false;
        }

        return (bool) preg_match('~(?:\$_(?:GET|POST|REQUEST|COOKIE)\s*\[[^\]]{1,96}\])\s*\(~i', $contents)
            || (bool) preg_match('~\$\w+\s*=\s*\$_(?:GET|POST|REQUEST|COOKIE)\s*\[[^\]]{1,96}\]\s*;[^;]{0,160}\$\w+\s*\(~is', $contents)
            || (bool) preg_match('~\bcall_user_func(?:_array)?\s*\(\s*(?:\$_(?:GET|POST|REQUEST|COOKIE)\s*\[[^\]]{1,96}\]|\$\w+)~i', $contents)
            || (bool) preg_match('~\$\{\s*\$_(?:GET|POST|REQUEST|COOKIE)\s*\[[^\]]{1,96}\]\s*\}\s*\(~i', $contents);
    }

    private function isPhpLikePath(string $path): bool
    {
        return (bool) preg_match('~\.(?:php[0-9]?|phtml|phar|inc)(?:\.(?:bak|old|orig|save|swp))?$~i', $path);
    }

    private function isTrustedWordPressCompatibilityFile(string $path): bool
    {
        return in_array($path, [
            'wp-includes/class-snoopy.php',
        ], true);
    }

    /**
     * @return array<string,mixed>
     */
    private function issue(string $ruleId, string $type, string $severity, string $description, string $path, string $evidence): array
    {
        return [
            'rule_id' => $ruleId,
            'type' => $type,
            'severity' => $severity,
            'status' => 'active',
            'source' => 'scanner',
            'description' => $description,
            'affected_path' => $path,
            'evidence' => $evidence,
            'threat_id' => $this->threatId($ruleId, $path),
        ];
    }

    private function threatId(string $ruleId, string $path): string
    {
        return 'sx-' . substr(sha1($ruleId . '|' . self::normalizePath($path)), 0, 29);
    }
}
