<?php
/**
 * MalwareCleaner — Surgical file repair for quarantined items.
 *
 * Three-tier strategy:
 *   1. WordPress core files  → download checksum manifest, fetch clean ZIP, overwrite.
 *   2. Plugin / theme files  → fetch original ZIP from WordPress.org, overwrite.
 *   3. Unknown / custom files → strip known malicious code block via regex pattern stored
 *                               in ultraguard_malware_signatures and leave a clean marker comment.
 *
 * Rate limit: max 10 clean operations per 5 minutes (transient counter).
 * Every clean is logged via do_action('ultraguard_file_cleaned', ...).
 * On failure the quarantine record is left intact — never deleted.
 *
 * @package UltraguardPro\modules\antivirus
 */

namespace UltraguardPro\modules\antivirus;

use UltraguardPro\core\DatabaseCrud;
use UltraguardPro\core\Logger;
use UltraguardPro\support\SecureRemoteTransport;

if ( ! defined( 'ABSPATH' ) ) { exit; }

class MalwareCleaner {

    private const RATE_LIMIT_TRANSIENT = 'ultraguard_clean_rate_limit';
    private const RATE_LIMIT_MAX       = 10;
    private const RATE_LIMIT_WINDOW    = 300; // 5 minutes
    private const WP_CHECKSUMS_API     = 'https://api.wordpress.org/core/checksums/1.0/';
    private const WP_DOWNLOAD_BASE     = 'https://downloads.wordpress.org/release/';
    private const WP_PLUGIN_DL_BASE   = 'https://downloads.wordpress.org/plugin/';
    private const WP_THEME_DL_BASE    = 'https://downloads.wordpress.org/theme/';

    // ── Public API ─────────────────────────────────────────────────────────────

    /**
     * Clean a quarantined file identified by its quarantine record ID.
     *
     * @param  int $quarantine_id  Row ID in ultraguard_quarantine.
     * @return array{success:bool,method:string,backup_path:string,message:string}
     */
    public function clean( int $quarantine_id ): array {
        $fail = static function ( string $msg ): array {
            return [ 'success' => false, 'method' => '', 'backup_path' => '', 'message' => $msg ];
        };

        // ── Rate limit ────────────────────────────────────────────────────────
        if ( ! $this->check_rate_limit() ) {
            return $fail( __( 'Rate limit reached: maximum 10 clean operations per 5 minutes.', 'ultraguard-security-suite' ) );
        }

        // ── Load quarantine record ────────────────────────────────────────────
        $record = DatabaseCrud::get_by_id( 'ultraguard_quarantine', $quarantine_id );
        if ( ! $record ) {
            return $fail( __( 'Quarantine record not found.', 'ultraguard-security-suite' ) );
        }

        $original_path = is_array( $record ) ? ( $record['original_path'] ?? '' ) : ( $record->original_path ?? '' );
        $threat_type   = is_array( $record ) ? ( $record['threat_type']   ?? '' ) : ( $record->threat_type   ?? '' );

        if ( ! $original_path ) {
            return $fail( __( 'Original file path missing from quarantine record.', 'ultraguard-security-suite' ) );
        }

        // ── Path safety ───────────────────────────────────────────────────────
        $abs_base = realpath( ABSPATH );
        $real_orig = realpath( dirname( $original_path ) ) . DIRECTORY_SEPARATOR . basename( $original_path );

        if ( ! str_starts_with( $real_orig, $abs_base ) ) {
            return $fail( __( 'Original path is outside ABSPATH — aborting for safety.', 'ultraguard-security-suite' ) );
        }

        // ── Backup current state before any write ─────────────────────────────
        $backup_path = '';
        if ( file_exists( $original_path ) ) {
            $backup_path = $this->backup_before_clean( $original_path );
        }

        // ── Determine clean strategy ──────────────────────────────────────────
        $method  = '';
        $success = false;

        $wp_version = get_bloginfo( 'version' );

        if ( $this->is_core_file( $original_path ) ) {
            $success = $this->clean_core_file( $original_path, $wp_version );
            $method  = 'core';

        } elseif ( $plugin_info = $this->detect_plugin_file( $original_path ) ) {
            $success = $this->clean_plugin_file(
                $original_path,
                $plugin_info['slug'],
                $plugin_info['version']
            );
            $method = 'plugin';

        } elseif ( $theme_info = $this->detect_theme_file( $original_path ) ) {
            $success = $this->clean_theme_file(
                $original_path,
                $theme_info['slug'],
                $theme_info['version']
            );
            $method = 'theme';

        } else {
            // Fallback: surgical injection strip using stored signature pattern
            $sig_pattern = $this->get_signature_pattern( $threat_type );
            if ( $sig_pattern ) {
                $success = $this->strip_injection( $original_path, $sig_pattern );
                $method  = 'injection_strip';
            }
        }

        if ( ! $success ) {
            // Leave quarantine intact — never remove on failure
            Logger::warning( '[MalwareCleaner] Clean failed', [
                'quarantine_id' => $quarantine_id,
                'path'          => $original_path,
                'method'        => $method ?: 'none',
            ] );
            return [
                'success'     => false,
                'method'      => $method,
                'backup_path' => $backup_path,
                'message'     => __( 'Could not clean file automatically. Try manual restoration or deletion.', 'ultraguard-security-suite' ),
            ];
        }

        // ── Post-clean verification ───────────────────────────────────────────
        $verified = $this->verify_cleaned_file( $original_path );

        // ── Update quarantine record ──────────────────────────────────────────
        DatabaseCrud::update(
            'ultraguard_quarantine',
            [
                'clean_method'      => $method,
                'cleaned_at'        => gmdate( 'Y-m-d H:i:s' ),
                'clean_backup_path' => $backup_path,
                'status'            => 'cleaned',
            ],
            [ 'id' => $quarantine_id ]
        );

        do_action( 'ultraguard_file_cleaned', $original_path, $method, $quarantine_id, $backup_path );

        Logger::info( '[MalwareCleaner] File cleaned', [
            'quarantine_id' => $quarantine_id,
            'path'          => $original_path,
            'method'        => $method,
            'verified'      => $verified,
        ] );

        return [
            'success'     => true,
            'method'      => $method,
            'backup_path' => $backup_path,
            'message'     => sprintf(
                /* translators: %s: clean method (core/plugin/theme/injection_strip) */
                __( 'File cleaned successfully using method: %s', 'ultraguard-security-suite' ),
                esc_html( $method )
            ),
        ];
    }

