<?php
/**
 * REST package upload executor — plugin/theme ZIP installs without wp-admin HTML noise.
 *
 * @package FleekDash\Controllers\Plugins
 */

namespace FleekDash\Controllers\Plugins;

use FleekDash\Services\ResponseService;
use WP_REST_Request;
use WP_REST_Response;

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

/**
 * Handles ZIP uploads via WordPress upgrader APIs and returns structured JSON.
 */
class PackageUploadExecutor
{
    private const PLUGIN_OVERWRITE_VALUES = ['update-plugin', 'downgrade-plugin'];
    private const THEME_OVERWRITE_VALUES = ['update-theme', 'downgrade-theme'];

    private WordPressUpdateLoader $wpLoader;

    public function __construct(?WordPressUpdateLoader $wpLoader = null)
    {
        $this->wpLoader = $wpLoader ?? new WordPressUpdateLoader();
    }

    public function uploadPlugin(WP_REST_Request $request): WP_REST_Response
    {
        return $this->uploadPackage($request, [
            'type' => 'plugin',
            'capability' => 'upload_plugins',
            'file_field' => 'pluginzip',
            'overwrite_values' => self::PLUGIN_OVERWRITE_VALUES,
            'load' => fn () => $this->wpLoader->load(),
            'failure_label' => 'Plugin installation failed.',
            'success_label' => 'Plugin installed successfully.',
        ]);
    }

    public function uploadTheme(WP_REST_Request $request): WP_REST_Response
    {
        return $this->uploadPackage($request, [
            'type' => 'theme',
            'capability' => 'upload_themes',
            'file_field' => 'themezip',
            'overwrite_values' => self::THEME_OVERWRITE_VALUES,
            'load' => fn () => $this->wpLoader->loadThemeUpgrade(),
            'failure_label' => 'Theme installation failed.',
            'success_label' => 'Theme installed successfully.',
        ]);
    }

    /**
     * @param array{
     *   type: string,
     *   capability: string,
     *   file_field: string,
     *   overwrite_values: list<string>,
     *   load: callable,
     *   failure_label: string,
     *   success_label: string
     * } $config
     */
    private function uploadPackage(WP_REST_Request $request, array $config): WP_REST_Response
    {
        @\set_time_limit(120);
        $response = null;

        if (!\current_user_can($config['capability'])) {
            $response = ResponseService::error(
                'You are not allowed to upload packages on this site.',
                null,
                403,
            );
        }

        if ($response === null) {
            ($config['load'])();
            $overwrite = $this->resolveOverwriteParam($request, $config['overwrite_values']);
            $packageState = $this->resolvePackageState($request, $config['file_field']);

            if ($packageState instanceof \WP_Error) {
                $response = ResponseService::error(
                    $packageState->get_error_message(),
                    ['code' => $packageState->get_error_code()],
                    400,
                );
            } else {
                $response = $this->runPackageInstall(
                    $packageState,
                    $overwrite,
                    $config,
                );
            }
        }

        return $response;
    }

    /**
     * @param array{
     *   type: string,
     *   overwrite_values: list<string>,
     *   failure_label: string,
     *   success_label: string
     * } $config
     * @param array{path: string, package_id: int, cleanup: callable} $packageState
     */
    private function runPackageInstall(
        array $packageState,
        string $overwrite,
        array $config,
    ): WP_REST_Response {
        $skin = new PackageInstallerSkin(['overwrite' => $overwrite]);
        $isPlugin = $config['type'] === 'plugin';
        $upgrader = $isPlugin ? new \Plugin_Upgrader($skin) : new \Theme_Upgrader($skin);
        $installResult = $upgrader->install(
            $packageState['path'],
            ['overwrite_package' => $overwrite !== ''],
        );

        $response = null;
        $isFolderExists = \is_wp_error($installResult)
            && $installResult->get_error_code() === 'folder_exists';

        if ($isFolderExists) {
            $response = $this->buildFolderExistsResponse(
                $packageState,
                $upgrader,
                $installResult,
                $config['type'],
                $config['overwrite_values'],
            );
        } elseif ($installResult !== true) {
            $packageState['cleanup']();
            $message = \is_wp_error($installResult)
                ? $installResult->get_error_message()
                : $config['failure_label'];
            $response = ResponseService::error($message, [
                'code' => \is_wp_error($installResult)
                    ? $installResult->get_error_code()
                    : 'install_failed',
            ], 500);
        } else {
            $packageState['cleanup']();
            $payload = $isPlugin
                ? $this->buildPluginSuccessPayload($upgrader)
                : $this->buildThemeSuccessPayload($upgrader);
            $response = ResponseService::success($config['success_label'], $payload);
        }

        return $response;
    }

