<?php

declare(strict_types=1);

namespace SecurWP\Modules\Scanner;

use SecurWP\Modules\Scanner\Service\IssueType;
use SecurWP\Modules\Scanner\Service\ScanLogger;
use SecurWP\Modules\Scanner\Repository\IssueRepository;

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

/**
 * Class VulnerabilityScanner
 * 
 * Adapter for the Cloud\VulnerabilitySync module.
 * Triggers the sync process and transforms results into standard ScanResult objects.
 */
class VulnerabilityScanner  {

    private bool $should_sync;

    public function __construct( bool $should_sync = true ) {
        $this->should_sync = $should_sync;
    }

    /**
     * Get the scanner title.
     * 
     * @return string
     */
    public function get_title() {
        return 'Vulnerability Scanner';
    }

    /**
     * Run the vulnerability scan (sync).
     *
     * @return array List of ScanResult arrays
     */
    public function run() {
        if ( class_exists( ScanLogger::class ) ) {
            ScanLogger::log( "Checking for known vulnerabilities..." );
        }

        if ( $this->should_sync ) {
            $sync_class = '\\SecurWP\\Modules\\Cloud\\Services\\VulnerabilitySync';
            
            if ( ! class_exists( $sync_class ) ) {
                $path = dirname( __DIR__ ) . '/Cloud/Services/VulnerabilitySync.php';
                if ( file_exists( $path ) ) {
                    require_once $path;
                }
            }

            if ( ! class_exists( $sync_class ) ) {
                if ( class_exists( ScanLogger::class ) ) {
                    ScanLogger::log( "Vulnerability scanner unavailable: sync service class missing." );
                }
                securwp_log( 'SecurWP: Vulnerability sync service class missing.' );
                return [ 'issues' => [], 'count' => 0, 'complete' => false, 'failed' => true ];
            }

            try {
                $syncer = new $sync_class();
                $sync_ok = $syncer->run();
            } catch ( \RuntimeException $e ) {
                if ( class_exists( ScanLogger::class ) ) {
                    ScanLogger::log( "Failed to check vulnerabilities: " . $e->getMessage() );
                }
                securwp_log( 'SecurWP: Vulnerability sync execution failed: ' . $e->getMessage() );
                return [ 'issues' => [], 'count' => 0, 'complete' => false, 'failed' => true ];
            }
        } else {
            $sync_ok = true;
        }

        $raw_issues = \SecurWP\Includes\SettingsStore::get_field( \SecurWP\Includes\SettingsStore::COL_STATE, 'vulnerability_issues', [] );
        if ( ! is_array( $raw_issues ) ) {
            $raw_issues = [];
        }

        if ( $sync_ok !== true ) {
            securwp_log( 'SecurWP: Vulnerability sync failed.' );
            return [ 'issues' => [], 'count' => 0, 'complete' => false, 'failed' => true ];
        }

        if ( ! function_exists( 'get_plugins' ) ) {
            require_once ABSPATH . 'wp-admin/includes/plugin.php';
        }
        $installed_plugins = get_plugins();
        $installed_themes  = wp_get_themes();
        $active_theme = wp_get_theme();
        $active_stylesheet = $active_theme instanceof \WP_Theme ? (string) $active_theme->get_stylesheet() : '';
        $active_template = $active_theme instanceof \WP_Theme ? (string) $active_theme->get_template() : '';

        $results = [];

        // Plugins
        if ( ! empty( $raw_issues['plugins'] ) && is_array( $raw_issues['plugins'] ) ) {
            $plugin_updates = $this->get_plugin_update_map();
            foreach ( $raw_issues['plugins'] as $slug => $issues ) {
                if ( ! is_string( $slug ) || ! is_array( $issues ) ) {
                    continue;
                }

                $version = 'Unknown';
                $is_installed = false;
                $asset_name = $slug;
                $available_update_version = '';

                $plugin_file = '';
                foreach( $installed_plugins as $file => $data ) {
                    $matches_slug = ( dirname( $file ) === $slug ) || ( $file === $slug ) || ( strpos( $file, $slug . '/' ) === 0 );
                    if ( $matches_slug ) {
                        $is_installed = true;
                        $plugin_file  = $file;
                        $version = $data['Version'];
                        $asset_name = (string) ( $data['Name'] ?? $slug );
                        if ( isset( $plugin_updates[ $file ] ) ) {
                            $available_update_version = $this->resolve_available_update_version( $plugin_updates[ $file ] );
                        }
                        break;
                    }
                }

                // Strict source-of-truth: never render vulnerabilities for assets not currently installed.
                if ( ! $is_installed ) {
                    continue;
                }

                foreach ( $issues as $issue_data ) {
                    $ranges = $this->parse_affected_versions( $issue_data['affected_versions'] ?? null );
                    if ( $version !== 'Unknown' && ! empty( $ranges ) ) {
                        if ( ! $this->is_version_affected( $version, $ranges ) ) {
                            continue;
                        }
                    }
                    $can_update = $this->is_safe_update_candidate( $available_update_version, $ranges );
                    $results[] = $this->transform( $issue_data, 'plugin', $slug, $version, $can_update, $plugin_file, null, $asset_name, $available_update_version );
                }
            }
        }

        // Themes
        if ( ! empty( $raw_issues['themes'] ) && is_array( $raw_issues['themes'] ) ) {
            $theme_updates = $this->get_theme_update_map();
            foreach ( $raw_issues['themes'] as $slug => $issues ) {
                if ( ! is_string( $slug ) || ! is_array( $issues ) ) {
                    continue;
                }

                $version = 'Unknown';
                $asset_name = $slug;
                $available_update_version = '';
                if ( isset( $installed_themes[$slug] ) ) {
                    $version = $installed_themes[$slug]->get( 'Version' );
                    $asset_name = (string) $installed_themes[$slug]->get( 'Name' );
                    if ( isset( $theme_updates[ $slug ] ) ) {
                        $available_update_version = $this->resolve_available_update_version( $theme_updates[ $slug ] );
                    }
                    $is_active = (
                        ( $active_stylesheet !== '' && $active_stylesheet === $slug ) ||
                        ( $active_template !== '' && $active_template === $slug )
                    );
                } else {
                    // Strict source-of-truth: ignore stale cloud cache rows for removed themes.
                    continue;
                }

                foreach ( $issues as $issue_data ) {
                    $ranges = $this->parse_affected_versions( $issue_data['affected_versions'] ?? null );
                    if ( $version !== 'Unknown' && ! empty( $ranges ) ) {
                        if ( ! $this->is_version_affected( $version, $ranges ) ) {
                            continue;
                        }
                    }
                    $can_update = $this->is_safe_update_candidate( $available_update_version, $ranges );
                    $results[] = $this->transform( $issue_data, 'theme', $slug, $version, $can_update, '', $is_active, $asset_name, $available_update_version );
                }
            }
        }
        
        // WordPress Core
         if ( ! empty( $raw_issues['wordpress'] ) && is_array( $raw_issues['wordpress'] ) ) {
             global $wp_version;
             foreach ( $raw_issues['wordpress'] as $issue_data ) {
                $ranges = $this->parse_affected_versions( $issue_data['affected_versions'] ?? null );
                if ( ! empty( $ranges ) && ! $this->is_version_affected( $wp_version, $ranges ) ) {
                    continue;
                }
                
                $results[] = $this->transform( $issue_data, 'core', 'wordpress', $wp_version, false, '', null, 'WordPress Core' );
            }
        }

        if ( class_exists( ScanLogger::class ) ) {
            ScanLogger::log( "Vulnerability check complete. Found " . count( $results ) . " known issues." );
        }

        return [
            'issues'   => $results,
            'count'    => count( $results ),
            'complete' => true,
            'failed'   => false,
        ];
    }

