<?php

declare(strict_types=1);

namespace SiteFort\Modules\Cloud\Services;

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

use SiteFort\Modules\Cloud\SecureApi;
use SiteFort\Modules\Cloud\Service\CloudSettings;

class VulnerabilitySync {

    public function run(): bool {
        $settings = CloudSettings::get_all();
        if ( empty( $settings['site_token'] ) || empty( $settings['site_secret'] ) ) {
            return false;
        }

        if ( ! function_exists( 'get_plugins' ) ) {
            require_once ABSPATH . 'wp-admin/includes/plugin.php';
        }

        $plugins = $this->collect_plugins();
        $themes  = $this->collect_themes();

        $custom_plugins = $this->detect_custom_plugins( $plugins );

        $payload = [
            'plugins'   => $this->prepare_payload_map( $plugins ),
            'themes'    => $this->prepare_payload_map( $themes ),
            'wordpress' => get_bloginfo( 'version' ),
            'locale'    => get_locale(),
        ];

        if ( ! empty( $custom_plugins ) ) {
            $payload['custom_plugins'] = $custom_plugins;
        }

        $api = new SecureApi(
            $settings['license_key'] ?? '',
            $settings['site_token'],
            $settings['site_secret']
        );

        $response = $api->post( 'services/intelligence/vulnerabilities', $payload, SecureApi::SCANNER_WORKER_TIMEOUT_SECONDS, SecureApi::SCANNER_WORKER_MAX_ATTEMPTS );

        if ( is_wp_error( $response ) ) {
            $this->set_last_sync_error( (string) $response->get_error_message() );
            return false;
        }

        if ( ! $this->is_valid_response( $response ) ) {
            $this->set_last_sync_error( 'Vulnerability intelligence response format invalid.' );
            return false;
        }

        \SiteFort\Includes\SettingsStore::set_field( \SiteFort\Includes\SettingsStore::COL_STATE, 'vulnerability_issues', $response );
        \SiteFort\Includes\SettingsStore::set_field( \SiteFort\Includes\SettingsStore::COL_STATE, 'vulnerability_last_sync_error', '' );
        CloudSettings::update_stored_fields( [ 'last_sync_vuln' => time() ] );

        return true;
    }

    /**
     * Detect custom plugins not known to the WordPress.org update system.
     *
     * @param array<string, string> $plugins
     * @return array<int, string>
     */
    private function detect_custom_plugins( array $plugins ): array {
        $update_items = $this->get_plugin_update_items();
        if ( empty( $update_items ) ) {
            return [];
        }

        $repo_slugs = [];
        foreach ( $update_items as $item ) {
            if ( ! is_object( $item ) || empty( $item->slug ) ) {
                continue;
            }
            $repo_slug = $this->normalize_alias( (string) $item->slug );
            if ( $repo_slug !== '' ) {
                $repo_slugs[ $repo_slug ] = true;
            }
        }

        if ( empty( $repo_slugs ) ) {
            return [];
        }

        $custom = [];
        foreach ( array_keys( $plugins ) as $slug ) {
            $normalized_slug = $this->normalize_alias( (string) $slug );
            if ( $normalized_slug === '' || ! isset( $repo_slugs[ $normalized_slug ] ) ) {
                $custom[] = $slug;
            }
        }

        return $custom;
    }

    /**
     * Build a comprehensive plugin inventory for Cloud enrichment.
     *
     * @return array<string, string>
     */
    private function collect_plugins(): array {
        $plugins = [];

        foreach ( get_plugins() as $file => $data ) {
            $slug = $this->derive_plugin_slug( $file );
            if ( $slug === '' ) {
                continue;
            }
            $version = $this->normalize_version( $data['Version'] ?? '' );
            // Prefer the canonical main plugin file ({slug}/{slug}.php).
            // This avoids sub-files with their own Version headers overwriting the real version.
            $is_main = ( basename( $file, '.php' ) === $slug );
            if ( $is_main || ! isset( $plugins[ $slug ] ) ) {
                $plugins[ $slug ] = $version;
            }
        }

        $plugins = $this->merge_active_plugin_files( $plugins );
        $plugins = $this->merge_directory_plugins( $plugins );

        return $plugins;
    }

    /**
     * Fill gaps for active plugins that are not returned by get_plugins().
     *
     * @param array<string, string> $plugins
     * @return array<string, string>
     */
    private function merge_active_plugin_files( array $plugins ): array {
        $active = get_option( 'active_plugins', [] );
        if ( ! is_array( $active ) ) {
            return $plugins;
        }

        foreach ( $active as $file ) {
            $slug = $this->derive_plugin_slug( (string) $file );
            if ( $slug === '' || isset( $plugins[ $slug ] ) ) {
                continue;
            }
            $path = trailingslashit( WP_PLUGIN_DIR ) . ltrim( $file, '/' );
            if ( ! file_exists( $path ) ) {
                continue;
            }
            $data = get_plugin_data( $path, false, false );
            $plugins[ $slug ] = $this->normalize_version( $data['Version'] ?? '' );
        }

        return $plugins;
    }

    /**
     * Build the installed theme version map.
     *
     * @return array<string, string>
     */
    private function collect_themes(): array {
        $themes = [];

        foreach ( wp_get_themes() as $slug => $theme ) {
            $themes[ $slug ] = $this->normalize_version( (string) $theme->get( 'Version' ) );
        }

        return $themes;
    }

