<?php
/**
 * 🔌 PLUGINS CONTROLLER - V2.2 PURE
 *
 * Manages WordPress plugin operations:
 * - List all plugins with metadata
 * - Activate/Deactivate plugins
 * - Delete plugins
 * - Update plugins
 * - Plugin statistics
 *
 * @package FleekDash\Controllers
 * @since 2.2.0
 */

namespace FleekDash\Controllers;

use WP_REST_Request;
use WP_REST_Response;
use WP_Error;
use FleekDash\Services\ResponseService;
use FleekDash\API\Plugins\PluginAssetManager;
use FleekDash\Controllers\Plugins\PluginUpdateExecutor;
use FleekDash\Controllers\Plugins\ThemeUpdateExecutor;
use FleekDash\Controllers\Plugins\PluginChangelogService;
use FleekDash\Controllers\Plugins\PluginActionLinksResolver;
use FleekDash\Controllers\Plugins\PluginSignatureService;
use FleekDash\Admin\Menu\MenuCaptureService;
use FleekDash\Admin\Menu\MenuSnapshotRebuildService;
use FleekDash\API\MenuCacheLifecycleService;

/**
 * Class PluginsController
 *
 * Pure V2.2 Controller - No legacy dependencies
 *
 * @package FleekDash\Controllers
 */
class PluginsController
{
    private const CLASS_PLUGIN_HELPER = '\FleekDash\Core\PluginHelper';
    private const PATH_PLUGIN_PHP = 'wp-admin/includes/plugin.php';
    private const PATH_FILE_PHP = 'wp-admin/includes/file.php';
    private const MSG_FILE_REQUIRED = 'Plugin file parameter is required';
    private MenuSnapshotRebuildService $menuSnapshotRebuildService;

    /** @var PluginAssetManager */
    private $assetManager;

    /** @var PluginUpdateExecutor */
    private $updateExecutor;

    /** @var ThemeUpdateExecutor */
    private $themeUpdateExecutor;

    /** @var PluginChangelogService */
    private $changelogService;

    /** @var PluginActionLinksResolver */
    private $actionLinksResolver;

    private PluginSignatureService $signatureService;

    /**
     * Constructor
     */
    public function __construct()
    {
        $this->assetManager = new PluginAssetManager();
        $this->updateExecutor = new PluginUpdateExecutor();
        $this->themeUpdateExecutor = new ThemeUpdateExecutor();
        $this->changelogService = new PluginChangelogService();
        $this->actionLinksResolver = new PluginActionLinksResolver();
        $this->menuSnapshotRebuildService = new MenuSnapshotRebuildService();
        $this->signatureService = new PluginSignatureService();

        if (!function_exists('is_plugin_active') && is_admin()) {
            $this->ensurePluginFunctionsLoaded();
        }
    }

    /**
     * Ensures wp-admin/includes/plugin.php is loaded (\get_plugins, \is_plugin_active, etc.).
     *
     * REST API requests are not {@see \is_admin()}; use {@see PluginHelper::ensurePluginApiLoaded()}.
     */
    private function ensurePluginFunctionsLoaded(): void
    {
        if (\class_exists(self::CLASS_PLUGIN_HELPER)) {
            \call_user_func([self::CLASS_PLUGIN_HELPER, 'ensurePluginApiLoaded']);
            return;
        }
        if (!\function_exists('get_plugins')) {
            require_once ABSPATH . self::PATH_PLUGIN_PHP;
        }
    }

    /** Ensures file.php functions (e.g. list_files) are loaded. */
    private function ensureFileFunctionsLoaded(): void
    {
        if (!function_exists('list_files') || !function_exists('request_filesystem_credentials')) {
            require_once ABSPATH . self::PATH_FILE_PHP;
        }
    }

