<?php
/**
 * Plugin Update Executor - Runs WordPress plugin upgrade logic.
 *
 * @package FleekDash\Controllers\Plugins
 * @since 2.2.0
 */

namespace FleekDash\Controllers\Plugins;

if (!\function_exists('fleekdash_log_debug')) {
    require_once FLEEKDASH_PLUGIN_DIR . 'includes/Core/debug-helpers.php';
}

/**
 * Handles plugin update execution: validation, license check, upgrade, result building.
 */
class PluginUpdateExecutor
{
    /** @var WordPressUpdateLoader */
    private $wpLoader;

    public function __construct(?WordPressUpdateLoader $wpLoader = null)
    {
        $this->wpLoader = $wpLoader ?? new WordPressUpdateLoader();
    }
    /**
     * Resolve plugin file from JSON body: plugins[] (WP update form) or file (legacy / SPA).
     * Mirrors ThemeUpdateExecutor::resolveThemeStylesheetParam — one plugin per REST request.
     */
    public function resolvePluginFileParam(\WP_REST_Request $request): ?string
    {
        $params = $request->get_json_params();
        if (!\is_array($params)) {
            $params = [];
        }

        $plugin_file = $request->get_param('file');
        if (!empty($plugin_file) && \is_string($plugin_file)) {
            return \sanitize_text_field($plugin_file);
        }

        $plugins = $params['plugins'] ?? $request->get_param('plugins');
        if (\is_array($plugins) && isset($plugins[0]) && \is_string($plugins[0]) && $plugins[0] !== '') {
            return \sanitize_text_field($plugins[0]);
        }

        return null;
    }

    /**
     * Load WordPress files required for plugin updates.
     */
    public function loadWordPressDependencies(): void
    {
        $this->wpLoader->load();
    }

    /**
     * Run update for a single plugin.
     *
     * @return array{status: string, name?: string, message?: string, old_version?: string, new_version?: string}
     */
    public function runUpdate(string $plugin_file): array
    {
        $plugin_file = \sanitize_text_field($plugin_file);
        $full_path = WP_PLUGIN_DIR . '/' . $plugin_file;

        $result = $this->validateForUpdate($plugin_file, $full_path);
        if ($result !== null) {
            return $result;
        }

        $plugin_data = \get_plugin_data($full_path, false, false);
        $plugin_name = (is_array($plugin_data) && isset($plugin_data['Name'])) ? $plugin_data['Name'] : $plugin_file;
        $old_version = (is_array($plugin_data) && isset($plugin_data['Version'])) ? $plugin_data['Version'] : 'unknown';
        $current = $this->resolveUpdatePluginsTransient($plugin_file);

        $result = $this->checkFleekDashLicense($plugin_file, $plugin_name, $old_version, $current);
        if ($result !== null) {
            return $result;
        }

        return $this->executeUpgrade($plugin_file, $plugin_name, $old_version, $full_path, $current);
    }

    /**
     * Build REST response from plugin update result.
     */
    public function buildSuccessResponse(array $result, string $plugin_file)
    {
        $name = $result['name'] ?? $plugin_file;
        $status = $result['status'];
        $response = null;

        if ($status === 'updated') {
            $response = \rest_ensure_response([
                'success' => true,
                'message' => sprintf('Plugin "%s" updated successfully', $name),
                'updated_plugins' => [['name' => $name, 'old_version' => $result['old_version'] ?? '', 'new_version' => $result['new_version'] ?? '']],
            ]);
        } elseif ($status === 'already_up_to_date') {
            $response = \rest_ensure_response([
                'success' => true,
                'already_up_to_date' => true,
                'message' => $result['message'] ?? sprintf('Plugin "%s" is already up to date', $name),
                'plugin_name' => $name,
            ]);
        } elseif ($status === 'license_required') {
            $response = \rest_ensure_response([
                'success' => false,
                'license_required' => true,
                'message' => sprintf('Plugin "%s" requires a valid license to update', $name),
                'error' => $result['message'] ?? '',
                'plugin_name' => $name,
            ]);
            $response->set_status(403);
        } else {
            $response = \rest_ensure_response([
                'success' => false,
                'message' => $result['message'] ?? 'Plugin update failed',
                'plugin_name' => $name,
            ]);
            // not_found → 404; failed (and any other non-success status in this branch) → 5xx so clients can use HTTP status.
            $response->set_status($status === 'not_found' ? 404 : 500);
        }

        return $response;
    }