    /**
     * Normalize affected_versions to a plain PHP array of range strings.
     * Handles: array, JSON string (["<3.1.5"]), plain string ("<3.1.5").
     *
     * @param mixed $value
     * @return array
     */
    private function parse_affected_versions( $value ): array {
        if ( empty( $value ) ) {
            return [];
        }
        if ( is_array( $value ) ) {
            return $value;
        }
        if ( is_string( $value ) ) {
            if ( $value[0] === '[' ) {
                $decoded = json_decode( $value, true );
                if ( is_array( $decoded ) ) {
                    return $decoded;
                }
            }
            return [ $value ];
        }
        return [];
    }

    /**
     * Check if installed version matches any of the affected ranges.
     *
     * Each element in $ranges is ORed (the version is affected if it matches any range).
     * Within a single range string, multiple constraints separated by ',' or ' - ' are ANDed.
     *
     * Supported formats per element:
     *   "<=6.3.6"                  - single comparator
     *   "6.3.7 - 6.3.8"           - dash-separated lower/upper bound (ANDed)
     *   ">=6.3.7,<=6.3.8"         - comma-separated compound constraints (ANDed)
     *   ">=6.3.7, <=6.3.8"        - same with spaces
     *
     * @param string $installed
     * @param array  $ranges
     * @return bool
     */
    private function is_version_affected( $installed, $ranges ) {
        foreach ( $ranges as $range_str ) {
            $range_str = trim( (string) $range_str );

            // Format: "X - Y" (dash-separated lower - upper bound, both ANDed)
            if ( strpos( $range_str, ' - ' ) !== false ) {
                $parts = explode( ' - ', $range_str, 2 );
                if (
                    $this->satisfies( $installed, trim( $parts[0] ), '>=' ) &&
                    $this->satisfies( $installed, trim( $parts[1] ), '<=' )
                ) {
                    return true;
                }
                continue;
            }

            // Format: ">=X,<=Y" or ">=X, <=Y" (comma-separated compound, all ANDed)
            if ( strpos( $range_str, ',' ) !== false ) {
                $parts = array_map( 'trim', explode( ',', $range_str ) );
                $all_match = true;
                foreach ( $parts as $part ) {
                    if ( ! $this->satisfies( $installed, $part ) ) {
                        $all_match = false;
                        break;
                    }
                }
                if ( $all_match ) {
                    return true;
                }
                continue;
            }

            // Single comparator: ">=X", "<=X", "<X", etc.
            if ( $this->satisfies( $installed, $range_str ) ) {
                return true;
            }
        }
        return false;
    }