    /**
     * Ensure custom plugin folders are represented even when headers are non-standard.
     *
     * @param array<string, string> $plugins
     * @return array<string, string>
     */
    private function merge_directory_plugins( array $plugins ): array {
        if ( ! defined( 'WP_PLUGIN_DIR' ) || ! is_dir( WP_PLUGIN_DIR ) ) {
            return $plugins;
        }

        $entries = new \DirectoryIterator( WP_PLUGIN_DIR );
        foreach ( $entries as $entry ) {
            if ( $entry->isDot() || ! $entry->isDir() ) {
                continue;
            }
            $slug = $entry->getFilename();
            if ( isset( $plugins[ $slug ] ) ) {
                continue;
            }
            $plugin_file = $this->find_plugin_header_file( $entry->getPathname() );
            if ( ! $plugin_file && ! $this->directory_has_php( $entry->getPathname() ) ) {
                continue;
            }
            $version = 'unknown';
            if ( $plugin_file ) {
                $data = get_plugin_data( $plugin_file, false, false );
                $version = $this->normalize_version( $data['Version'] ?? '' );
            }
            $plugins[ $slug ] = $version;
        }

        return $plugins;
    }

    /**
     * Locate the first plugin header file within a directory (root or one level deep).
     *
     * @param string $dir
     * @return string|null
     */
    private function find_plugin_header_file( string $dir ): ?string {
        $candidate = $this->scan_for_header( $dir );
        if ( $candidate ) {
            return $candidate;
        }

        foreach ( glob( trailingslashit( $dir ) . '*/', GLOB_ONLYDIR ) as $subdir ) {
            $candidate = $this->scan_for_header( $subdir );
            if ( $candidate ) {
                return $candidate;
            }
        }

        return null;
    }

    /**
     * Scan a directory for a plugin header file.
     *
     * @param string $dir
     * @return string|null
     */
    private function scan_for_header( string $dir ): ?string {
        foreach ( glob( trailingslashit( $dir ) . '*.php' ) as $file ) {
            $data = get_plugin_data( $file, false, false );
            if ( ! empty( $data['Name'] ) ) {
                return $file;
            }
        }
        return null;
    }

    /**
     * Check whether a directory contains PHP files.
     *
     * @param string $dir
     * @return bool
     */
    private function directory_has_php( string $dir ): bool {
        $files = glob( trailingslashit( $dir ) . '*.php' );
        return ! empty( $files );
    }

    /**
     * Derive the primary installed plugin slug from a plugin file path.
     *
     * @param string $file
     * @return string
     */
    private function derive_plugin_slug( string $file ): string {
        $slug = dirname( $file );
        if ( $slug === '.' ) {
            $slug = basename( $file, '.php' );
        }

        return trim( $slug );
    }

    /**
     * Load update transient plugin items keyed by plugin file.
     *
     * @return array<string, object>
     */
    private function get_plugin_update_items(): array {
        $update_plugins = get_site_transient( 'update_plugins' );
        if ( ! is_object( $update_plugins ) ) {
            return [];
        }

        $items = [];
        foreach ( [ 'response', 'no_update' ] as $bucket ) {
            foreach ( $this->normalize_update_bucket( $update_plugins->{$bucket} ?? null ) as $file => $item ) {
                if ( is_object( $item ) ) {
                    $items[ (string) $file ] = $item;
                }
            }
        }

        return $items;
    }

    /**
     * Normalize object-or-array update buckets into a plain array.
     *
     * @param mixed $bucket
     * @return array<string, mixed>
     */
    private function normalize_update_bucket( $bucket ): array {
        if ( is_array( $bucket ) ) {
            return $bucket;
        }

        if ( is_object( $bucket ) ) {
            return get_object_vars( $bucket );
        }

        return [];
    }

    /**
     * Normalize alias candidates into a canonical slug-like token.
     *
     * @param string $candidate
     * @return string
     */
    private function normalize_alias( string $candidate ): string {
        $candidate = trim( $candidate );
        if ( $candidate === '' ) {
            return '';
        }

        return sanitize_key( $candidate );
    }

    /**
     * Preserve dictionary semantics for empty inventories when encoding JSON.
     *
     * @param array<string, mixed> $map
     * @return array<string, mixed>|\stdClass
     */
    private function prepare_payload_map( array $map ) {
        return empty( $map ) ? new \stdClass() : $map;
    }

    /**
     * Normalize version values to avoid empty payload entries.
     *
     * @param string $version
     * @return string
     */
    private function normalize_version( string $version ): string {
        $version = trim( $version );
        return $version === '' ? 'unknown' : $version;
    }

    /**
     * Validate minimal vulnerability payload shape.
     *
     * @param mixed $response
     * @return bool
     */
    private function is_valid_response( $response ): bool {
        if ( ! is_array( $response ) ) {
            return false;
        }

        foreach ( [ 'plugins', 'themes', 'wordpress' ] as $key ) {
            if ( array_key_exists( $key, $response ) && ! is_array( $response[ $key ] ) ) {
                return false;
            }
        }

        return true;
    }

    /**
     * Persist last sync error and write throttled scan log entry.
     *
     * @param string $message
     * @return void
     */
    private function set_last_sync_error( string $message ): void {
        \SiteFort\Includes\SettingsStore::set_field(
            \SiteFort\Includes\SettingsStore::COL_STATE,
            'vulnerability_last_sync_error',
            $message
        );

        if ( class_exists( '\\SiteFort\\Modules\\Scanner\\Service\\ScanLogger' ) ) {
            \SiteFort\Modules\Scanner\Service\ScanLogger::log( 'Vulnerability sync failed: ' . $message );
        }
    }
}