    /**
     * Build error response for plugin update exception.
     */
    public function buildErrorResponse(\Throwable $e, ?string $plugin_file)
    {
        $error_message = $e->getMessage();
        $plugin_name = $plugin_file ?? 'Unknown plugin';

        if (!empty($plugin_file)) {
            try {
                $plugin_data = \get_plugin_data(WP_PLUGIN_DIR . '/' . $plugin_file, false, false);
                if (is_array($plugin_data) && isset($plugin_data['Name'])) {
                    $plugin_name = $plugin_data['Name'];
                }
            } catch (\Throwable $plugin_error) {
                // Ignore
            }
        }

        $is_file_error = (
            strpos($error_message, 'Failed opening required') !== false ||
            strpos($error_message, 'No such file') !== false ||
            strpos($error_message, 'failed to open stream') !== false ||
            strpos($error_message, 'include') !== false ||
            strpos($error_message, 'require') !== false
        );

        $user_message = $is_file_error
            ? sprintf(
                'Failed to update %s: The plugin appears to have corrupted or missing files. Please try deactivating and reactivating the plugin, or update it manually from the WordPress Plugins page.',
                $plugin_name
            )
            : sprintf('Failed to update %s: %s', $plugin_name, $error_message);

        fleekdash_log_error(
            sprintf(
                'Plugin update error for %s: %s in %s:%d',
                $plugin_file ?? 'unknown',
                $error_message,
                $e->getFile(),
                $e->getLine()
            )
        );

        $resp = \rest_ensure_response([
            'success' => false,
            'message' => $user_message,
            'error' => $error_message,
            'error_file' => $e->getFile(),
            'error_line' => $e->getLine(),
            'plugin_file' => $plugin_file ?? 'unknown',
            'plugin_name' => $plugin_name,
        ]);
        $resp->set_status(500);
        return $resp;
    }

    /**
     * Load update_plugins metadata for a plugin. Refreshes from WordPress.org when the
     * transient was cleared by a prior upgrade in the same bulk session.
     *
     * @return object|false
     */
    private function resolveUpdatePluginsTransient(string $plugin_file)
    {
        $current = \get_site_transient('update_plugins');
        $has_entry = \is_object($current)
            && isset($current->response)
            && \is_array($current->response)
            && isset($current->response[$plugin_file]);

        if (!$has_entry && \function_exists('wp_update_plugins')) {
            \wp_update_plugins();
            $current = \get_site_transient('update_plugins');
        }

        return $current;
    }

    private function validateForUpdate(string $plugin_file, string $full_path): ?array
    {
        if (!\file_exists($full_path)) {
            return [
                'status' => 'not_found',
                'name' => $plugin_file,
                'message' => 'Plugin not found or not installed',
            ];
        }

        $plugin_data = \get_plugin_data($full_path, false, false);
        $plugin_name = (is_array($plugin_data) && isset($plugin_data['Name'])) ? $plugin_data['Name'] : $plugin_file;
        $current = $this->resolveUpdatePluginsTransient($plugin_file);
        $has_update = \is_object($current)
            && isset($current->response)
            && \is_array($current->response)
            && isset($current->response[$plugin_file]);

        if (!$has_update && (empty($plugin_data) || !isset($plugin_data['Version']))) {
            return [
                'status' => 'not_found',
                'name' => $plugin_name,
                'message' => 'Plugin not found or not installed',
            ];
        }

        return null;
    }