    /**
     * Helper to compare version against a single constraint string (e.g. ">=3.7").
     *
     * @param string $installed   Installed version.
     * @param string $constraint  Constraint string (e.g. ">=3.7" or bare "3.7").
     * @param string $default_op  Operator to assume when constraint has no prefix.
     */
    private function satisfies( $installed, $constraint, $default_op = '' ) {
        $constraint = trim( $constraint );
        if ( preg_match( '/^([<>=!]+)\s*(.+)$/', $constraint, $matches ) ) {
            $op     = $matches[1];
            $target = trim( $matches[2] );
            return version_compare( $installed, $target, $op );
        }
        if ( $default_op !== '' ) {
            return version_compare( $installed, $constraint, $default_op );
        }
        return false;
    }

    /**
     * Transform raw cloud issue to standard ScanResult.
     */
    private function transform( $issue, $type, $slug, $current_version = 'Unknown', $can_update = false, $plugin_file = '', ?bool $is_theme_active = null, $asset_name = '', $available_update_version = '' ) {
        $raw_severity = strtolower( $issue['severity'] ?? 'medium' );
        $severity = $raw_severity;
        if ( ! in_array( $severity, ['critical', 'high', 'medium', 'low'] ) ) {
            $severity = 'medium';
            $raw_severity = 'medium';
        }

        $preferred_remediation = 'update';
        if ( $type === 'theme' && $is_theme_active === false ) {
            $severity = $this->downgrade_inactive_theme_severity( $severity );
            $preferred_remediation = 'delete';
        }

        $normalized_asset_name = is_string( $asset_name ) && trim( $asset_name ) !== '' ? trim( $asset_name ) : $slug;
        if ( $type === 'core' ) {
            $normalized_asset_name = 'WordPress Core';
        }

        return [
            'id'          => $issue['id'] ?? uniqid('vuln_'), 
            'hash'        => IssueRepository::generateHash( 'vulnerability', ($issue['id'] ?? '') . '_' . $slug ),
            'type'        => IssueType::VULNERABILITY,
            'status'      => 'vulnerable',
            'severity'    => $severity,
            'file'        => $slug, 
            'message'     => $issue['title'] ?? 'Unknown Vulnerability',
            'description' => $issue['impact'] ?? '',
            'created_at'  => time(),
            'slug'        => $slug,
            'version'     => $current_version, // Actual installed version
            'data'        => [
                'component_type' => $type,
                'slug'           => $slug, // PERSISTENCE: Save slug in JSON since table has no slug column
                'plugin_file'    => $plugin_file, // Full file path for plugin.update command (e.g. "woocommerce/woocommerce.php")
                'component_name' => $normalized_asset_name,
                'asset_name'     => $normalized_asset_name,
                'fixed_in'       => $issue['fixed_in'] ?? null,
                'cve'            => $issue['cve'] ?? '',
                'vnv'            => $issue['vnv'] ?? '',
                'cvss'           => $issue['cvss'] ?? 0,
                'references'     => $issue['references'] ?? [],
                'can_update'     => $can_update,
                'available_update_version' => $available_update_version,
                'has_update_available' => $available_update_version !== '',
                'is_active'      => $is_theme_active,
                'original_severity' => $raw_severity,
                'remediation_preferred' => $preferred_remediation,
                'version'        => $current_version,
                'title'          => $issue['title'] ?? 'Unknown Vulnerability',
                'description'    => $issue['impact'] ?? '',
            ],
            'meta'        => [
                'label'         => ucfirst($type) . ': ' . $slug,
                'layout'        => 'vulnerability_card',
                'asset_slug'    => $slug,
                'asset_name'    => $normalized_asset_name,
                'asset_type'    => $type,
                'asset_version' => $current_version,
                'is_active'     => $is_theme_active,
            ]
        ];
    }