    /**
     * @param \Plugin_Upgrader $upgrader
     * @return array<string, string>
     */
    private function buildPluginSuccessPayload(\WP_Upgrader $upgrader): array
    {
        \FleekDash\Core\PluginHelper::ensurePluginApiLoaded();
        $pluginFile = $upgrader instanceof \Plugin_Upgrader ? $upgrader->plugin_info() : false;
        $payload = [
            'pluginPath' => \is_string($pluginFile) ? $pluginFile : '',
            'message' => 'Plugin installed successfully.',
        ];

        if (\is_string($pluginFile) && $pluginFile !== '') {
            $payload['activateNonce'] = \wp_create_nonce('activate-plugin_' . $pluginFile);
        }

        return $payload;
    }

    /**
     * @param \Theme_Upgrader $upgrader
     * @return array<string, string>
     */
    private function buildThemeSuccessPayload(\WP_Upgrader $upgrader): array
    {
        $theme = $upgrader instanceof \Theme_Upgrader ? $upgrader->theme_info() : false;
        $stylesheet = $theme instanceof \WP_Theme ? $theme->get_stylesheet() : '';
        $payload = [
            'stylesheet' => $stylesheet,
            'message' => 'Theme installed successfully.',
        ];

        if ($stylesheet !== '') {
            $payload['activateNonce'] = \wp_create_nonce('switch-theme_' . $stylesheet);
            $payload['activateUrl'] = \wp_nonce_url(
                \self_admin_url('themes.php?action=activate&stylesheet=' . \rawurlencode($stylesheet)),
                'switch-theme_' . $stylesheet,
            );
        }

        return $payload;
    }

    /**
     * @param list<string> $allowed
     */
    private function resolveOverwriteParam(WP_REST_Request $request, array $allowed): string
    {
        $overwrite = (string) $request->get_param('overwrite');
        return \in_array($overwrite, $allowed, true) ? $overwrite : '';
    }

    /**
     * @return array{path: string, package_id: int, cleanup: callable}|\WP_Error
     */
    private function resolvePackageState(WP_REST_Request $request, string $field)
    {
        $result = null;
        $files = $request->get_file_params();

        if (!empty($files[$field]) && \is_array($files[$field])) {
            $result = $this->ingestUploadedZip($files[$field], $field);
        } else {
            $packageId = (int) $request->get_param('package');
            if ($packageId > 0) {
                $result = $this->resolveStoredPackage($packageId);
            } else {
                $result = new \WP_Error('no_package', 'Please select a ZIP file to upload.');
            }
        }

        return $result;
    }

    /**
     * @param array<string, mixed> $file
     * @return array{path: string, package_id: int, cleanup: callable}|\WP_Error
     */
    private function ingestUploadedZip(array $file, string $field)
    {
        $result = $this->validateUploadedZipFile($file);

        if ($result === null) {
            require_once ABSPATH . 'wp-admin/includes/file.php';

            $handled = \wp_handle_upload(
                $file,
                ['test_form' => false, 'test_type' => false],
            );

            $result = $this->validateHandledUpload($handled);

            if ($result === null) {
                $attachmentId = (int) \wp_insert_attachment(
                    [
                        'post_title' => (string) $file['name'],
                        'post_content' => $handled['url'] ?? '',
                        'post_mime_type' => $handled['type'] ?? 'application/zip',
                        'guid' => $handled['url'] ?? '',
                        'context' => 'upgrader',
                        'post_status' => 'private',
                    ],
                    $handled['file'],
                );

                if ($attachmentId <= 0) {
                    \wp_delete_file($handled['file']);
                    $result = new \WP_Error('upload_error', 'Could not store uploaded package.');
                } else {
                    \wp_schedule_single_event(
                        \time() + 2 * HOUR_IN_SECONDS,
                        'upgrader_scheduled_cleanup',
                        [$attachmentId],
                    );

                    fleekdash_log_debug(
                        '[PackageUpload] Stored ' . $field . ' attachment id=' . $attachmentId,
                    );

                    $result = $this->buildPackageState($handled['file'], $attachmentId);
                }
            }
        }

        return $result;
    }

