<?php

namespace VulnTitan\Services;

use FilesystemIterator;
use RecursiveIteratorIterator;
use RecursiveDirectoryIterator;
use RecursiveCallbackFilterIterator;

class FileIntegrityScanner
{
    protected string $wpVersion;
    public array $coreHashes = [];
    public array $baseline = [];
    protected array $scannedFiles = [];

    protected array $skipExtensions = [
        'gif', 'png', 'jpg', 'jpeg', 'jpe', 'jfif', 'pjpeg', 'pjp', 'svg', 'webp', 'bmp', 'ico', 'avif', 'tif', 'tiff', 'heic', 'heif',
        'css', 'scss',
        'log', 'queue', 'zip', 'tar', 'gz', 'rar', '7z',
        'map', 'json', 'md', 'txt', 'csv',
        'lock', 'sqlite', 'db', 'ds_store',
        'woff', 'woff2', 'ttf', 'otf', 'eot',
    ];
    
    protected array $skipFolders = [
        'node_modules',
        'vendor',
        '.git',
        'uploads',
        'cache',
        'backups',
        '.svn'
    ];

    public function __construct(array $baseline = [])
    {
        $this->wpVersion = get_bloginfo('version');
        $this->baseline = $this->normalizeBaselinePaths($baseline);
    }

    public function loadBaseline(): void
    {
        $file = VULNTITAN_BASELINE_FILE;

        if (file_exists($file)) {
            $raw = json_decode(file_get_contents($file), true) ?? [];
            $this->baseline = $this->normalizeBaselinePaths($raw);
        }
    }