    private function checkFleekDashLicense(string $plugin_file, string $plugin_name, string $old_version, object|false $current): ?array
    {
        if ($plugin_file !== 'fleekdash/fleekdash.php') {
            return null;
        }

        $license_status = \get_option('fleekdash_v2_license_status', 'inactive');
        if ($license_status === 'active') {
            return null;
        }

        $new_version = (is_object($current) && isset($current->response[$plugin_file]))
            ? $current->response[$plugin_file]->new_version
            : '';
        return [
            'status' => 'license_required',
            'name' => $plugin_name,
            'message' => 'Please activate your FleekDash license before updating. Go to FleekDash → Settings → License to enter your license key.',
            'current_version' => $old_version,
            'new_version' => $new_version,
        ];
    }

    private function executeUpgrade(string $plugin_file, string $plugin_name, string $old_version, string $full_path, object|false $current): array
    {
        $skin = new PluginUpgraderSkin();
        $upgrader = new \Plugin_Upgrader($skin);
        $was_active_before = \is_plugin_active($plugin_file);

        try {
            // Do not delete the full update_plugins transient — bulk REST updates rely on
            // other plugins staying in the transient until FleekDash clears them individually.
            $upgrade_result = $upgrader->upgrade($plugin_file, ['clear_update_cache' => false]);
        } catch (\Throwable $e) {
            fleekdash_log_error('FleekDash: Plugin update threw for ' . $plugin_file . ': ' . $e->getMessage());
            return [
                'status' => 'failed',
                'name' => $plugin_name,
                'message' => $e->getMessage(),
            ];
        }

        $early_result = $this->handleUpgradeResult($upgrade_result, $skin, $plugin_file, $plugin_name, $old_version, $current);
        if ($early_result !== null) {
            return $early_result;
        }

        if ($was_active_before && !\is_plugin_active($plugin_file)) {
            $act = \activate_plugin($plugin_file);
            if (\is_wp_error($act)) {
                fleekdash_log_warn('FleekDash: Reactivation failed for ' . $plugin_file . ': ' . $act->get_error_message());
            }
        }

        $new_plugin_data = \get_plugin_data($full_path, false, false);
        $new_version = (is_array($new_plugin_data) && isset($new_plugin_data['Version'])) ? $new_plugin_data['Version'] : $old_version;

        return [
            'status' => 'updated',
            'name' => $plugin_name,
            'old_version' => $old_version,
            'new_version' => $new_version,
        ];
    }

    private function handleUpgradeResult(bool|\WP_Error|null $upgrade_result, PluginUpgraderSkin $skin, string $plugin_file, string $plugin_name, string $old_version, object|false $current): ?array
    {
        $result = null;

        if (\is_wp_error($upgrade_result)) {
            $error_code = $upgrade_result->get_error_code();
            $error_message = $upgrade_result->get_error_message();
            if ($this->isLicenseRelatedError($skin, $error_message)) {
                $result = $this->buildLicenseRequiredResult($error_message, $plugin_file, $plugin_name, $old_version, $current);
            } elseif ($this->isAlreadyUpToDateError($error_code, $error_message)) {
                $result = $this->buildAlreadyUpToDateResult($plugin_name, $old_version, $error_message);
            } else {
                $result = $this->buildUpgradeErrorResult($skin, $error_message, $plugin_file, $plugin_name, $old_version, $current);
            }
        } elseif ($upgrade_result === false) {
            $skin_error = $skin->get_first_error() ?? '';
            if (!$skin->has_errors()) {
                $result = $this->buildAlreadyUpToDateResult($plugin_name, $old_version);
            } elseif ($this->isAlreadyUpToDateError($skin_error, $skin_error)) {
                $result = $this->buildAlreadyUpToDateResult($plugin_name, $old_version, $skin_error);
            } else {
                $result = $this->buildUpgradeErrorResult(
                    $skin,
                    $skin_error !== '' ? $skin_error : 'Update process returned false',
                    $plugin_file,
                    $plugin_name,
                    $old_version,
                    $current
                );
            }
        } elseif ($skin->has_errors() && $skin->is_license_error()) {
            $result = $this->buildLicenseRequiredResult(
                $skin->get_first_error() ?? $skin->get_all_error_messages(),
                $plugin_file,
                $plugin_name,
                $old_version,
                $current
            );
        }

        return $result;
    }

