<?php

namespace VulnTitan\Services;

use Throwable;
use FilesystemIterator;
use UnexpectedValueException;
use RecursiveIteratorIterator;
use RecursiveDirectoryIterator;

class MalwareScanner
{
    protected string $scope;
    protected string $basePath;
    protected int $maxFindingsPerFile = 40;

    public function __construct(?string $scope = null)
    {
        $this->scope = $scope ?: 'all';

        switch ($this->scope) {
            case 'wp-content':
                $this->basePath = WP_CONTENT_DIR;
                break;
            case 'plugins':
                $this->basePath = WP_PLUGIN_DIR;
                break;
            case 'themes':
                $this->basePath = get_theme_root();
                break;
            default:
                $this->basePath = ABSPATH;
                break;
        }
    }

    public function getFilesToScan(): array
    {
        $files = [];

        $skipExtensions = get_option('vulntitan_malware_skip_extensions', 'js,png,jpg,jpeg,gif,bmp,tif,tiff,psd,svg,doc,docx,ttf,fla,flv,mov,mp3,pdf,css,pot,po,mo,so,exe,zip,7z,gz,rar');
        $skipExtensionsArray = array_filter(array_map('trim', explode(',', strtolower($skipExtensions))));

        $iterator = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($this->basePath, FilesystemIterator::SKIP_DOTS)
        );