    // ── Strategy: Core files ───────────────────────────────────────────────────

    /**
     * Replace a core WP file with the clean copy from WordPress.org.
     *
     * @param  string $path     Absolute path to the infected file.
     * @param  string $version  WordPress version string.
     * @return bool
     */
    public function clean_core_file( string $path, string $version ): bool {
        // Fetch checksum manifest
        $api_url   = add_query_arg(
            [ 'version' => urlencode( $version ), 'locale' => 'en_US' ],
            self::WP_CHECKSUMS_API
        );
        $api_resp  = SecureRemoteTransport::request_json( 'GET', $api_url, [ 'timeout' => 15 ] );
        if ( ! $api_resp['success'] ) {
            Logger::warning( '[MalwareCleaner] Checksums API error', [ 'error' => $api_resp['message'] ?? 'Unknown error' ] );
            return false;
        }

        $body      = is_array( $api_resp['body'] ?? null ) ? $api_resp['body'] : [];
        $checksums = $body['checksums'] ?? [];
        if ( empty( $checksums ) ) {
            return false;
        }

        // Resolve relative path inside WP root
        $abs_base    = trailingslashit( realpath( ABSPATH ) );
        $rel_path    = ltrim( str_replace( $abs_base, '', realpath( $path ) ?: $path ), '/\\' );

        if ( ! isset( $checksums[ $rel_path ] ) ) {
            // File not in checksums list (e.g. wp-config.php, custom files)
            return false;
        }

        // Download WP no-content ZIP and extract just this file
        $zip_url  = self::WP_DOWNLOAD_BASE . "wordpress-{$version}-no-content.zip";
        $zip_path = $this->download_clean_copy( 'core', '', $version, "wordpress-{$version}-no-content.zip" );

        if ( ! $zip_path ) {
            return false;
        }

        $extracted = $this->extract_single_file_from_zip(
            $zip_path,
            'wordpress/' . $rel_path,
            $path
        );

        wp_delete_file( $zip_path );

        if ( ! $extracted ) {
            return false;
        }

        // Verify hash matches checksum manifest
        return md5_file( $path ) === $checksums[ $rel_path ];
    }

    // ── Strategy: Plugin files ─────────────────────────────────────────────────