    private function buildUpgradeErrorResult(PluginUpgraderSkin $skin, string $error_message, string $plugin_file, string $plugin_name, string $old_version, object|false $current): array
    {
        if ($skin->has_errors()) {
            $skin_err = $skin->get_first_error() ?? $skin->get_all_error_messages();
            if (!empty($skin_err) && strlen($skin_err) > strlen($error_message)) {
                $error_message = $skin_err;
            }
        }

        $is_license = $this->isLicenseRelatedError($skin, $error_message);
        if ($is_license) {
            return $this->buildLicenseRequiredResult($error_message, $plugin_file, $plugin_name, $old_version, $current);
        }

        if ($this->isAlreadyUpToDateError($error_message, $error_message)) {
            return $this->buildAlreadyUpToDateResult($plugin_name, $old_version, $error_message);
        }

        return [
            'status' => 'failed',
            'name' => $plugin_name,
            'message' => $error_message,
        ];
    }

    private function buildAlreadyUpToDateResult(string $plugin_name, string $old_version, string $message = ''): array
    {
        $friendly_message = !$this->isRawAlreadyUpToDateCode($message)
            ? $message
            : '';
        if ($friendly_message === '') {
            $friendly_message = \sprintf('Plugin "%s" is already up to date (version %s)', $plugin_name, $old_version);
        }

        return [
            'status' => 'already_up_to_date',
            'name' => $plugin_name,
            'message' => $friendly_message,
            'version' => $old_version,
        ];
    }

    /**
     * WordPress / premium updaters may return WP_Error codes like up_to_date instead of upgrade=false.
     */
    private function isAlreadyUpToDateError(string $error_code, string $error_message): bool
    {
        $normalized_code = \strtolower($error_code);
        $already_up_to_date_codes = ['up_to_date', 'already_up_to_date', 'no_update_available'];
        $matches_code = \in_array($normalized_code, $already_up_to_date_codes, true);
        $normalized_message = \strtolower(\trim($error_message));
        $matches_raw_message = $normalized_message !== ''
            && \in_array($normalized_message, $already_up_to_date_codes, true);
        $matches_message = $matches_raw_message || (bool) \preg_match(
            '/already up to date|up to date|no update available/i',
            $error_message
        );

        return $matches_code || $matches_message;
    }

    private function isRawAlreadyUpToDateCode(string $message): bool
    {
        $normalized_message = \strtolower(\trim($message));
        $raw_codes = ['up_to_date', 'already_up_to_date', 'no_update_available'];

        return $normalized_message === '' || \in_array($normalized_message, $raw_codes, true);
    }

    private function isLicenseRelatedError(PluginUpgraderSkin $skin, string $error_message): bool
    {
        return $skin->is_license_error() || (bool) \preg_match('/license|licence|activation|premium|subscription|key/i', $error_message);
    }

    private function buildLicenseRequiredResult(string $message, string $plugin_file, string $plugin_name, string $old_version, object|false $current): array
    {
        $update_info = (is_object($current) && isset($current->response[$plugin_file]))
            ? $current->response[$plugin_file]
            : null;
        $new_version = is_object($update_info) && isset($update_info->new_version) ? $update_info->new_version : '';
        return [
            'status' => 'license_required',
            'name' => $plugin_name,
            'message' => $message,
            'current_version' => $old_version,
            'new_version' => $new_version,
        ];
    }
}