        foreach ($iterator as $file) {
            $path = $file->getPathname();
            $extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));

            if (in_array($extension, ['php', 'js']) && !in_array($extension, $skipExtensionsArray, true)) {
                $files[] = $path;
            }
        }

        return $files;
    }

    public function scan(string $filePath): array
    {
        if (!is_file($filePath)) {
            throw new UnexpectedValueException(esc_html__('Not a file:', 'vulntitan') . ' ' . esc_attr($filePath));
        }

        $results = [];
        $extension = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));

        $patterns = [
            'eval'                 => ['regex' => '/\beval\s*\(/i', 'score' => 3, 'extensions' => ['php', 'js']],
            'assert'               => ['regex' => '/\bassert\s*\(/i', 'score' => 2, 'extensions' => ['php']],
            'base64_decode'        => ['regex' => '/\bbase64_decode\s*\(/i', 'score' => 2, 'extensions' => ['php']],
            'gzinflate'            => ['regex' => '/\bgzinflate\s*\(/i', 'score' => 2, 'extensions' => ['php']],
            'gzuncompress'         => ['regex' => '/\bgzuncompress\s*\(/i', 'score' => 2, 'extensions' => ['php']],
            'str_rot13'            => ['regex' => '/\bstr_rot13\s*\(/i', 'score' => 1, 'extensions' => ['php']],
            'strrev'               => ['regex' => '/\bstrrev\s*\(/i', 'score' => 1, 'extensions' => ['php']],
            'rawurldecode'         => ['regex' => '/\brawurldecode\s*\(/i', 'score' => 1, 'extensions' => ['php']],
            'urldecode'            => ['regex' => '/\burldecode\s*\(/i', 'score' => 1, 'extensions' => ['php']],
            'create_function'      => ['regex' => '/\bcreate_function\s*\(/i', 'score' => 2, 'extensions' => ['php']],
            'preg_replace_eval'    => ['regex' => '/\bpreg_replace\s*\(\s*[\'"][^\'"]*\/e[\'"]/i', 'score' => 3, 'extensions' => ['php']],
            'system'               => ['regex' => '/\bsystem\s*\(/i', 'score' => 3, 'extensions' => ['php']],
            'exec'                 => ['regex' => '/\bexec\s*\(/i', 'score' => 3, 'extensions' => ['php']],
            'shell_exec'           => ['regex' => '/\bshell_exec\s*\(/i', 'score' => 3, 'extensions' => ['php']],
            'passthru'             => ['regex' => '/\bpassthru\s*\(/i', 'score' => 3, 'extensions' => ['php']],
            'popen'                => ['regex' => '/\bpopen\s*\(/i', 'score' => 3, 'extensions' => ['php']],
            'proc_open'            => ['regex' => '/\bproc_open\s*\(/i', 'score' => 3, 'extensions' => ['php']],
            'superglobal_input'    => ['regex' => '/\$_(?:GET|POST|REQUEST|COOKIE|FILES|SERVER)\s*\[/i', 'score' => 1, 'extensions' => ['php']],
            'include_user_input'   => ['regex' => '/\b(?:include|require|include_once|require_once)\s*\(?\s*\$_(?:GET|POST|REQUEST|COOKIE)\s*\[/i', 'score' => 3, 'extensions' => ['php']],
            'file_put_contents'    => ['regex' => '/\bfile_put_contents\s*\(/i', 'score' => 1, 'extensions' => ['php']],
            'fwrite'               => ['regex' => '/\bfwrite\s*\(/i', 'score' => 1, 'extensions' => ['php']],
            'move_uploaded_file'   => ['regex' => '/\bmove_uploaded_file\s*\(/i', 'score' => 1, 'extensions' => ['php']],
            'error_reporting_0'    => ['regex' => '/\berror_reporting\s*\(\s*0\s*\)/i', 'score' => 1, 'extensions' => ['php']],
            'ini_set_display_err0' => ['regex' => '/\bini_set\s*\(\s*[\'"]display_errors[\'"]\s*,\s*[\'"]?0[\'"]?\s*\)/i', 'score' => 1, 'extensions' => ['php']],
            'base64_blob'          => ['regex' => '/[A-Za-z0-9+\/]{200,}={0,2}/', 'score' => 2, 'extensions' => ['php', 'js']],
            'chr_chain'            => ['regex' => '/(?:\bchr\s*\(\s*\d{1,3}\s*\)\s*\.\s*){3,}\bchr\s*\(\s*\d{1,3}\s*\)/i', 'score' => 2, 'extensions' => ['php']],
            'js_function_ctor'     => ['regex' => '/\bFunction\s*\(/i', 'score' => 2, 'extensions' => ['js']],
            'js_atob'              => ['regex' => '/\batob\s*\(/i', 'score' => 1, 'extensions' => ['js']],
            'js_unescape'          => ['regex' => '/\bunescape\s*\(/i', 'score' => 1, 'extensions' => ['js']],
            'js_document_write'    => ['regex' => '/\bdocument\.write\s*\(/i', 'score' => 1, 'extensions' => ['js']],
            'js_fromcharcode'      => ['regex' => '/\bString\.fromCharCode\s*\(\s*(?:\d{2,3}\s*,\s*){6,}\d{2,3}\s*\)/i', 'score' => 3, 'extensions' => ['js']],
            'js_eval_accessor'     => ['regex' => '/\b(?:window|this)\s*\[\s*[\'"]eval[\'"]\s*\]/i', 'score' => 2, 'extensions' => ['js']],
            'js_timer_string'      => ['regex' => '/\bset(?:Timeout|Interval)\s*\(\s*[\'"][^\'"]+/i', 'score' => 1, 'extensions' => ['js']],
        ];

        $executionPatterns = [
            'eval', 'assert', 'create_function', 'preg_replace_eval',
            'system', 'exec', 'shell_exec', 'passthru', 'popen', 'proc_open',
            'js_function_ctor', 'js_eval_accessor',
        ];
        $decodePatterns = [
            'base64_decode', 'gzinflate', 'gzuncompress', 'str_rot13', 'strrev',
            'rawurldecode', 'urldecode', 'base64_blob', 'chr_chain',
            'js_atob', 'js_unescape', 'js_fromcharcode',
        ];

        $lines = file($filePath, FILE_IGNORE_NEW_LINES);
        if (!is_array($lines)) {
            return [];
        }

        $previewRadius = 2;
        $scoreThreshold = $extension === 'js' ? 5 : 4;

        foreach ($lines as $lineNumber => $line) {
            $lineClean = trim($line);
            if ($lineClean === '') {
                continue;
            }

            // Reduce false positives from suspicious keywords inside static strings/comments.
            $lineCodeOnly = $this->stripStringLiterals($lineClean);
            $score = 0;
            $matchedPatterns = [];
            $rawMatchPatterns = ['base64_blob', 'preg_replace_eval', 'js_eval_accessor', 'js_timer_string'];

            // Match each pattern
            foreach ($patterns as $name => $rule) {
                $targets = $rule['extensions'] ?? ['php', 'js'];
                if (!in_array($extension, $targets, true)) {
                    continue;
                }

                $subject = in_array($name, $rawMatchPatterns, true) ? $lineClean : $lineCodeOnly;

                if (preg_match($rule['regex'], $subject)) {
                    $score += $rule['score'];
                    $matchedPatterns[] = $name;
                }
            }

            $hasExecution = !empty(array_intersect($executionPatterns, $matchedPatterns));
            $hasFileWrite = !empty(array_intersect(['file_put_contents', 'fwrite', 'move_uploaded_file'], $matchedPatterns));
            $hasBenignDataImage = $this->isBenignDataImageLine($lineClean) || $this->isBenignSvgDataUriLine($lineClean);
            $hasSerializedBlob = $this->isLikelySerializedDataLine($lineClean);
            $hasInput = in_array('superglobal_input', $matchedPatterns, true) || in_array('include_user_input', $matchedPatterns, true);
            $hasBase64Blob = in_array('base64_blob', $matchedPatterns, true);

            // Reduce false positives for base64-decoded signatures/keys/certs.
            if (
                $this->isBenignBase64Context($lineClean)
                && in_array('base64_decode', $matchedPatterns, true)
                && in_array('base64_blob', $matchedPatterns, true)
                && !$hasExecution
                && !$hasFileWrite
            ) {
                $score -= $patterns['base64_blob']['score'];
                $matchedPatterns = array_values(array_diff($matchedPatterns, ['base64_blob']));
            }

            // Reduce false positives for decode-only utilities without execution or file writes.
            if (
                !$hasBase64Blob
                && !$hasExecution
                && !$hasFileWrite
                && !$hasInput
                && !empty($matchedPatterns)
                && empty(array_diff($matchedPatterns, $decodePatterns))
            ) {
                $score = max(0, $score - 1);
            }

            // Combo detection
            if (in_array('eval', $matchedPatterns) && in_array('base64_decode', $matchedPatterns)) {
                $score += 2;
                $matchedPatterns[] = 'eval_base64_chain';
            }

            if (!empty(array_intersect($executionPatterns, $matchedPatterns)) && !empty(array_intersect($decodePatterns, $matchedPatterns))) {
                $score += 2;
                $matchedPatterns[] = 'decode_execution_chain';
            }

            if (in_array('superglobal_input', $matchedPatterns, true) && !empty(array_intersect($executionPatterns, $matchedPatterns))) {
                $score += 3;
                $matchedPatterns[] = 'user_input_execution_chain';
            }

            if (in_array('file_put_contents', $matchedPatterns, true) && !empty(array_intersect($decodePatterns, $matchedPatterns))) {
                $score += 2;
                $matchedPatterns[] = 'encoded_file_write_chain';
            }

            if (in_array('move_uploaded_file', $matchedPatterns, true) && in_array('superglobal_input', $matchedPatterns, true)) {
                $score += 1;
                $matchedPatterns[] = 'upload_input_chain';
            }

            // Ignore benign CSS content declarations embedded in JS/PHP strings.
            if ($this->isLikelyCssContentLine($lineClean) && empty(array_intersect($executionPatterns, $matchedPatterns))) {
                continue;
            }

            // Long line (obfuscation)
            if (strlen($lineClean) > 1000) {
                if (!(
                    (!$hasExecution && !$hasFileWrite)
                    && ($hasBenignDataImage || $hasSerializedBlob)
                )) {
                    $score += 2;
                    $matchedPatterns[] = 'obfuscated_line';
                }
            }

            // Only return if score is meaningful
            if ($score >= $scoreThreshold) {
                $matchedPatterns = array_values(array_unique($matchedPatterns));
                $severity = $score >= 10 ? 'high' : ($score >= 7 ? 'medium' : 'low');
                $previewStart = max(0, $lineNumber - $previewRadius);
                $previewEnd = min(count($lines) - 1, $lineNumber + $previewRadius);
                $preview = [];

                for ($i = $previewStart; $i <= $previewEnd; $i++) {
                    $preview[] = [
                        'line' => $i + 1,
                        'code' => substr((string) $lines[$i], 0, 500),
                        'hit'  => $i === $lineNumber,
                    ];
                }

                $results[] = [
                    'line'         => $lineNumber + 1,
                    'score'        => $score,
                    'patterns'     => $matchedPatterns,
                    'severity'     => $severity,
                    'code_snippet' => substr($lineClean, 0, 500),
                    'preview'      => $preview,
                ];

                if (count($results) >= $this->maxFindingsPerFile) {
                    break;
                }
            }
        }

        return $results;
    }

    protected function stripStringLiterals(string $line): string
    {
        // Remove quoted literals (single, double, template) then trim inline comments.
        $line = (string) preg_replace('/"(?:\\\\.|[^"\\\\])*"|\'(?:\\\\.|[^\'\\\\])*\'|`(?:\\\\.|[^`\\\\])*`/u', '""', $line);
        $line = (string) preg_replace('/\/\/.*$/', '', $line);
        $line = (string) preg_replace('/#.*$/', '', $line);

        return $line;
    }

    protected function isLikelyCssContentLine(string $line): bool
    {
        // Examples: content:""; content: "\f101"; content:'•';
        return (bool) preg_match('/\bcontent\s*:\s*(["\']).*?\1\s*;?/i', $line);
    }

    protected function isBenignDataImageLine(string $line): bool
    {
        // Allowlist common raster image data URIs; exclude SVG because it can be scriptable.
        return (bool) preg_match('/data:image\/(?:png|jpe?g|gif|webp|bmp|ico);base64,[A-Za-z0-9+\/=]+/i', $line);
    }

    protected function isBenignSvgDataUriLine(string $line): bool
    {
        if (!preg_match('/data:image\\/svg\\+xml;base64,([A-Za-z0-9+\\/=]+)/i', $line, $matches)) {
            return false;
        }

        $payload = $matches[1] ?? '';
        if ($payload === '' || strlen($payload) > 20000) {
            return false;
        }

        $decoded = base64_decode($payload, true);
        if ($decoded === false) {
            return false;
        }

        $lower = strtolower($decoded);
        if (strpos($lower, '<svg') === false) {
            return false;
        }

        if (preg_match('/<script|\\bon\\w+\\s*=|foreignobject|<iframe|<object|<embed|javascript:/i', $decoded)) {
            return false;
        }

        return true;
    }

    protected function isLikelySerializedDataLine(string $line): bool
    {
        // Large serialized option blobs (e.g., security plugin caches).
        return (bool) (preg_match('/\ba:\d+:{/i', $line) && preg_match('/\bs:\d+:"[A-Za-z0-9+\/=]{200,}"/i', $line));
    }

    protected function isBenignBase64Context(string $line): bool
    {
        if (preg_match('/\$(?:signature|sig|public[_]?key|private[_]?key|pubkey|privkey|cert(?:ificate)?|pem|csr|x509)\b/i', $line)) {
            return true;
        }

        if (preg_match('/\b(?:openssl_(?:verify|sign|public_decrypt|private_decrypt|public_encrypt|private_encrypt|pkey_get_public|pkey_get_private|x509_read)|hash_equals|sodium_crypto_sign(?:_verify|_open)?|jwt)\b/i', $line)) {
            return true;
        }

        return false;
    }

    public function scanSingleFile(string $filePath): array
    {
        $raw = $this->scan($filePath);
        $findings = [];

        foreach ($raw as $issue) {
            $patterns = implode(', ', $issue['patterns']);
            $line     = $issue['line'];
            $code     = $issue['code_snippet'];
            $severity = $issue['severity'];

            $findings[] = [
                'type'       => $patterns,
                'message'    => "Line {$line} ({$severity}): " . trim($code),
                'line'       => $line,
                'severity'   => $severity,
                'patterns'   => $issue['patterns'],
                'code'       => $code,
                'fixable'    => $this->isFixableFinding($issue['patterns'] ?? [], $severity),
                'preview'    => $issue['preview'] ?? [],
            ];
        }

        return $findings;
    }

    public function processFiles(): array
    {
        $results = [];
        $files = $this->getFilesToScan();

        foreach ($files as $filePath) {
            try {
                $issues = $this->scan($filePath);

                if (!empty($issues)) {
                    $relativePath = ltrim(str_replace(ABSPATH, '', $filePath), '/\\');
                    $results[$relativePath] = $issues;
                }
            } catch (Throwable $e) {
                continue;
            }
        }

        return $results;
    }

    public function getScope(): string
    {
        return $this->scope;
    }

    public function getBasePath(): string
    {
        $path = realpath(ABSPATH);
        if (!$path) {
            return 'wp-root';
        }

        // Try to go one level up (e.g., public_html)
        $parent = dirname($path);
        $parentName = basename($parent);
        $currentName = basename($path);

        // If parent contains wp-config.php, assume it's public_html or root
        if (file_exists($parent . '/wp-config.php')) {
            return $parentName . '/';
        }

        // Fallback to current dir name (wp or public or app/public, etc.)
        return $currentName . '/';
    }

    public function getEngine(): string
    {
        return 'VulnTitan';
    }

    protected function isFixableFinding(array $patterns, string $severity): bool
    {
        if (strtolower($severity) === 'low') {
            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 ($patterns as $pattern) {
            if (in_array($pattern, $fixablePatterns, true)) {
                return true;
            }
        }

        return false;
    }
}