    /**
     * Replace a plugin file with its clean original from WordPress.org.
     *
     * @param  string $path         Absolute path to the infected file.
     * @param  string $plugin_slug  Plugin slug (e.g. 'contact-form-7').
     * @param  string $version      Plugin version string.
     * @return bool
     */
    public function clean_plugin_file( string $path, string $plugin_slug, string $version ): bool {
        $zip_name = sanitize_file_name( "{$plugin_slug}.{$version}.zip" );
        $zip_path = $this->download_clean_copy( 'plugin', $plugin_slug, $version, $zip_name );

        if ( ! $zip_path ) {
            return false;
        }

        // The relative path inside the zip: plugin-slug/...rest
        $plugins_dir = trailingslashit( realpath( WP_PLUGIN_DIR ) );
        $rel_in_zip  = $plugin_slug . '/' . ltrim(
            str_replace( $plugins_dir . $plugin_slug . '/', '', realpath( $path ) ?: $path ),
            '/\\'
        );

        $extracted = $this->extract_single_file_from_zip( $zip_path, $rel_in_zip, $path );
        wp_delete_file( $zip_path );

        return $extracted;
    }

    // ── Strategy: Theme files ──────────────────────────────────────────────────

    /**
     * Replace a theme file with its clean original from WordPress.org.
     *
     * @param  string $path        Absolute path to the infected file.
     * @param  string $theme_slug  Theme slug.
     * @param  string $version     Theme version string.
     * @return bool
     */
    public function clean_theme_file( string $path, string $theme_slug, string $version ): bool {
        $zip_name = sanitize_file_name( "{$theme_slug}.{$version}.zip" );
        $zip_path = $this->download_clean_copy( 'theme', $theme_slug, $version, $zip_name );

        if ( ! $zip_path ) {
            return false;
        }

        $themes_dir = trailingslashit( realpath( get_theme_root() ) );
        $rel_in_zip = $theme_slug . '/' . ltrim(
            str_replace( $themes_dir . $theme_slug . '/', '', realpath( $path ) ?: $path ),
            '/\\'
        );

        $extracted = $this->extract_single_file_from_zip( $zip_path, $rel_in_zip, $path );
        wp_delete_file( $zip_path );

        return $extracted;
    }

    // ── Strategy: Injection strip ──────────────────────────────────────────────

    /**
     * Surgically remove a known malicious code block from a file.
     * Leaves a dated comment at the strip location. Falls back gracefully
     * if the pattern produces no matches (file may have been modified).
     *
     * @param  string $path             Absolute path to the file.
     * @param  string $signature_pattern  Regex pattern (stored in DB, validated against allowlist).
     * @return bool
     */
    public function strip_injection( string $path, string $signature_pattern ): bool {
        if ( ! file_exists( $path ) || ! is_readable( $path ) ) {
            return false;
        }

        // Validate the pattern is well-formed before applying to file contents
        if ( @preg_match( $signature_pattern, '' ) === false ) {
            Logger::warning( '[MalwareCleaner] Invalid signature pattern — skipping strip', [
                'path'    => $path,
                'pattern' => substr( $signature_pattern, 0, 80 ),
            ] );
            return false;
        }

        $content = file_get_contents( $path );
        if ( $content === false ) {
            return false;
        }

        $stamp   = '/* CLEANED BY ULTRAGUARD ' . gmdate( 'Y-m-d H:i:s' ) . ' */';
        $cleaned = preg_replace( $signature_pattern, $stamp, $content );

        if ( $cleaned === null || $cleaned === $content ) {
            // Pattern not found — nothing to strip
            return false;
        }

        $written = file_put_contents( $path, $cleaned, LOCK_EX );
        return $written !== false;
    }

    // ── Backup helper ──────────────────────────────────────────────────────────

    /**
     * Copy the current file to the plugin's backup directory before overwriting.
     *
     * @param  string $path  Absolute path to the file.
     * @return string        Path to the backup copy, or empty string on failure.
     */
    public function backup_before_clean( string $path ): string {
        $upload_dir  = wp_upload_dir();
        $backup_dir  = trailingslashit( $upload_dir['basedir'] ) . 'ultraguard-security-suite/backups/';
        wp_mkdir_p( $backup_dir );

        // Protect the directory
        $idx = $backup_dir . 'index.php';
        if ( ! file_exists( $idx ) ) {
            file_put_contents( $idx, "<?php\n// Silence is golden\n" );
        }

        $backup_path = $backup_dir
            . sanitize_file_name( basename( $path ) )
            . '.' . gmdate( 'YmdHis' ) . '.bak';

        if ( ! @copy( $path, $backup_path ) ) {
            Logger::warning( '[MalwareCleaner] Could not create pre-clean backup', [ 'path' => $path ] );
            return '';
        }

        return $backup_path;
    }