    /**
     * Mark menu snapshots stale after plugin lifecycle changes, then best-effort rebuild for the current user.
     *
     * Iframe flows run in full admin: {@see \FleekDash\Admin\MenuChangeNotifier::attemptRecaptureSnapshotForCurrentUser()}
     * persists a fresh tree (skipped when REST_REQUEST). SPA mutations only hit this path — we mirror iframe intent by
     * calling {@see MenuSnapshotRebuildService::rebuildForCurrentUser()}, which persists only when the built menu passes
     * the quality gate (avoids overwriting with sparse REST trees). If the gate fails, merge logic in MenuDataAPI still
     * reconciles stale snapshots with REST globals.
     */
    private function syncMenuSnapshotAfterPluginLifecycleChange(): void
    {
        try {
            $lifecycle = new MenuCacheLifecycleService(new MenuCaptureService());
            $lifecycle->invalidateSnapshots('plugin_lifecycle_rest');
            $this->menuSnapshotRebuildService->rebuildForCurrentUser();
        } catch (\Throwable $e) {
            if (\defined('WP_DEBUG') && \WP_DEBUG) {
                \error_log('[FleekDash PluginsController] Menu snapshot sync failed: ' . $e->getMessage());
            }
        }
    }

    private function ensureWordPressPluginUpdateDataLoaded(): void
    {
        if (!\function_exists('wp_update_plugins')) {
            require_once ABSPATH . 'wp-admin/includes/update.php';
        }
        $stored = \get_site_transient('update_plugins');
        // Bootstrap only when no transient exists. Avoid wp_update_plugins() on every list load
        // when `response` is missing — that blocks on api.wordpress.org and can exceed max_execution_time.
        if (!\is_object($stored)) {
            \wp_update_plugins();
        }
    }

    /**
     * 🎯 Get all plugins
     *
     * @param WP_REST_Request $_request
     * @return WP_REST_Response
     */
    public function get_plugins(WP_REST_Request $_request) // NOSONAR php:S100,S1172 - WP REST callback signature
    {
        try {
            $this->ensurePluginFunctionsLoaded();
            $this->ensureFileFunctionsLoaded();
            $this->ensureWordPressPluginUpdateDataLoaded();

            // MEMORY OPTIMIZATION: Cache \get_plugins() result to avoid loading all plugins multiple times
            // This can be memory-intensive on sites with many plugins
            static $cached_plugins = null;
            if ($cached_plugins === null) {
                $cached_plugins = \get_plugins() ?: [];
            }
            $all_plugins = $cached_plugins;
            $update_plugins = \get_site_transient('update_plugins');
            $update_response = (is_object($update_plugins) && isset($update_plugins->response) && is_array($update_plugins->response))
                ? $update_plugins->response
                : [];
            $plugins = [];

            foreach ($all_plugins as $plugin_file => $plugin_data) {
                // Normalize plugin data to array defensively
                if (!is_array($plugin_data)) {
                    $plugin_data = (array) $plugin_data;
                }

                // Check if plugin has an available update
                $has_update = false;
                if (isset($update_response[$plugin_file])) {
                    $has_update = true;
                }

                // Safely resolve derived fields (author url, thumbnail, banner)
                $author_uri = isset($plugin_data['AuthorURI']) ? $plugin_data['AuthorURI'] : '';
                try {
                    $author_uri = $this->assetManager->getAuthorProfileUrl($plugin_data);
                } catch (\Throwable $e) {
                    // Keep fallback author_uri
                }

                $thumbnail = ['type' => 'icon', 'url' => null, 'icon' => 'mdi:puzzle', 'color' => '#6c757d'];
                try {
                    $thumbnail = $this->assetManager->getPluginThumbnail($plugin_file, $plugin_data);
                } catch (\Throwable $e) {
                    // Keep fallback thumbnail
                }

                $banner = ['type' => 'none', 'url' => null];
                try {
                    $banner = $this->assetManager->getPluginBanner($plugin_file, $plugin_data);
                } catch (\Throwable $e) {
                    // Keep fallback banner
                }

                // Get plugin action links (Settings, etc.)
                $action_links = [];
                try {
                    $action_links = $this->actionLinksResolver->getActionLinks($plugin_file, $plugin_data);
                } catch (\Throwable $e) {
                    // Keep empty action_links on error
                }

                $plugins[] = [
                    'file' => $plugin_file,
                    'name' => $plugin_data['Name'],
                    'version' => $plugin_data['Version'],
                    'description' => $plugin_data['Description'],
                    'author' => $plugin_data['Author'],
                    'author_uri' => $author_uri,
                    'plugin_uri' => $plugin_data['PluginURI'] ?? '',
                    // Use core helper so network-activated (multisite) plugins match WP admin and stats.
                    'is_active' => \is_plugin_active($plugin_file),
                    'update_available' => $has_update,
                    // Main plugin file mtime (UTC epoch seconds). Best available proxy for last install/update on disk.
                    'updated_at' => $this->resolvePluginMainFileUnixTime($plugin_file),
                    'thumbnail' => $thumbnail,
                    'banner' => $banner,
                    'action_links' => $action_links,
                ];
            }

            // Set cache-busting headers
            $response = \rest_ensure_response($plugins);
            $response->header('Cache-Control', 'no-cache, no-store, must-revalidate, max-age=0');
            $response->header('Pragma', 'no-cache');
            $response->header('Expires', '0');
            $response->header('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT');

            return $response;

        } catch (\Throwable $e) {
            return ResponseService::error(
                'Failed to retrieve plugins',
                ['error' => $e->getMessage()],
                500
            );
        }
    }