    public function loadCoreHashes(): void
    {
        $locale = get_locale();
        $cacheKey = 'vulntitan_core_hashes_' . md5($this->wpVersion . '|' . $locale);
        $cached = get_transient($cacheKey);
        if (is_array($cached) && !empty($cached)) {
            $this->coreHashes = $cached;
            return;
        }

        $response = wp_remote_get("https://api.wordpress.org/core/checksums/1.0/?version={$this->wpVersion}&locale={$locale}");

        if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
            $data = json_decode(wp_remote_retrieve_body($response), true);
            if (!empty($data['files'])) {
                $this->coreHashes = $this->normalizeBaselinePaths($data['files']);
                set_transient($cacheKey, $this->coreHashes, 6 * HOUR_IN_SECONDS);
            }
        }
    }

    public function scanSingleFile(string $relativePath): array
    {
        $absolutePath = realpath(ABSPATH . $relativePath);

        if (!$absolutePath) {
            return [
                'file_path'     => $relativePath,
                'status'        => 'missing',
                'actual_hash'   => null,
                'expected_hash' => null,
                'is_core'       => false,
            ];
        }

        $normalizedPath = $this->normalizePath($absolutePath);
        $isCore = isset($this->coreHashes[$normalizedPath]);
        $expectedHash = $isCore
            ? $this->coreHashes[$normalizedPath]
            : ($this->baseline[$normalizedPath] ?? null);

        $actualHash = null;
        if ($expectedHash) {
            $actualHash = sha1_file($absolutePath);
        }

        if (!$expectedHash) {
            $status = 'unexpected';
        } elseif ($actualHash !== $expectedHash) {
            $status = 'modified';
        } else {
            $status = 'ok';
        }

        return [
            'file_path'     => $normalizedPath,
            'status'        => $status,
            'actual_hash'   => $actualHash,
            'expected_hash' => $expectedHash,
            'is_core'       => $isCore,
        ];
    }

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

        foreach ($this->iterateFilePaths() as $relativePath) {
            $files[] = $relativePath;
        }

        return $files;
    }

    public function collectQueuePaths(bool $includeUnexpected = true): array
    {
        $files = [];

        foreach ($this->iterateQueuePaths($includeUnexpected) as $relativePath) {
            $files[] = $relativePath;
        }

        return $files;
    }

    public function writeFileQueue(string $queueFile): int
    {
        $dir = dirname($queueFile);
        if (!is_dir($dir)) {
            wp_mkdir_p($dir);
        }

        $handle = fopen($queueFile, 'wb');
        if (!$handle) {
            return 0;
        }

        $count = 0;
        foreach ($this->iterateQueuePaths(true) as $relativePath) {
            fwrite($handle, $relativePath . "\n");
            $count++;
        }

        fclose($handle);

        return $count;
    }

    protected function iterateQueuePaths(bool $includeUnexpected): \Generator
    {
        $seen = [];

        foreach (array_keys($this->coreHashes) as $path) {
            if ($this->shouldSkipByExtension($path)) {
                continue;
            }
            if (!isset($seen[$path])) {
                $seen[$path] = true;
                yield $path;
            }
        }

        foreach (array_keys($this->baseline) as $path) {
            if ($this->shouldSkipByExtension($path)) {
                continue;
            }
            if (!isset($seen[$path])) {
                $seen[$path] = true;
                yield $path;
            }
        }

        if (!$includeUnexpected) {
            return;
        }

        foreach ($this->iterateContentFilePaths() as $path) {
            if ($this->shouldSkipByExtension($path)) {
                continue;
            }
            if (!isset($seen[$path])) {
                $seen[$path] = true;
                yield $path;
            }
        }
    }

    protected function iterateContentFilePaths(): \Generator
    {
        foreach ($this->getContentDirectories() as $dir) {
            foreach ($this->iterateDirectoryFiles($dir) as $path) {
                yield $path;
            }
        }
    }

    protected function getContentDirectories(): array
    {
        $dirs = [];

        if (defined('WP_PLUGIN_DIR') && is_dir(WP_PLUGIN_DIR)) {
            $dirs[] = WP_PLUGIN_DIR;
        }

        if (defined('WPMU_PLUGIN_DIR') && is_dir(WPMU_PLUGIN_DIR)) {
            $dirs[] = WPMU_PLUGIN_DIR;
        }

        foreach (wp_get_themes() as $theme) {
            $dir = $theme->get_stylesheet_directory();
            if ($dir && is_dir($dir)) {
                $dirs[] = $dir;
            }
        }

        return array_values(array_unique($dirs));
    }

    protected function iterateDirectoryFiles(string $dir): \Generator
    {
        $directoryIterator = new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS);
        $filter = new RecursiveCallbackFilterIterator(
            $directoryIterator,
            function ($current) {
                if ($current->isDir()) {
                    return !in_array($current->getFilename(), $this->skipFolders, true);
                }
                return true;
            }
        );
        $iterator = new RecursiveIteratorIterator($filter);

        foreach ($iterator as $fileInfo) {
            if (!$fileInfo->isFile()) {
                continue;
            }

            $filePath = $fileInfo->getPathname();
            if ($this->shouldSkipByExtension($filePath)) {
                continue;
            }

            yield $this->normalizePath($filePath);
        }
    }

    protected function iterateFilePaths(): \Generator
    {
        $directoryIterator = new RecursiveDirectoryIterator(ABSPATH, FilesystemIterator::SKIP_DOTS);
        $filter = new RecursiveCallbackFilterIterator(
            $directoryIterator,
            function ($current) {
                if ($current->isDir()) {
                    return !in_array($current->getFilename(), $this->skipFolders, true);
                }
                return true;
            }
        );
        $iterator = new RecursiveIteratorIterator($filter);

        foreach ($iterator as $fileInfo) {
            if (!$fileInfo->isFile()) {
                continue;
            }

            $filePath = $fileInfo->getPathname();
            if ($this->shouldSkipByExtension($filePath)) {
                continue;
            }

            yield $this->normalizePath($filePath);
        }
    }

    protected function getAllFiles(string $dir): array
    {
        $files = [];

        if (!is_dir($dir)) {
            return $files;
        }

        $rii = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator(ABSPATH, FilesystemIterator::SKIP_DOTS)
        );

        foreach ($rii as $file) {
            if (!$file->isFile()) continue;

            $filePath = $file->getPathname();
            $relativePath = $this->normalizePath($filePath);

            if (
                $this->startsWith($relativePath, 'wp-content/uploads/') ||
                $this->startsWith($relativePath, 'wp-content/cache/') ||
                $this->startsWith($relativePath, 'wp-content/backups/')
            ) continue;

            if (preg_match('/\.(log|tmp|bak|zip|gz|tar|rar|7z)$/i', $filePath)) continue;

            $files[] = $filePath;
        }

        return $files;
    }

    protected function normalizePath(string $path): string
    {
        return ltrim(str_replace(['\\', '//'], '/', str_replace(realpath(ABSPATH), '', realpath($path) ?: $path)), '/');
    }

    protected function startsWith(string $haystack, string $needle): bool
    {
        if ($needle === '') {
            return true;
        }

        return strncmp($haystack, $needle, strlen($needle)) === 0;
    }

    protected function shouldSkipByExtension(string $path): bool
    {
        $extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));

        return $extension !== '' && in_array($extension, $this->skipExtensions, true);
    }

    protected function normalizeBaselinePaths(array $array): array
    {
        $normalized = [];

        foreach ($array as $path => $hash) {
            $key = $this->normalizePath($path);
            $normalized[$key] = $hash;
        }

        return $normalized;
    }
}