    // ── Download helper ────────────────────────────────────────────────────────

    /**
     * Download a ZIP archive from WordPress.org to a temp file.
     *
     * @param  string $type     'core' | 'plugin' | 'theme'.
     * @param  string $slug     Component slug (empty for core).
     * @param  string $version  Component version.
     * @param  string $filename Filename segment for the URL.
     * @return string|false     Temp file path, or false on failure.
     */
    public function download_clean_copy( string $type, string $slug, string $version, string $filename ) {
        $url = match ( $type ) {
            'plugin' => self::WP_PLUGIN_DL_BASE . sanitize_file_name( $filename ),
            'theme'  => self::WP_THEME_DL_BASE  . sanitize_file_name( $filename ),
            default  => self::WP_DOWNLOAD_BASE   . sanitize_file_name( $filename ),
        };

        if ( ! filter_var( $url, FILTER_VALIDATE_URL ) ) {
            return false;
        }

        $tmp = wp_tempnam( $filename );
        if ( ! $tmp ) {
            return false;
        }

        $resp = SecureRemoteTransport::download( $url, $tmp, [
            'timeout'  => 60,
            'sslverify' => true,
        ] );

        if ( ! $resp['success'] ) {
            wp_delete_file( $tmp );
            Logger::warning( '[MalwareCleaner] Download failed', [
                'url'   => $url,
                'error' => $resp['message'] ?? 'unknown error',
            ] );
            return false;
        }

        return $tmp;
    }

    // ── Post-clean verification ────────────────────────────────────────────────

    /**
     * Re-scan the file after cleaning by checking it against active signatures.
     * Returns true if no signatures matched (file appears clean).
     *
     * @param  string $path  Absolute path to the cleaned file.
     * @return bool
     */
    public function verify_cleaned_file( string $path ): bool {
        if ( ! file_exists( $path ) ) {
            return false;
        }

        $content    = file_get_contents( $path );
        if ( $content === false ) {
            return false;
        }

        // Pull active patterns from the signatures table
        $sigs = DatabaseCrud::get_records( 'ultraguard_malware_signatures', [
            'where'  => [ 'is_active' => 1, 'type' => 'pattern' ],
            'limit'  => 500,
            'fields' => [ 'pattern' ],
        ] );

        if ( empty( $sigs ) ) {
            return true; // No signatures to check against
        }

        foreach ( $sigs as $sig ) {
            $pattern = is_array( $sig ) ? ( $sig['pattern'] ?? '' ) : ( $sig->pattern ?? '' );
            if ( ! $pattern ) {
                continue;
            }
            // Only apply patterns that pass a quick sanity check
            if ( @preg_match( $pattern, '' ) !== false && @preg_match( $pattern, $content ) ) {
                return false; // Still infected
            }
        }

        return true;
    }

    // ── Private helpers ────────────────────────────────────────────────────────

    /**
     * Enforce a sliding-window rate limit using a transient counter.
     * Increments on each call; returns false once the limit is reached.
     *
     * @return bool  True if within limit, false if exceeded.
     */
    private function check_rate_limit(): bool {
        $count = (int) get_transient( self::RATE_LIMIT_TRANSIENT );
        if ( $count >= self::RATE_LIMIT_MAX ) {
            return false;
        }
        if ( $count === 0 ) {
            set_transient( self::RATE_LIMIT_TRANSIENT, 1, self::RATE_LIMIT_WINDOW );
        } else {
            // Extend the existing transient value
            set_transient( self::RATE_LIMIT_TRANSIENT, $count + 1, self::RATE_LIMIT_WINDOW );
        }
        return true;
    }

    /**
     * Determine whether the file belongs to the WordPress core installation.
     *
     * @param  string $path  Absolute path to check.
     * @return bool
     */
    private function is_core_file( string $path ): bool {
        $abs  = trailingslashit( realpath( ABSPATH ) );
        $real = realpath( $path ) ?: $path;
        // Must be directly under ABSPATH and NOT inside wp-content
        return str_starts_with( $real, $abs )
            && ! str_starts_with( $real, trailingslashit( realpath( WP_CONTENT_DIR ) ) );
    }