    /**
* Activate plugin
     *
     * @param WP_REST_Request $request
     * @return WP_REST_Response
     */
    public function activate_plugin(WP_REST_Request $request) // NOSONAR php:S100 - WP REST callback
    {
        $response = null;
        try {
            $this->ensurePluginFunctionsLoaded();
            $file = $request->get_param('file');

            if (empty($file)) {
                $response = ResponseService::error(self::MSG_FILE_REQUIRED, null, 400);
            } else {

                $result = \activate_plugin($file);

                if ($result && is_wp_error($result)) {
                    $response = ResponseService::error(
                        'Activation failed',
                        ['error' => $result->get_error_message()],
                        500
                    );
                } else {
                    $this->invalidate_plugins_cache();
                    $this->syncMenuSnapshotAfterPluginLifecycleChange();
                    $response = ResponseService::success('Plugin activated successfully');
                }
            }
        } catch (\Exception $e) {
            $response = ResponseService::error(
                'Error activating plugin',
                ['error' => $e->getMessage()],
                500
            );
        }
        return $response;
    }

    /**
     * ❌ Deactivate plugin
     *
     * @param WP_REST_Request $request
     * @return WP_REST_Response
     */
    public function deactivate_plugin(WP_REST_Request $request) // NOSONAR php:S100 - WP REST callback
    {
        $response = null;
        try {
            $this->ensurePluginFunctionsLoaded();
            $file = $request->get_param('file');

            if (empty($file)) {
                $response = ResponseService::error(self::MSG_FILE_REQUIRED, null, 400);
            } else {
                \deactivate_plugins($file);
                $this->invalidate_plugins_cache();
                $this->syncMenuSnapshotAfterPluginLifecycleChange();
                $response = ($file === 'fleekdash/fleekdash.php')
                    ? ResponseService::success(
                        'FleekDash V2 has been deactivated. Redirecting to WordPress plugins page...',
                        ['redirect' => \admin_url('plugins.php?fleekdash=off')]
                    )
                    : ResponseService::success('Plugin deactivated successfully');
            }
        } catch (\Exception $e) {
            $response = ResponseService::error(
                'Error deactivating plugin',
                ['error' => $e->getMessage()],
                500
            );
        }
        return $response;
    }