    private function downgrade_inactive_theme_severity( string $severity ): string {
        if ( $severity === 'critical' ) {
            return 'high';
        }
        if ( $severity === 'high' ) {
            return 'medium';
        }
        if ( $severity === 'medium' ) {
            return 'low';
        }
        return 'low';
    }

    /**
     * Resolve plugin update map keyed by plugin base file.
     * Refreshes stale/missing transients to avoid false "manual fix" results.
     *
     * @return array<string,mixed>
     */
    private function get_plugin_update_map(): array {
        if ( ! function_exists( 'wp_update_plugins' ) ) {
            require_once ABSPATH . 'wp-includes/update.php';
        }

        $update_plugins = get_site_transient( 'update_plugins' );
        $is_stale = true;
        if ( is_object( $update_plugins ) && isset( $update_plugins->last_checked ) && is_numeric( $update_plugins->last_checked ) ) {
            $is_stale = ( time() - (int) $update_plugins->last_checked ) > ( 6 * HOUR_IN_SECONDS );
        }

        // Refresh when transient is missing, malformed, or stale.
        if (
            ! is_object( $update_plugins ) ||
            ! isset( $update_plugins->response ) ||
            $is_stale
        ) {
            wp_update_plugins();
            $update_plugins = get_site_transient( 'update_plugins' );
        }

        if ( ! is_object( $update_plugins ) || ! isset( $update_plugins->response ) ) {
            return [];
        }

        if ( is_array( $update_plugins->response ) ) {
            return $update_plugins->response;
        }

        if ( is_object( $update_plugins->response ) ) {
            return (array) $update_plugins->response;
        }

        return [];
    }

    /**
     * Resolve theme update map keyed by theme stylesheet slug.
     *
     * @return array<string,mixed>
     */
    private function get_theme_update_map(): array {
        if ( ! function_exists( 'wp_update_themes' ) ) {
            require_once ABSPATH . 'wp-includes/update.php';
        }

        $update_themes = get_site_transient( 'update_themes' );
        $is_stale = true;
        if ( is_object( $update_themes ) && isset( $update_themes->last_checked ) && is_numeric( $update_themes->last_checked ) ) {
            $is_stale = ( time() - (int) $update_themes->last_checked ) > ( 6 * HOUR_IN_SECONDS );
        }

        if (
            ! is_object( $update_themes ) ||
            ! isset( $update_themes->response ) ||
            $is_stale
        ) {
            wp_update_themes();
            $update_themes = get_site_transient( 'update_themes' );
        }

        if ( ! is_object( $update_themes ) || ! isset( $update_themes->response ) ) {
            return [];
        }

        if ( is_array( $update_themes->response ) ) {
            return $update_themes->response;
        }

        if ( is_object( $update_themes->response ) ) {
            return (array) $update_themes->response;
        }

        return [];
    }

    /**
     * Extract the target version from a WordPress update payload.
     *
     * @param mixed $update_entry
     * @return string
     */
    private function resolve_available_update_version( $update_entry ): string {
        if ( is_object( $update_entry ) && isset( $update_entry->new_version ) ) {
            return sanitize_text_field( (string) $update_entry->new_version );
        }

        if ( is_array( $update_entry ) && isset( $update_entry['new_version'] ) ) {
            return sanitize_text_field( (string) $update_entry['new_version'] );
        }

        return '';
    }

    /**
     * Determine whether the available WordPress update falls outside the affected range.
     *
     * @param mixed $available_update_version
     * @param array $affected_ranges
     * @return bool
     */
    private function is_safe_update_candidate( $available_update_version, array $affected_ranges ): bool {
        $available = sanitize_text_field( (string) $available_update_version );

        if ( $available === '' || empty( $affected_ranges ) ) {
            return false;
        }

        return ! $this->is_version_affected( $available, $affected_ranges );
    }
}