    /**
     * Detect whether the file belongs to an installed plugin.
     *
     * @param  string $path  Absolute path to check.
     * @return array{slug:string,version:string}|null
     */
    private function detect_plugin_file( string $path ): ?array {
        $plugins_base = trailingslashit( realpath( WP_PLUGIN_DIR ) );
        $real         = realpath( $path ) ?: $path;

        if ( ! str_starts_with( $real, $plugins_base ) ) {
            return null;
        }

        // Second path segment after WP_PLUGIN_DIR is the plugin slug
        $relative = ltrim( str_replace( $plugins_base, '', $real ), '/\\' );
        $parts    = explode( DIRECTORY_SEPARATOR, $relative );
        $slug     = $parts[0] ?? '';

        if ( ! $slug ) {
            return null;
        }

        if ( ! function_exists( 'get_plugins' ) ) {
            require_once ABSPATH . 'wp-admin/includes/plugin.php';
        }
        foreach ( get_plugins() as $plugin_file => $data ) {
            if ( str_starts_with( $plugin_file, $slug . '/' ) || $plugin_file === $slug ) {
                return [
                    'slug'    => $slug,
                    'version' => $data['Version'] ?? '',
                ];
            }
        }
        return null;
    }

    /**
     * Detect whether the file belongs to an installed theme.
     *
     * @param  string $path  Absolute path to check.
     * @return array{slug:string,version:string}|null
     */
    private function detect_theme_file( string $path ): ?array {
        $themes_base = trailingslashit( realpath( get_theme_root() ) );
        $real        = realpath( $path ) ?: $path;

        if ( ! str_starts_with( $real, $themes_base ) ) {
            return null;
        }

        $relative = ltrim( str_replace( $themes_base, '', $real ), '/\\' );
        $parts    = explode( DIRECTORY_SEPARATOR, $relative );
        $slug     = $parts[0] ?? '';

        if ( ! $slug ) {
            return null;
        }

        $theme = wp_get_theme( $slug );
        if ( $theme->exists() ) {
            return [
                'slug'    => $slug,
                'version' => $theme->get( 'Version' ) ?: '',
            ];
        }

        return null;
    }

    /**
     * Retrieve the regex pattern stored for a given threat type.
     *
     * @param  string $threat_type  Threat-type label from the quarantine record.
     * @return string  Regex pattern, or empty string if not found.
     */
    private function get_signature_pattern( string $threat_type ): string {
        if ( ! $threat_type ) {
            return '';
        }

        $sig = DatabaseCrud::get_row( 'ultraguard_malware_signatures', [
            'name'      => $threat_type,
            'is_active' => 1,
            'type'      => 'pattern',
        ] );

        if ( ! $sig ) {
            return '';
        }

        return is_array( $sig ) ? ( $sig['pattern'] ?? '' ) : ( $sig->pattern ?? '' );
    }

    /**
     * Extract a single file from a ZIP archive and write it to a target path.
     *
     * @param  string $zip_path    Absolute path to the ZIP file.
     * @param  string $entry_name  Entry path inside the ZIP.
     * @param  string $dest_path   Absolute path to write the extracted content.
     * @return bool
     */
    private function extract_single_file_from_zip(
        string $zip_path,
        string $entry_name,
        string $dest_path
    ): bool {
        if ( ! class_exists( 'ZipArchive' ) ) {
            Logger::warning( '[MalwareCleaner] ZipArchive extension not available' );
            return false;
        }

        $zip = new \ZipArchive();
        if ( $zip->open( $zip_path ) !== true ) {
            return false;
        }

        $contents = $zip->getFromName( $entry_name );
        $zip->close();

        if ( $contents === false ) {
            Logger::warning( '[MalwareCleaner] Entry not found in ZIP', [
                'zip'   => basename( $zip_path ),
                'entry' => $entry_name,
            ] );
            return false;
        }

        // Ensure destination directory exists
        $dest_dir = dirname( $dest_path );
        if ( ! is_dir( $dest_dir ) ) {
            wp_mkdir_p( $dest_dir );
        }

        // Verify destination is inside ABSPATH before writing
        $abs_base = realpath( ABSPATH );
        $real_dir = realpath( $dest_dir );
        if ( ! $real_dir || ! str_starts_with( $real_dir, $abs_base ) ) {
            return false;
        }

        $written = file_put_contents( $dest_path, $contents, LOCK_EX );
        return $written !== false;
    }
}