    /**
     * 🗑️ Delete plugin
     *
     * @param WP_REST_Request $request
     * @return WP_REST_Response
     */
    public function delete_plugin(WP_REST_Request $request) // NOSONAR php:S100 - WP REST callback
    {
        $response = null;
        try {
            $this->ensurePluginFunctionsLoaded();
            $this->ensureFileFunctionsLoaded();

            $file = $request->get_param('file');
            if (empty($file)) {
                $json_params = $request->get_json_params();
                $file = $json_params['file'] ?? null;
            }
            if (empty($file)) {
                $response = ResponseService::error(self::MSG_FILE_REQUIRED, null, 400);
            } elseif ($file === 'fleekdash/fleekdash.php') {
                $response = ResponseService::error(
                    'FleekDash cannot delete itself. Please deactivate first, then delete from WordPress Plugins page.',
                    null,
                    400
                );
            } elseif (!\file_exists(WP_PLUGIN_DIR . '/' . $file)) {
                $response = ResponseService::error('Plugin file not found', ['file' => $file], 404);
            } else {
                $result = delete_plugins([$file]);
                if (is_wp_error($result)) {
                    $response = ResponseService::error(
                        'Failed to delete plugin',
                        ['error' => $result->get_error_message()],
                        500
                    );
                } else {
                    $this->invalidate_plugins_cache();
                    $this->syncMenuSnapshotAfterPluginLifecycleChange();
                    $response = ResponseService::success('Plugin deleted successfully');
                }
            }
        } catch (\Exception $e) {
            $response = ResponseService::error(
                'Error deleting plugin',
                ['error' => $e->getMessage()],
                500
            );
        }
        return $response;
    }

    /**
     * 🔄 Update plugin(s)
     *
     * @param WP_REST_Request $request
     * @return WP_REST_Response
     */
    public function update_plugin(WP_REST_Request $request) // NOSONAR php:S100 - WP REST callback
    {
        @set_time_limit(120);
        $plugin_file = null;
        $response = null;

        try {
            $plugin_file = $this->updateExecutor->resolvePluginFileParam($request);
            if (empty($plugin_file)) {
                $response = ResponseService::error(self::MSG_FILE_REQUIRED, null, 400);
            } else {
                $this->updateExecutor->loadWordPressDependencies();
                $result = $this->updateExecutor->runUpdate($plugin_file);

                $this->invalidate_plugins_cache();
                $this->syncMenuSnapshotAfterPluginLifecycleChange();
                if ($result['status'] === 'updated') {
                    $this->clearPluginUpdateFlag($plugin_file);
                }

                $response = $this->updateExecutor->buildSuccessResponse($result, $plugin_file);
            }
        } catch (\Throwable $e) {
            $response = $this->updateExecutor->buildErrorResponse($e, $plugin_file);
        }

        return $response;
    }

    /**
     * Update a single installed WordPress theme (stylesheet slug; same payload as core update form).
     *
     * @param WP_REST_Request $request
     * @return WP_REST_Response
     */
    public function update_wordpress_theme(WP_REST_Request $request) // NOSONAR php:S100 - WP REST callback
    {
        @set_time_limit(120);
        $stylesheet = null;
        $response = null;

        try {
            $stylesheet = $this->themeUpdateExecutor->resolveThemeStylesheetParam($request);
            if (empty($stylesheet)) {
                $response = ResponseService::error('Theme stylesheet is required', null, 400);
            } else {
                $this->themeUpdateExecutor->loadWordPressDependencies();
                $result = $this->themeUpdateExecutor->runUpdate($stylesheet);
                $response = $this->themeUpdateExecutor->buildSuccessResponse($result, $stylesheet);
            }
        } catch (\Throwable $e) {
            $response = $this->themeUpdateExecutor->buildErrorResponse($e, $stylesheet);
        }

        return $response;
    }