    /**
     * @param array<string, mixed> $file
     */
    private function validateUploadedZipFile(array $file): ?\WP_Error
    {
        $name = isset($file['name']) ? (string) $file['name'] : '';
        $isValidZipName = $name !== '' && \str_ends_with(\strtolower($name), '.zip');
        $isValidUpload = isset($file['tmp_name'])
            && \is_uploaded_file((string) $file['tmp_name']);
        $uploadOk = ($file['error'] ?? UPLOAD_ERR_OK) === UPLOAD_ERR_OK;

        $error = null;
        if (!$isValidZipName) {
            $error = new \WP_Error('invalid_archive', 'Only .zip archives may be uploaded.');
        } elseif (!$isValidUpload) {
            $error = new \WP_Error('invalid_upload', 'Uploaded file is missing or invalid.');
        } elseif (!$uploadOk) {
            $error = new \WP_Error('upload_error', 'Failed to receive uploaded file.');
        }

        return $error;
    }

    /**
     * @param array<string, mixed>|null $handled
     */
    private function validateHandledUpload(?array $handled): ?\WP_Error
    {
        $error = null;

        if (!empty($handled['error'])) {
            $error = new \WP_Error('upload_error', (string) $handled['error']);
        } elseif (!isset($handled['file']) || !\is_string($handled['file'])) {
            $error = new \WP_Error('upload_error', 'Upload handler did not return a file path.');
        } elseif (!\wp_zip_file_is_valid($handled['file'])) {
            \wp_delete_file($handled['file']);
            $error = new \WP_Error('incompatible_archive', 'Incompatible archive.');
        }

        return $error;
    }

    /**
     * @return array{path: string, package_id: int, cleanup: callable}|\WP_Error
     */
    private function resolveStoredPackage(int $packageId)
    {
        $attachment = \get_post($packageId);
        $path = \is_object($attachment) ? \get_attached_file($packageId) : false;
        $pathOk = \is_string($path) && $path !== '' && \file_exists($path);

        $result = null;
        if (!$attachment) {
            $result = new \WP_Error('invalid_package', 'Stored upload package was not found.');
        } elseif (!$pathOk) {
            $result = new \WP_Error('invalid_package', 'Stored upload package file is missing.');
        } else {
            $result = $this->buildPackageState($path, $packageId);
        }

        return $result;
    }

    /**
     * @return array{path: string, package_id: int, cleanup: callable}
     */
    private function buildPackageState(string $path, int $packageId): array
    {
        return [
            'path' => $path,
            'package_id' => $packageId,
            'cleanup' => static function () use ($packageId): void {
                if ($packageId > 0) {
                    \wp_delete_attachment($packageId, true);
                }
            },
        ];
    }

    /**
     * @param array{path: string, package_id: int, cleanup: callable} $packageState
     * @param list<string> $overwriteValues
     */
    private function buildFolderExistsResponse(
        array $packageState,
        \WP_Upgrader $upgrader,
        \WP_Error $folderError,
        string $packageType,
        array $overwriteValues,
    ): WP_REST_Response {
        $suggestedOverwrite = $overwriteValues[0];
        $newData = $packageType === 'plugin'
            ? ($upgrader->new_plugin_data ?? [])
            : ($upgrader->new_theme_data ?? []);

        if (!empty($newData['Version']) && $packageType === 'plugin') {
            $existingVersion = $this->resolveExistingPluginVersion($folderError);
            if (
                $existingVersion !== ''
                && \version_compare($existingVersion, (string) $newData['Version'], '>')
            ) {
                $suggestedOverwrite = $overwriteValues[1] ?? $overwriteValues[0];
            }
        }

        return \rest_ensure_response([
            'success' => false,
            'message' => 'Destination folder already exists.',
            'data' => [
                'code' => 'folder_exists',
                'packageId' => $packageState['package_id'],
                'suggestedOverwrite' => $suggestedOverwrite,
                'overwriteOptions' => $overwriteValues,
            ],
        ]);
    }

    private function resolveExistingPluginVersion(\WP_Error $folderError): string
    {
        $folder = $folderError->get_error_data();
        $version = '';

        if (\is_string($folder) && $folder !== '') {
            $folderSlug = \ltrim(\substr($folder, \strlen(WP_PLUGIN_DIR)), '/');
            $allPlugins = \get_plugins();
            foreach ($allPlugins as $plugin => $pluginData) {
                if (\str_starts_with($plugin, $folderSlug)) {
                    $version = isset($pluginData['Version']) ? (string) $pluginData['Version'] : '';
                    break;
                }
            }
        }

        return $version;
    }
}