    /**
     * Get plugins statistics.
     *
     * @param WP_REST_Request $_request
     * @return WP_REST_Response
     */
    public function get_plugins_stats(WP_REST_Request $_request) // NOSONAR php:S100,S1172 - WP REST callback
    {
        try {
            $this->ensurePluginFunctionsLoaded();
            $this->ensureFileFunctionsLoaded();
            $this->ensureWordPressPluginUpdateDataLoaded();

            // MEMORY OPTIMIZATION: Reuse cached plugins from \get_plugins() method
            // This avoids loading all plugins multiple times in the same request
            static $cached_plugins = null;
            if ($cached_plugins === null) {
                $cached_plugins = \get_plugins() ?: [];
            }
            $all_plugins = $cached_plugins;
            $active_plugins = \get_option('active_plugins', []) ?: [];
            $network_active_plugins = [];

            // Check for network-active plugins if multisite
            if (is_multisite()) {
                $network_active_plugins = get_site_option('active_sitewide_plugins', []);
            }

            // Get update information
            $update_plugins = \get_site_transient('update_plugins');
            $plugins_with_updates = [];
            if (is_object($update_plugins) && isset($update_plugins->response) && is_array($update_plugins->response)) {
                $plugins_with_updates = array_keys($update_plugins->response);
            }

            // Calculate statistics
            $total = count($all_plugins);
            $active = 0;
            $inactive = 0;
            $update_available = 0;

            foreach ($all_plugins as $plugin_file => $plugin_data) {
                // Check if plugin is active
                $is_active = in_array($plugin_file, $active_plugins ?: []) ||
                    (is_multisite() && array_key_exists($plugin_file, $network_active_plugins));

                if ($is_active) {
                    $active++;
                } else {
                    $inactive++;
                }

                // Check if update is available
                if (in_array($plugin_file, $plugins_with_updates)) {
                    $update_available++;
                }
            }

            $stats = [
                'total' => $total,
                'active' => $active,
                'inactive' => $inactive,
                'update_available' => $update_available,
            ];

            return ResponseService::success('Plugin statistics retrieved', $stats);

        } catch (\Exception $e) {
            return ResponseService::error(
                'Failed to retrieve plugin statistics',
                ['error' => $e->getMessage()],
                500
            );
        }
    }

    /**
     * 🎭 Get plugin role patterns (from PluginDetectionAPI)
     *
     * @param WP_REST_Request $request
     * @return WP_REST_Response
     */
    public function get_plugin_role_patterns(WP_REST_Request $request) // NOSONAR php:S100 - WP REST callback
    {
        try {
            // Use existing PluginDetectionAPI for this specific functionality
            $detection_api = new \FleekDash\API\PluginDetectionAPI();
            return $detection_api->get_plugin_role_patterns($request);

        } catch (\Exception $e) {
            return ResponseService::error(
                'Failed to retrieve plugin role patterns',
                ['error' => $e->getMessage()],
                500
            );
        }
    }

    /**
* Get active plugins with roles (from PluginDetectionAPI)
     *
     * @param WP_REST_Request $request
     * @return WP_REST_Response
     */
    public function get_active_plugins(WP_REST_Request $request) // NOSONAR php:S100 - WP REST callback
    {
        try {
            // Use existing PluginDetectionAPI for this specific functionality
            $detection_api = new \FleekDash\API\PluginDetectionAPI();
            return $detection_api->get_active_plugins($request);

        } catch (\Exception $e) {
            return ResponseService::error(
                'Failed to retrieve active plugins',
                ['error' => $e->getMessage()],
                500
            );
        }
    }

    /**
     * 📋 Get plugin changelog
     *
     * Reads changelog from readme/changelog files via backend, then WordPress.org if needed (avoids 455 blocking errors).
     *
     * @param WP_REST_Request $request
     * @return WP_REST_Response
     */
    public function get_changelog(WP_REST_Request $request) // NOSONAR php:S100 - WP REST callback
    {
        $response = null;
        try {
            $plugin_file = $request->get_param('file');
            if (empty($plugin_file)) {
                $response = ResponseService::error(
                    self::MSG_FILE_REQUIRED,
                    ['error' => 'Missing required parameter: file'],
                    400
                );
            } else {
                $result = $this->changelogService->getChangelog($plugin_file);
                if (!$result['success']) {
                    $err = $result['error'] ?? '';
                    $status = in_array($err, ['Changelog file not found', 'Changelog section not found']) ? 404 : 400;
                    $response = ResponseService::error(
                        $err ?: 'Failed to retrieve changelog',
                        $result['error_data'] ?? [],
                        $status
                    );
                } else {
                    $response = ResponseService::success('Changelog retrieved successfully', [
                        'changelog' => $result['changelog'],
                        'plugin_file' => $result['plugin_file'],
                        'plugin_slug' => $result['plugin_slug'],
                        'source_file' => $result['source_file'],
                    ]);
                }
            }
        } catch (\Exception $e) {
            $response = ResponseService::error(
                'Failed to retrieve changelog',
                [
                    'error' => $e->getMessage(),
                    'plugin_file' => $request->get_param('file') ?? 'unknown',
                ],
                500
            );
        }
        return $response;
    }

    /**
     * 🔄 Invalidate plugins cache
     *
     * Clears caches that affect plugin list/state. Does NOT delete
     * update_plugins transient: that would make the next list response
     * return update_available=false for ALL plugins until WordPress
     * re-runs wp_update_plugins(), so badges would be wrong until browser refresh.
     * Individual plugin update state is cleared by clearPluginUpdateFlag()
     * after a successful update.
     *
     * Never call wp_cache_flush() here: it wipes the object cache (including the
     * in-memory copy of update_plugins on persistent backends). REST then reads
     * a cold/missing transient while wp_update_plugins() does not run automatically
     * like on a full wp-admin load — FleekDash grids lose all "Update" badges until F5.
     * Targeted deletes match FleekDash\API\Plugins\PluginHelpers::invalidatePluginsCache().
     *
     * @return void
     */
    private function invalidate_plugins_cache() // NOSONAR php:S100 - internal naming
    {
        \delete_transient('plugin_slugs');
        \wp_cache_delete('plugins', 'plugins');
        \do_action('fleekdash_plugins_cache_invalidated');
    }

    /**
     * Remove a specific plugin from the update_plugins site transient.
     * Called after a successful plugin update to immediately reflect the new status
     * without waiting for WordPress to rebuild the full transient.
     *
     * @param string $plugin_file Plugin basename (e.g. "akismet/akismet.php")
     * @return void
     */
    private function clearPluginUpdateFlag($plugin_file)
    {
        $update_plugins = \get_site_transient('update_plugins');
        if (!is_object($update_plugins) || !isset($update_plugins->response)) {
            return;
        }
        if (isset($update_plugins->response[$plugin_file])) {
            unset($update_plugins->response[$plugin_file]);
            \set_site_transient('update_plugins', $update_plugins);
        }
    }

    /**
     * Unix timestamp (seconds) of the main plugin file's last modification.
     * WordPress does not store "last updated" in plugin headers; mtime reflects last unpack/change on disk.
     *
     * @param string $plugin_file Relative path under wp-content/plugins (e.g. "akismet/akismet.php").
     * @return int|null
     */
    private function resolvePluginMainFileUnixTime($plugin_file)
    {
        $unix = null;
        if (\is_string($plugin_file) && $plugin_file !== '') {
            $path = \WP_PLUGIN_DIR . '/' . $plugin_file;
            if (\file_exists($path)) {
                $mtime = @\filemtime($path);
                if ($mtime !== false) {
                    $unix = (int) $mtime;
                }
            }
        }
        return $unix;
    }

    /**
     * Lightweight plugin file signature used to verify post-upload replacements.
     *
     * Accepts either:
     *  - A full plugin file relative path (e.g. "fleekdash/fleekdash.php")
     *  - A plugin folder slug (e.g. "fleekdash") -- in which case the controller
     *    resolves the main file from the installed plugin list.
     *
     * Returns mtime, size and version so the caller (plugin-upload.js) can
     * detect whether an upload actually replaced the files on disk.
     *
     * @param WP_REST_Request $request
     * @return WP_REST_Response
     */
    public function get_plugin_signature(WP_REST_Request $request) // NOSONAR php:S100 - WP REST callback
    {
        $response = null;
        try {
            $this->ensurePluginFunctionsLoaded();

            $raw = (string) $request->get_param('file');
            $resolvedFile = $this->signatureService->resolveTargetPath($raw);

            if ($resolvedFile === '') {
                $response = ResponseService::error('Invalid plugin file parameter', null, 400);
            } else {
                $response = \rest_ensure_response([
                    'success' => true,
                    'data' => $this->signatureService->buildPayload($resolvedFile),
                ]);
                $response->header('Cache-Control', 'no-cache, no-store, must-revalidate, max-age=0');
                $response->header('Pragma', 'no-cache');
            }
        } catch (\Throwable $e) {
            $response = ResponseService::error(
                'Failed to read plugin signature',
                ['error' => $e->getMessage()],
                500
            );
        }

        return $response;
    }
}
