<?php
declare(strict_types=1);

namespace Help4BuilderSuite\Modules\ReleaseBridge;

use Help4BuilderSuite\Contracts\ModuleContract;
use Help4BuilderSuite\Modules\Demos\Module as DemosModule;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;

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

final class Module implements ModuleContract
{
    private const MAX_PACKAGE_BYTES = 52428800;
    private const MAX_CHUNK_BYTES = 1048576;
    private const PACKAGE_HOSTS = [
        'help4wordpress.com',
        'md.help4wordpress.com',
        'plugins.help4wordpress.com',
        'plugin.help4wordpress.com',
    ];

    public function key(): string
    {
        return 'release_bridge';
    }

    public function boot(): void
    {
        add_action('rest_api_init', [$this, 'registerRestRoutes']);
        add_action('admin_menu', [$this, 'registerAdminPage']);
    }

    public function registerAdminPage(): void
    {
        add_submenu_page(
            'h4bs-settings',
            __('Release Bridge', 'help4-builder-suite'),
            __('Release Bridge', 'help4-builder-suite'),
            'manage_options',
            'h4bs-release-bridge',
            [$this, 'renderAdminPage']
        );
    }

    public function renderAdminPage(): void
    {
        if (! current_user_can('manage_options')) {
            return;
        }
        ?>
        <div class="wrap h4bs-admin-wrap">
            <h1><?php esc_html_e('Help4 Release Bridge', 'help4-builder-suite'); ?></h1>
            <p><?php esc_html_e('Admin-only REST tools for approved tutorial updates, Builder Suite releases, and Help4 Blank theme releases. Use a dedicated WordPress application password for automation; do not place account passwords in scripts.', 'help4-builder-suite'); ?></p>
            <table class="widefat striped">
                <tbody>
                    <tr>
                        <th><?php esc_html_e('Status', 'help4-builder-suite'); ?></th>
                        <td><code><?php echo esc_html(rest_url('h4bs/v1/release-bridge/status')); ?></code></td>
                    </tr>
                    <tr>
                        <th><?php esc_html_e('Page Batch', 'help4-builder-suite'); ?></th>
                        <td><code><?php echo esc_html(rest_url('h4bs/v1/release-bridge/pages')); ?></code></td>
                    </tr>
                    <tr>
                        <th><?php esc_html_e('Builder Suite Release', 'help4-builder-suite'); ?></th>
                        <td><code><?php echo esc_html(rest_url('h4bs/v1/release-bridge/builder-suite-release')); ?></code></td>
                    </tr>
                    <tr>
                        <th><?php esc_html_e('Package Chunk Upload', 'help4-builder-suite'); ?></th>
                        <td><code><?php echo esc_html(rest_url('h4bs/v1/release-bridge/package-chunk')); ?></code></td>
                    </tr>
                    <tr>
                        <th><?php esc_html_e('Tutorial Media Release', 'help4-builder-suite'); ?></th>
                        <td><code><?php echo esc_html(rest_url('h4bs/v1/release-bridge/tutorial-media-release')); ?></code></td>
                    </tr>
                    <tr>
                        <th><?php esc_html_e('Blank Theme Release', 'help4-builder-suite'); ?></th>
                        <td><code><?php echo esc_html(rest_url('h4bs/v1/release-bridge/blank-theme-release')); ?></code></td>
                    </tr>
                    <tr>
                        <th><?php esc_html_e('Builder Suite Upgrade', 'help4-builder-suite'); ?></th>
                        <td><code><?php echo esc_html(rest_url('h4bs/v1/release-bridge/builder-suite-upgrade')); ?></code></td>
                    </tr>
                    <tr>
                        <th><?php esc_html_e('Help4 Blank Upgrade', 'help4-builder-suite'); ?></th>
                        <td><code><?php echo esc_html(rest_url('h4bs/v1/release-bridge/blank-theme-upgrade')); ?></code></td>
                    </tr>
                </tbody>
            </table>
        </div>
        <?php
    }

    public function registerRestRoutes(): void
    {
        register_rest_route(
            'h4bs/v1',
            '/release-bridge/status',
            [
                'methods'             => 'GET',
                'callback'            => [$this, 'status'],
                'permission_callback' => [$this, 'canManage'],
            ]
        );

        register_rest_route(
            'h4bs/v1',
            '/release-bridge/pages',
            [
                'methods'             => 'POST',
                'callback'            => [$this, 'upsertPages'],
                'permission_callback' => [$this, 'canManage'],
            ]
        );

        register_rest_route(
            'h4bs/v1',
            '/release-bridge/marketing-content',
            [
                'methods'             => 'POST',
                'callback'            => [$this, 'refreshMarketingContent'],
                'permission_callback' => [$this, 'canManage'],
            ]
        );

        register_rest_route(
            'h4bs/v1',
            '/release-bridge/builder-suite-release',
            [
                'methods'             => 'POST',
                'callback'            => [$this, 'publishBuilderSuiteRelease'],
                'permission_callback' => [$this, 'canManage'],
            ]
        );

        register_rest_route(
            'h4bs/v1',
            '/release-bridge/builder-suite-upgrade',
            [
                'methods'             => 'POST',
                'callback'            => [$this, 'upgradeBuilderSuite'],
                'permission_callback' => [$this, 'canManage'],
            ]
        );

        register_rest_route(
            'h4bs/v1',
            '/release-bridge/package-chunk',
            [
                'methods'             => 'POST',
                'callback'            => [$this, 'stagePackageChunk'],
                'permission_callback' => [$this, 'canManage'],
            ]
        );

        register_rest_route(
            'h4bs/v1',
            '/release-bridge/tutorial-media-release',
            [
                'methods'             => 'POST',
                'callback'            => [$this, 'publishTutorialMediaRelease'],
                'permission_callback' => [$this, 'canManage'],
            ]
        );

        register_rest_route(
            'h4bs/v1',
            '/release-bridge/blank-theme-release',
            [
                'methods'             => 'POST',
                'callback'            => [$this, 'publishBlankThemeRelease'],
                'permission_callback' => [$this, 'canManage'],
            ]
        );

        register_rest_route(
            'h4bs/v1',
            '/release-bridge/blank-theme-upgrade',
            [
                'methods'             => 'POST',
                'callback'            => [$this, 'upgradeBlankTheme'],
                'permission_callback' => [$this, 'canManage'],
            ]
        );
    }

    public function canManage(): bool
    {
        return current_user_can('manage_options');
    }

    public function status(): WP_REST_Response
    {
        $upload = wp_upload_dir(null, false);
        $updateSummary = $this->updateSummary();

        return new WP_REST_Response(
            [
                'ok'                           => true,
                'site'                         => home_url('/'),
                'wordpress_version'            => $updateSummary['wordpress_version'],
                'plugin_version'               => defined('H4BS_VERSION') ? H4BS_VERSION : '',
                'build_hash'                   => defined('H4BS_BUILD_HASH') ? H4BS_BUILD_HASH : '',
                'release_bridge'               => 'enabled',
                'supports_package_url'         => true,
                'supports_chunked_package_upload' => true,
                'supports_tutorial_media_release' => true,
                'supports_plugin_upgrade'      => true,
                'supports_blank_theme_release' => true,
                'supports_blank_theme_upgrade' => true,
                'max_package_bytes'            => self::MAX_PACKAGE_BYTES,
                'max_chunk_bytes'              => self::MAX_CHUNK_BYTES,
                'uploads_writable'             => wp_is_writable((string) $upload['basedir']),
                'static_writable'              => is_dir(ABSPATH . 'help4-builder-suite')
                    ? wp_is_writable(ABSPATH . 'help4-builder-suite')
                    : wp_is_writable(ABSPATH),
                'updates'                      => $updateSummary['updates'],
                'endpoints'                    => [
                    'pages'                 => rest_url('h4bs/v1/release-bridge/pages'),
                    'builder_suite_release' => rest_url('h4bs/v1/release-bridge/builder-suite-release'),
                    'package_chunk'         => rest_url('h4bs/v1/release-bridge/package-chunk'),
                    'tutorial_media_release' => rest_url('h4bs/v1/release-bridge/tutorial-media-release'),
                    'blank_theme_release'   => rest_url('h4bs/v1/release-bridge/blank-theme-release'),
                    'blank_theme_upgrade'   => rest_url('h4bs/v1/release-bridge/blank-theme-upgrade'),
                    'builder_suite_upgrade' => rest_url('h4bs/v1/release-bridge/builder-suite-upgrade'),
                ],
            ]
        );
    }

    private function updateSummary(): array
    {
        $this->loadUpdateFunctions();

        return [
            'wordpress_version' => (string) get_bloginfo('version'),
            'updates'           => [
                'core'    => $this->coreUpdateSummary(),
                'plugins' => $this->pluginUpdateSummary(),
                'themes'  => $this->themeUpdateSummary(),
            ],
        ];
    }

    private function loadUpdateFunctions(): void
    {
        $files = [
            ABSPATH . 'wp-admin/includes/update.php',
            ABSPATH . 'wp-admin/includes/plugin.php',
            ABSPATH . 'wp-admin/includes/theme.php',
        ];

        foreach ($files as $file) {
            if (is_readable($file)) {
                require_once $file;
            }
        }
    }

    private function coreUpdateSummary(): array
    {
        if (function_exists('wp_version_check')) {
            wp_version_check([], true);
        }

        $updates = function_exists('get_core_updates') ? get_core_updates(['dismissed' => false]) : [];
        if (! is_array($updates)) {
            $updates = [];
        }

        $items = [];
        foreach ($updates as $update) {
            if (! is_object($update)) {
                continue;
            }

            $items[] = [
                'current'  => isset($update->current) ? (string) $update->current : '',
                'response' => isset($update->response) ? (string) $update->response : '',
                'locale'   => isset($update->locale) ? (string) $update->locale : '',
            ];
        }

        return [
            'current_version' => (string) get_bloginfo('version'),
            'available'       => count(array_filter($items, static fn(array $item): bool => $item['response'] !== '' && $item['response'] !== 'latest')) > 0,
            'items'           => $items,
        ];
    }

    private function pluginUpdateSummary(): array
    {
        if (function_exists('wp_update_plugins')) {
            wp_update_plugins();
        }

        $knownPlugins = function_exists('get_plugins') ? get_plugins() : [];
        $updates = get_site_transient('update_plugins');
        $responses = is_object($updates) && isset($updates->response) && is_array($updates->response)
            ? $updates->response
            : [];

        $items = [];
        foreach ($responses as $pluginFile => $update) {
            if (! is_object($update)) {
                continue;
            }

            $items[] = [
                'plugin'          => (string) $pluginFile,
                'slug'            => isset($update->slug) ? (string) $update->slug : '',
                'current_version' => isset($knownPlugins[$pluginFile]['Version']) ? (string) $knownPlugins[$pluginFile]['Version'] : '',
                'new_version'     => isset($update->new_version) ? (string) $update->new_version : '',
            ];
        }

        return [
            'available' => count($items) > 0,
            'count'     => count($items),
            'items'     => $items,
        ];
    }

    private function themeUpdateSummary(): array
    {
        if (function_exists('wp_update_themes')) {
            wp_update_themes();
        }

        $knownThemes = function_exists('wp_get_themes') ? wp_get_themes() : [];
        $updates = get_site_transient('update_themes');
        $responses = is_object($updates) && isset($updates->response) && is_array($updates->response)
            ? $updates->response
            : [];

        $items = [];
        foreach ($responses as $stylesheet => $update) {
            if (! is_array($update)) {
                continue;
            }

            $theme = $knownThemes[$stylesheet] ?? null;
            $items[] = [
                'theme'           => (string) $stylesheet,
                'current_version' => $theme && method_exists($theme, 'get') ? (string) $theme->get('Version') : '',
                'new_version'     => isset($update['new_version']) ? (string) $update['new_version'] : '',
            ];
        }

        return [
            'available' => count($items) > 0,
            'count'     => count($items),
            'items'     => $items,
        ];
    }

    public function upsertPages(WP_REST_Request $request)
    {
        $pages = $request->get_param('pages');
        if (! is_array($pages)) {
            return new WP_Error('h4bs_bridge_pages_required', __('Expected a pages array.', 'help4-builder-suite'), ['status' => 400]);
        }

        $dryRun = rest_sanitize_boolean($request->get_param('dry_run') ?? false);
        $results = [];
        foreach ($pages as $page) {
            if (! is_array($page)) {
                continue;
            }

            $slug = sanitize_title((string) ($page['slug'] ?? ''));
            if ($slug === '') {
                $results[] = ['ok' => false, 'error' => 'missing_slug'];
                continue;
            }

            $postType = sanitize_key((string) ($page['post_type'] ?? 'page'));
            if (! in_array($postType, ['page', 'post'], true)) {
                $results[] = ['ok' => false, 'slug' => $slug, 'error' => 'unsupported_post_type'];
                continue;
            }

            $status = sanitize_key((string) ($page['status'] ?? 'publish'));
            if (! in_array($status, ['publish', 'draft', 'private'], true)) {
                $status = 'draft';
            }

            $postId = $this->findPostIdBySlug($slug, $postType);
            $action = $postId > 0 ? 'update' : 'insert';
            if ($dryRun) {
                $results[] = ['ok' => true, 'slug' => $slug, 'action' => $action, 'id' => $postId];
                continue;
            }

            $postData = [
                'post_name'    => $slug,
                'post_type'    => $postType,
                'post_status'  => $status,
                'post_title'   => sanitize_text_field((string) ($page['title'] ?? $slug)),
                'post_excerpt' => wp_kses_post((string) ($page['excerpt'] ?? '')),
                'post_content' => (string) ($page['content'] ?? ''),
            ];
            if ($postId > 0) {
                $postData['ID'] = $postId;
                $savedId = wp_update_post(wp_slash($postData), true);
            } else {
                $savedId = wp_insert_post(wp_slash($postData), true);
            }

            if (is_wp_error($savedId)) {
                $results[] = ['ok' => false, 'slug' => $slug, 'action' => $action, 'error' => $savedId->get_error_message()];
                continue;
            }

            $savedId = (int) $savedId;
            $this->updateAllowedMeta($savedId, is_array($page['meta'] ?? null) ? $page['meta'] : []);

            $result = ['ok' => true, 'slug' => $slug, 'action' => $action, 'id' => $savedId];
            if (is_array($page['builder_append_blocks'] ?? null)) {
                $prefix = sanitize_key((string) ($page['builder_append_prefix'] ?? 'release-bridge-'));
                if ($prefix === '') {
                    $prefix = 'release-bridge-';
                }
                $builderResult = $this->appendBuilderLayoutBlocks($savedId, $page['builder_append_blocks'], $prefix);
                $result['builder_layout_count'] = $builderResult['count'];
                $result['builder_layout_appended'] = $builderResult['appended'];
            }

            if (! empty($page['builder_enabled'])) {
                update_post_meta($savedId, '_h4_builder_mode', 'help4');
                update_post_meta($savedId, '_h4_builder_enabled', '1');
            }

            $results[] = $result;
        }

        return new WP_REST_Response(['ok' => true, 'dry_run' => $dryRun, 'results' => $results]);
    }

    public function refreshMarketingContent(WP_REST_Request $request)
    {
        $result = (new DemosModule())->refreshMarketingContent(
            rest_sanitize_boolean($request->get_param('dry_run') ?? true)
        );
        if (empty($result['ok'])) {
            return new WP_Error(
                'h4bs_bridge_marketing_host',
                __('Marketing content refresh is limited to approved Help4 WordPress release sites.', 'help4-builder-suite'),
                ['status' => 403]
            );
        }

        return new WP_REST_Response($result);
    }

    public function publishBuilderSuiteRelease(WP_REST_Request $request)
    {
        $version = sanitize_text_field((string) $request->get_param('version'));
        if (! preg_match('/^\d+\.\d+\.\d+$/', $version)) {
            return new WP_Error('h4bs_bridge_bad_version', __('Expected a semantic release version like 1.3.126.', 'help4-builder-suite'), ['status' => 400]);
        }

        $zipPackage = $this->packageFromRequest($request);
        if (is_wp_error($zipPackage)) {
            return $zipPackage;
        }

        $manifest = $this->normalizeManifest((string) ($request->get_param('manifest_json') ?? ''));
        if ($manifest === '') {
            return new WP_Error('h4bs_bridge_bad_manifest', __('Expected valid manifest JSON.', 'help4-builder-suite'), ['status' => 400]);
        }

        $changelog = (string) ($request->get_param('changelog') ?? '');
        if ($changelog === '') {
            $changelog = 'Help4 Builder Suite ' . $version . "\n";
        }

        $dryRun = rest_sanitize_boolean($request->get_param('dry_run') ?? false);
        if ($dryRun) {
            return new WP_REST_Response(
                [
                    'ok'       => true,
                    'dry_run'  => true,
                    'version'  => $version,
                    'zip_size' => strlen($zipPackage['bytes']),
                    'package_source' => $zipPackage['source'],
                ]
            );
        }

        $upload = wp_upload_dir(null, false);
        $relativeBase = 'help4-releases/help4-builder-suite/' . $version;
        $releaseDir = trailingslashit((string) $upload['basedir']) . $relativeBase;
        if (! wp_mkdir_p($releaseDir)) {
            return new WP_Error('h4bs_bridge_release_dir', __('Could not create release directory.', 'help4-builder-suite'), ['status' => 500]);
        }

        $files = [
            'latest.zip' => $zipPackage['bytes'],
            'help4-builder-suite.zip' => $zipPackage['bytes'],
            'help4-builder-suite-' . $version . '.zip' => $zipPackage['bytes'],
            'manifest.json' => $manifest,
            'changelog.txt' => $changelog,
        ];

        $assets = [];
        foreach ($files as $name => $contents) {
            $path = trailingslashit($releaseDir) . $name;
            file_put_contents($path, $contents);
            $assets[$name] = [
                'path' => $path,
                'url'  => trailingslashit((string) $upload['baseurl']) . $relativeBase . '/' . $name,
                'size' => filesize($path),
            ];
        }

        $this->syncStaticReleaseFiles($releaseDir, $version);
        $this->registerReleaseAttachments($relativeBase, $assets, $version);
        $redirects = $this->registerReleaseRedirects($version);

        return new WP_REST_Response(
            [
                'ok'        => true,
                'version'   => $version,
                'package_source' => $zipPackage['source'],
                'assets'    => $assets,
                'redirects' => $redirects,
            ]
        );
    }

    public function publishBlankThemeRelease(WP_REST_Request $request)
    {
        $version = sanitize_text_field((string) $request->get_param('version'));
        if (! preg_match('/^\d+\.\d+\.\d+$/', $version)) {
            return new WP_Error('h4bs_bridge_bad_theme_version', __('Expected a semantic theme version like 1.0.36.', 'help4-builder-suite'), ['status' => 400]);
        }

        $zipPackage = $this->packageFromRequest($request, 'blank_theme');
        if (is_wp_error($zipPackage)) {
            return $zipPackage;
        }

        if (! $this->packageContainsBlankTheme($zipPackage['bytes'])) {
            return new WP_Error('h4bs_bridge_bad_theme_package', __('Package does not contain the expected Help4 Blank theme folder.', 'help4-builder-suite'), ['status' => 400]);
        }

        $manifest = $this->normalizeManifest((string) ($request->get_param('manifest_json') ?? ''));
        if ($manifest === '') {
            return new WP_Error('h4bs_bridge_bad_theme_manifest', __('Expected valid theme manifest JSON.', 'help4-builder-suite'), ['status' => 400]);
        }

        $changelog = (string) ($request->get_param('changelog') ?? '');
        if ($changelog === '') {
            $changelog = 'Help4 Blank ' . $version . "\n";
        }

        $dryRun = rest_sanitize_boolean($request->get_param('dry_run') ?? false);
        if ($dryRun) {
            return new WP_REST_Response(
                [
                    'ok'       => true,
                    'dry_run'  => true,
                    'version'  => $version,
                    'zip_size' => strlen($zipPackage['bytes']),
                    'package_source' => $zipPackage['source'],
                ]
            );
        }

        $upload = wp_upload_dir(null, false);
        $relativeBase = 'help4-releases/help4-blank/' . $version;
        $releaseDir = trailingslashit((string) $upload['basedir']) . $relativeBase;
        if (! wp_mkdir_p($releaseDir)) {
            return new WP_Error('h4bs_bridge_theme_release_dir', __('Could not create theme release directory.', 'help4-builder-suite'), ['status' => 500]);
        }

        $files = [
            'latest.zip' => $zipPackage['bytes'],
            'help4-blank.zip' => $zipPackage['bytes'],
            'help4-blank-' . $version . '.zip' => $zipPackage['bytes'],
            'manifest.json' => $manifest,
            'changelog.txt' => $changelog,
        ];

        $assets = [];
        foreach ($files as $name => $contents) {
            $path = trailingslashit($releaseDir) . $name;
            file_put_contents($path, $contents);
            $assets[$name] = [
                'path' => $path,
                'url'  => trailingslashit((string) $upload['baseurl']) . $relativeBase . '/' . $name,
                'size' => filesize($path),
            ];
        }

        $staticSync = $this->syncStaticThemeReleaseFiles($releaseDir, $version);
        if (! $staticSync['ok']) {
            return new WP_Error(
                'h4bs_bridge_theme_static_sync_failed',
                __('Theme release assets were written, but one or more public aliases could not be replaced and verified.', 'help4-builder-suite'),
                [
                    'status' => 500,
                    'version' => $version,
                    'static_sync' => $staticSync,
                ]
            );
        }
        $this->registerThemeReleaseAttachments($relativeBase, $assets, $version);
        $redirects = $this->registerThemeReleaseRedirects($version);

        return new WP_REST_Response(
            [
                'ok'        => true,
                'version'   => $version,
                'package_source' => $zipPackage['source'],
                'assets'    => $assets,
                'static_sync' => $staticSync,
                'redirects' => $redirects,
            ]
        );
    }

    public function stagePackageChunk(WP_REST_Request $request)
    {
        $packageType = sanitize_key((string) ($request->get_param('package_type') ?? 'builder_suite'));
        if (! in_array($packageType, ['builder_suite', 'blank_theme', 'tutorial_media'], true)) {
            return new WP_Error('h4bs_bridge_bad_package_type', __('Package type must be builder_suite, blank_theme, or tutorial_media.', 'help4-builder-suite'), ['status' => 400]);
        }

        $version = sanitize_text_field((string) $request->get_param('version'));
        if (! preg_match('/^\d+\.\d+\.\d+$/', $version)) {
            return new WP_Error('h4bs_bridge_chunk_bad_version', __('Expected a semantic release version like 1.3.126.', 'help4-builder-suite'), ['status' => 400]);
        }

        $chunkIndex = absint($request->get_param('chunk_index'));
        $totalChunks = absint($request->get_param('total_chunks'));
        if ($totalChunks < 1 || $chunkIndex >= $totalChunks || $totalChunks > 500) {
            return new WP_Error('h4bs_bridge_chunk_bad_index', __('Chunk index and total chunk count are not valid.', 'help4-builder-suite'), ['status' => 400]);
        }

        $uploadId = sanitize_key((string) ($request->get_param('upload_id') ?? ''));
        if ($uploadId === '') {
            $uploadId = md5($packageType . '|' . $version);
        }

        $chunkBase64 = (string) $request->get_param('chunk_base64');
        $chunkBytes = base64_decode($chunkBase64, true);
        if (! is_string($chunkBytes) || $chunkBytes === '') {
            return new WP_Error('h4bs_bridge_chunk_bad_payload', __('Expected a base64 encoded chunk.', 'help4-builder-suite'), ['status' => 400]);
        }

        if (strlen($chunkBytes) > self::MAX_CHUNK_BYTES) {
            return new WP_Error('h4bs_bridge_chunk_too_large', __('Chunk is larger than the release bridge allows.', 'help4-builder-suite'), ['status' => 413]);
        }

        $expectedSha256 = strtolower(sanitize_text_field((string) ($request->get_param('package_sha256') ?? '')));
        if ($expectedSha256 !== '' && ! preg_match('/^[a-f0-9]{64}$/', $expectedSha256)) {
            return new WP_Error('h4bs_bridge_chunk_bad_hash', __('Package SHA-256 must be a lowercase hexadecimal hash.', 'help4-builder-suite'), ['status' => 400]);
        }

        $staging = $this->packageStagingPaths($packageType, $version, $uploadId);
        if (is_wp_error($staging)) {
            return $staging;
        }

        $meta = [];
        if (is_readable($staging['meta'])) {
            $decoded = json_decode((string) file_get_contents($staging['meta']), true);
            $meta = is_array($decoded) ? $decoded : [];
        }

        if ($chunkIndex === 0) {
            if (is_readable($staging['part'])) {
                wp_delete_file($staging['part']);
            }
            $meta = [
                'package_type' => $packageType,
                'version' => $version,
                'upload_id' => $uploadId,
                'total_chunks' => $totalChunks,
                'received_chunks' => 0,
                'bytes' => 0,
                'package_sha256' => $expectedSha256,
            ];
        }

        $expectedIndex = absint($meta['received_chunks'] ?? 0);
        if ($chunkIndex !== $expectedIndex || absint($meta['total_chunks'] ?? 0) !== $totalChunks) {
            return new WP_Error('h4bs_bridge_chunk_out_of_order', __('Chunk upload must be sent in order. Restart from chunk 0 to replace the staged file.', 'help4-builder-suite'), ['status' => 409]);
        }

        $bytesBefore = absint($meta['bytes'] ?? 0);
        if ($bytesBefore + strlen($chunkBytes) > self::MAX_PACKAGE_BYTES) {
            return new WP_Error('h4bs_bridge_chunk_package_too_large', __('Package is larger than the release bridge allows.', 'help4-builder-suite'), ['status' => 413]);
        }

        $written = file_put_contents($staging['part'], $chunkBytes, FILE_APPEND | LOCK_EX);
        if (! is_int($written) || $written !== strlen($chunkBytes)) {
            return new WP_Error('h4bs_bridge_chunk_write_failed', __('Could not write the release package chunk.', 'help4-builder-suite'), ['status' => 500]);
        }

        $meta['received_chunks'] = $expectedIndex + 1;
        $meta['bytes'] = $bytesBefore + strlen($chunkBytes);
        file_put_contents($staging['meta'], wp_json_encode($meta, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));

        $complete = absint($meta['received_chunks']) === $totalChunks;
        if (! $complete) {
            return new WP_REST_Response(
                [
                    'ok' => true,
                    'complete' => false,
                    'version' => $version,
                    'package_type' => $packageType,
                    'upload_id' => $uploadId,
                    'received_chunks' => absint($meta['received_chunks']),
                    'total_chunks' => $totalChunks,
                    'bytes_received' => absint($meta['bytes']),
                ]
            );
        }

        $bytes = (string) file_get_contents($staging['part']);
        if (! $this->isValidZipPackage($bytes)) {
            return new WP_Error('h4bs_bridge_chunk_bad_zip', __('Completed package was not a valid ZIP payload.', 'help4-builder-suite'), ['status' => 400]);
        }

        if ($expectedSha256 !== '' && hash('sha256', $bytes) !== $expectedSha256) {
            return new WP_Error('h4bs_bridge_chunk_hash_mismatch', __('Completed package hash did not match the expected SHA-256.', 'help4-builder-suite'), ['status' => 400]);
        }

        if ($packageType === 'blank_theme') {
            $containsExpectedPackage = $this->packageContainsBlankTheme($bytes);
        } elseif ($packageType === 'tutorial_media') {
            $containsExpectedPackage = $this->packageContainsTutorialMedia($bytes);
        } else {
            $containsExpectedPackage = $this->packageContainsBuilderSuite($bytes);
        }
        if (! $containsExpectedPackage) {
            return new WP_Error('h4bs_bridge_chunk_bad_package', __('Completed ZIP does not contain the expected Help4 package.', 'help4-builder-suite'), ['status' => 400]);
        }

        if (is_readable($staging['final'])) {
            wp_delete_file($staging['final']);
        }

        if (! function_exists('WP_Filesystem')) {
            require_once ABSPATH . 'wp-admin/includes/file.php';
        }
        global $wp_filesystem;
        if (! WP_Filesystem() || ! $wp_filesystem || ! $wp_filesystem->move($staging['part'], $staging['final'], true)) {
            return new WP_Error('h4bs_bridge_chunk_finalize_failed', __('Could not finalize the staged release package.', 'help4-builder-suite'), ['status' => 500]);
        }

        wp_delete_file($staging['meta']);

        return new WP_REST_Response(
            [
                'ok' => true,
                'complete' => true,
                'version' => $version,
                'package_type' => $packageType,
                'upload_id' => $uploadId,
                'bytes_received' => absint($meta['bytes']),
                'package_sha256' => hash('sha256', $bytes),
                'package_url' => $staging['url'],
            ]
        );
    }

    public function publishTutorialMediaRelease(WP_REST_Request $request)
    {
        $siteHost = strtolower((string) wp_parse_url(home_url('/'), PHP_URL_HOST));
        if (! in_array($siteHost, $this->packageHosts(), true)) {
            return new WP_Error('h4bs_bridge_tutorial_media_host', __('Tutorial media releases are limited to approved Help4 WordPress hosts.', 'help4-builder-suite'), ['status' => 403]);
        }

        $version = sanitize_text_field((string) $request->get_param('version'));
        $installedVersion = defined('H4BS_VERSION') ? (string) H4BS_VERSION : '';
        if (! preg_match('/^\d+\.\d+\.\d+$/', $version) || $version !== $installedVersion) {
            return new WP_Error('h4bs_bridge_tutorial_media_version', __('The media package version must match the active Builder Suite release.', 'help4-builder-suite'), ['status' => 409]);
        }

        $uploadId = sanitize_key((string) ($request->get_param('upload_id') ?? ''));
        if ($uploadId === '') {
            $uploadId = md5('tutorial_media|' . $version);
        }
        $staging = $this->packageStagingPaths('tutorial_media', $version, $uploadId);
        if (is_wp_error($staging)) {
            return $staging;
        }
        if (! is_readable($staging['final'])) {
            return new WP_Error('h4bs_bridge_tutorial_media_missing', __('The verified tutorial media package has not been staged.', 'help4-builder-suite'), ['status' => 404]);
        }

        $packageBytes = (string) file_get_contents($staging['final']);
        $expectedSha256 = strtolower(sanitize_text_field((string) ($request->get_param('package_sha256') ?? '')));
        if (! preg_match('/^[a-f0-9]{64}$/', $expectedSha256) || hash('sha256', $packageBytes) !== $expectedSha256) {
            return new WP_Error('h4bs_bridge_tutorial_media_hash', __('The staged tutorial media package does not match the approved SHA-256.', 'help4-builder-suite'), ['status' => 409]);
        }
        if (! $this->packageContainsTutorialMedia($packageBytes)) {
            return new WP_Error('h4bs_bridge_tutorial_media_invalid', __('The staged tutorial media package does not match the active tutorial catalog.', 'help4-builder-suite'), ['status' => 400]);
        }
        if (! class_exists('ZipArchive')) {
            return new WP_Error('h4bs_bridge_tutorial_media_zip', __('ZipArchive is required to publish tutorial media.', 'help4-builder-suite'), ['status' => 500]);
        }

        if (! function_exists('WP_Filesystem')) {
            require_once ABSPATH . 'wp-admin/includes/file.php';
        }
        global $wp_filesystem;
        if (! WP_Filesystem() || ! $wp_filesystem instanceof \WP_Filesystem_Base) {
            return new WP_Error('h4bs_bridge_tutorial_media_filesystem', __('WordPress could not initialize direct filesystem access for tutorial media.', 'help4-builder-suite'), ['status' => 500]);
        }

        $upload = wp_get_upload_dir();
        $uploadBaseDir = (string) ($upload['basedir'] ?? '');
        $uploadBaseUrl = (string) ($upload['baseurl'] ?? '');
        if (! empty($upload['error']) || $uploadBaseDir === '' || $uploadBaseUrl === '') {
            return new WP_Error('h4bs_bridge_tutorial_media_uploads', __('WordPress did not provide a usable public uploads destination for tutorial media.', 'help4-builder-suite'), ['status' => 500]);
        }

        $destination = trailingslashit($uploadBaseDir) . 'help4-builder-tutorial-media/2026-08';
        if (! wp_mkdir_p($destination) || ! $wp_filesystem->is_writable($destination)) {
            return new WP_Error('h4bs_bridge_tutorial_media_writable', __('The tutorial media destination is not writable.', 'help4-builder-suite'), ['status' => 500]);
        }

        $expectedFiles = $this->tutorialMediaExpectedFiles();
        $zip = new \ZipArchive();
        if ($zip->open($staging['final']) !== true) {
            return new WP_Error('h4bs_bridge_tutorial_media_open', __('The staged tutorial media ZIP could not be opened.', 'help4-builder-suite'), ['status' => 500]);
        }

        $packageFiles = [];
        foreach ($expectedFiles as $filename => $expectedFileSha256) {
            $contents = $zip->getFromName($filename);
            $target = trailingslashit($destination) . $filename;
            if (! is_string($contents)) {
                if (! is_readable($target) || hash_file('sha256', $target) !== $expectedFileSha256) {
                    $zip->close();
                    return new WP_Error('h4bs_bridge_tutorial_media_baseline', __('The incremental tutorial media package requires the existing catalog files to be present and verified.', 'help4-builder-suite'), ['status' => 409]);
                }
                continue;
            }
            if (hash('sha256', $contents) !== $expectedFileSha256) {
                $zip->close();
                return new WP_Error('h4bs_bridge_tutorial_media_entry', __('A tutorial media file failed its catalog hash check.', 'help4-builder-suite'), ['status' => 500]);
            }
            $packageFiles[$filename] = $contents;
        }
        $zip->close();

        $published = [];
        foreach ($packageFiles as $filename => $contents) {
            $expectedFileSha256 = $expectedFiles[$filename];
            $target = trailingslashit($destination) . $filename;
            $temporary = $target . '.h4bs-new';
            $written = $wp_filesystem->put_contents($temporary, $contents, defined('FS_CHMOD_FILE') ? FS_CHMOD_FILE : 0644);
            if (! $written || hash_file('sha256', $temporary) !== $expectedFileSha256) {
                wp_delete_file($temporary);
                return new WP_Error('h4bs_bridge_tutorial_media_write', __('A tutorial media file could not be written and verified.', 'help4-builder-suite'), ['status' => 500]);
            }
            if (! $wp_filesystem->move($temporary, $target, true)) {
                wp_delete_file($temporary);
                return new WP_Error('h4bs_bridge_tutorial_media_move', __('A verified tutorial media file could not be moved into place.', 'help4-builder-suite'), ['status' => 500]);
            }
            $published[] = $filename;
        }

        $accessFile = trailingslashit($destination) . '.htaccess';
        $accessRules = $this->tutorialMediaAccessRules($siteHost);
        $accessWritten = $wp_filesystem->put_contents($accessFile, $accessRules, defined('FS_CHMOD_FILE') ? FS_CHMOD_FILE : 0644);
        if (! $accessWritten || $wp_filesystem->size($accessFile) !== strlen($accessRules)) {
            return new WP_Error('h4bs_bridge_tutorial_media_access', __('Tutorial media was written, but its delivery policy could not be installed.', 'help4-builder-suite'), ['status' => 500]);
        }

        foreach ($expectedFiles as $filename => $expectedFileSha256) {
            $target = trailingslashit($destination) . $filename;
            if (! is_readable($target) || hash_file('sha256', $target) !== $expectedFileSha256) {
                return new WP_Error('h4bs_bridge_tutorial_media_postflight', __('A tutorial media file failed the final on-disk verification.', 'help4-builder-suite'), ['status' => 500]);
            }
        }

        $mediaUrl = trailingslashit($uploadBaseUrl) . 'help4-builder-tutorial-media/2026-08/';
        update_option('h4bs_tutorial_video_media_base_url', esc_url_raw($mediaUrl), false);
        wp_delete_file($staging['final']);

        return new WP_REST_Response(
            [
                'ok' => true,
                'version' => $version,
                'package_sha256' => $expectedSha256,
                'media_count' => count($published),
                'catalog_count' => count($expectedFiles),
                'media_base_url' => $mediaUrl,
            ]
        );
    }

    public function upgradeBuilderSuite(WP_REST_Request $request)
    {
        if (! current_user_can('update_plugins') && ! current_user_can('install_plugins')) {
            return new WP_Error('h4bs_bridge_upgrade_forbidden', __('The release account cannot update plugins.', 'help4-builder-suite'), ['status' => 403]);
        }

        $targetVersion = sanitize_text_field((string) ($request->get_param('version') ?? ''));
        if ($targetVersion !== '' && ! preg_match('/^\d+\.\d+\.\d+$/', $targetVersion)) {
            return new WP_Error('h4bs_bridge_upgrade_bad_version', __('Expected a semantic release version like 1.3.133.', 'help4-builder-suite'), ['status' => 400]);
        }

        if ($targetVersion !== '' && version_compare(H4BS_VERSION, $targetVersion, '>=')) {
            return new WP_REST_Response(
                [
                    'ok' => true,
                    'dry_run' => rest_sanitize_boolean($request->get_param('dry_run') ?? false),
                    'upgraded' => false,
                    'reason' => 'already_current_or_newer',
                    'installed_version' => H4BS_VERSION,
                    'target_version' => $targetVersion,
                ]
            );
        }

        $zipPackage = $this->packageFromRequest($request);
        if (is_wp_error($zipPackage)) {
            return $zipPackage;
        }

        $dryRun = rest_sanitize_boolean($request->get_param('dry_run') ?? false);
        $pluginFile = plugin_basename(H4BS_FILE);
        $packageSize = strlen($zipPackage['bytes']);
        $containsBuilderSuite = $this->packageContainsBuilderSuite($zipPackage['bytes']);
        if (! $containsBuilderSuite) {
            return new WP_Error('h4bs_bridge_upgrade_bad_package', __('Package does not contain the expected Help4 Builder Suite plugin folder.', 'help4-builder-suite'), ['status' => 400]);
        }

        $packageVersion = $this->packageBuilderSuiteVersion($zipPackage['bytes']);
        if ($packageVersion === '') {
            return new WP_Error('h4bs_bridge_upgrade_package_version_missing', __('The Builder Suite package version could not be verified.', 'help4-builder-suite'), ['status' => 400]);
        }
        if ($targetVersion === '') {
            $targetVersion = $packageVersion;
        }
        if ($packageVersion !== $targetVersion) {
            return new WP_Error(
                'h4bs_bridge_upgrade_package_version_mismatch',
                __('The Builder Suite package version does not match the requested target version.', 'help4-builder-suite'),
                [
                    'status' => 400,
                    'target_version' => $targetVersion,
                    'package_version' => $packageVersion,
                ]
            );
        }

        $writablePreflight = $this->builderSuiteWritablePreflight();

        $preflight = [
            'ok' => $writablePreflight['writable'],
            'dry_run' => $dryRun,
            'upgraded' => false,
            'plugin' => $pluginFile,
            'installed_version' => H4BS_VERSION,
            'target_version' => $targetVersion,
            'package_version' => $packageVersion,
            'package_source' => $zipPackage['source'],
            'package_size' => $packageSize,
            'plugins_dir_writable' => wp_is_writable(WP_PLUGIN_DIR),
            'plugin_tree_writable' => $writablePreflight['writable'],
            'writable_paths_checked' => $writablePreflight['checked'],
            'blocked_paths' => $writablePreflight['blocked'],
            'blocked_paths_truncated' => $writablePreflight['truncated'],
            'active_before' => $this->isBuilderSuiteActive(),
        ];

        if ($dryRun) {
            return new WP_REST_Response($preflight);
        }

        if (! $writablePreflight['writable']) {
            return new WP_Error(
                'h4bs_bridge_plugin_tree_not_writable',
                __('WordPress cannot replace every file in the installed Builder Suite plugin tree.', 'help4-builder-suite'),
                [
                    'status' => 500,
                    'blocked_paths' => $writablePreflight['blocked'],
                    'blocked_paths_truncated' => $writablePreflight['truncated'],
                    'writable_paths_checked' => $writablePreflight['checked'],
                ]
            );
        }

        $tempFile = $this->createTempFile('h4bs-release-');
        if (! is_string($tempFile) || $tempFile === '') {
            return new WP_Error('h4bs_bridge_upgrade_tempfile', __('Could not create a temporary package file.', 'help4-builder-suite'), ['status' => 500]);
        }

        $bytesWritten = file_put_contents($tempFile, $zipPackage['bytes']);
        if (! is_int($bytesWritten) || $bytesWritten !== $packageSize) {
            wp_delete_file($tempFile);
            return new WP_Error('h4bs_bridge_upgrade_tempfile_write', __('Could not write the complete release package to temporary storage.', 'help4-builder-suite'), ['status' => 500]);
        }

        $result = $this->runPluginUpgrade($tempFile);
        wp_delete_file($tempFile);

        if (is_wp_error($result)) {
            return $result;
        }

        wp_clean_plugins_cache(true);
        delete_site_transient('update_plugins');
        $installedVersion = $this->installedBuilderSuiteVersion();
        $activeAfter = $this->isBuilderSuiteActive();

        if ($installedVersion !== $targetVersion) {
            return new WP_Error(
                'h4bs_bridge_upgrade_version_not_installed',
                __('WordPress finished the upgrade process, but the requested Builder Suite version is not installed.', 'help4-builder-suite'),
                [
                    'status' => 500,
                    'target_version' => $targetVersion,
                    'installed_version_after' => $installedVersion,
                    'active_after' => $activeAfter,
                    'upgrader_result' => $result,
                ]
            );
        }

        if ($preflight['active_before'] && ! $activeAfter) {
            return new WP_Error(
                'h4bs_bridge_upgrade_plugin_inactive',
                __('The requested Builder Suite version was installed, but the plugin is no longer active.', 'help4-builder-suite'),
                [
                    'status' => 500,
                    'target_version' => $targetVersion,
                    'installed_version_after' => $installedVersion,
                ]
            );
        }

        return new WP_REST_Response(
            array_merge(
                $preflight,
                [
                    'ok' => true,
                    'upgraded' => true,
                    'installed_version_after' => $installedVersion,
                    'active_after' => $activeAfter,
                    'upgrader_result' => $result,
                ]
            )
        );
    }

    public function upgradeBlankTheme(WP_REST_Request $request)
    {
        if (! current_user_can('update_themes') && ! current_user_can('install_themes')) {
            return new WP_Error('h4bs_bridge_theme_upgrade_forbidden', __('The release account cannot update themes.', 'help4-builder-suite'), ['status' => 403]);
        }

        $targetVersion = sanitize_text_field((string) ($request->get_param('version') ?? ''));
        if ($targetVersion !== '' && ! preg_match('/^\d+\.\d+\.\d+$/', $targetVersion)) {
            return new WP_Error('h4bs_bridge_theme_upgrade_bad_version', __('Expected a semantic theme version like 1.0.44.', 'help4-builder-suite'), ['status' => 400]);
        }

        $installedVersion = $this->installedBlankThemeVersion();
        if ($targetVersion !== '' && $installedVersion !== '' && version_compare($installedVersion, $targetVersion, '>=')) {
            return new WP_REST_Response(
                [
                    'ok'                => true,
                    'dry_run'           => rest_sanitize_boolean($request->get_param('dry_run') ?? false),
                    'upgraded'          => false,
                    'reason'            => 'already_current_or_newer',
                    'theme'             => 'help4-blank',
                    'installed_version' => $installedVersion,
                    'target_version'    => $targetVersion,
                    'active'            => get_stylesheet() === 'help4-blank',
                ]
            );
        }

        $zipPackage = $this->packageFromRequest($request, 'blank_theme');
        if (is_wp_error($zipPackage)) {
            return $zipPackage;
        }
        if (! $this->packageContainsBlankTheme($zipPackage['bytes'])) {
            return new WP_Error('h4bs_bridge_theme_upgrade_bad_package', __('Package does not contain the expected Help4 Blank theme folder.', 'help4-builder-suite'), ['status' => 400]);
        }

        $packageVersion = $this->packageBlankThemeVersion($zipPackage['bytes']);
        if ($packageVersion === '') {
            return new WP_Error('h4bs_bridge_theme_upgrade_package_version_missing', __('The Help4 Blank package version could not be verified.', 'help4-builder-suite'), ['status' => 400]);
        }
        if ($targetVersion === '') {
            $targetVersion = $packageVersion;
        }
        if ($packageVersion !== $targetVersion) {
            return new WP_Error(
                'h4bs_bridge_theme_upgrade_package_version_mismatch',
                __('The Help4 Blank package version does not match the requested target version.', 'help4-builder-suite'),
                [
                    'status'          => 400,
                    'target_version'  => $targetVersion,
                    'package_version' => $packageVersion,
                ]
            );
        }

        $dryRun = rest_sanitize_boolean($request->get_param('dry_run') ?? false);
        $activeBefore = get_stylesheet() === 'help4-blank';
        $writablePreflight = $this->blankThemeWritablePreflight();
        $themesDirWritable = wp_is_writable(get_theme_root());
        $upgradeStrategy = $writablePreflight['writable']
            ? 'wordpress_upgrader'
            : ($themesDirWritable ? 'atomic_directory_swap' : 'unavailable');
        $canUpgrade = $upgradeStrategy !== 'unavailable';
        $preflight = [
            'ok'                      => $canUpgrade,
            'dry_run'                 => $dryRun,
            'upgraded'                => false,
            'theme'                   => 'help4-blank',
            'installed_version'       => $installedVersion,
            'target_version'          => $targetVersion,
            'package_version'         => $packageVersion,
            'package_source'          => $zipPackage['source'],
            'package_size'            => strlen($zipPackage['bytes']),
            'themes_dir_writable'     => $themesDirWritable,
            'theme_tree_writable'     => $writablePreflight['writable'],
            'upgrade_strategy'        => $upgradeStrategy,
            'writable_paths_checked'  => $writablePreflight['checked'],
            'blocked_paths'           => $writablePreflight['blocked'],
            'blocked_paths_truncated' => $writablePreflight['truncated'],
            'active_before'           => $activeBefore,
        ];

        if ($dryRun) {
            return new WP_REST_Response($preflight);
        }
        if (! $canUpgrade) {
            return new WP_Error(
                'h4bs_bridge_theme_tree_not_writable',
                __('WordPress cannot replace the Help4 Blank theme tree or create an atomic sibling replacement.', 'help4-builder-suite'),
                [
                    'status'                  => 500,
                    'blocked_paths'           => $writablePreflight['blocked'],
                    'blocked_paths_truncated' => $writablePreflight['truncated'],
                    'writable_paths_checked'  => $writablePreflight['checked'],
                ]
            );
        }

        $tempFile = $this->createTempFile('h4bs-theme-release-');
        if (! is_string($tempFile) || $tempFile === '') {
            return new WP_Error('h4bs_bridge_theme_upgrade_tempfile', __('Could not create a temporary theme package file.', 'help4-builder-suite'), ['status' => 500]);
        }

        $packageSize = strlen($zipPackage['bytes']);
        $bytesWritten = file_put_contents($tempFile, $zipPackage['bytes']);
        if (! is_int($bytesWritten) || $bytesWritten !== $packageSize) {
            wp_delete_file($tempFile);
            return new WP_Error('h4bs_bridge_theme_upgrade_tempfile_write', __('Could not write the complete theme package to temporary storage.', 'help4-builder-suite'), ['status' => 500]);
        }

        $result = $upgradeStrategy === 'atomic_directory_swap'
            ? $this->runBlankThemeAtomicSwap($tempFile, $targetVersion)
            : $this->runBlankThemeUpgrade($tempFile);
        wp_delete_file($tempFile);
        if (is_wp_error($result)) {
            return $result;
        }

        wp_clean_themes_cache(true);
        delete_site_transient('update_themes');
        $installedAfter = $this->installedBlankThemeVersion();
        $activeAfter = get_stylesheet() === 'help4-blank';
        if ($installedAfter !== $targetVersion) {
            return new WP_Error(
                'h4bs_bridge_theme_upgrade_version_not_installed',
                __('WordPress finished the upgrade process, but the requested Help4 Blank version is not installed.', 'help4-builder-suite'),
                [
                    'status'                  => 500,
                    'target_version'          => $targetVersion,
                    'installed_version_after' => $installedAfter,
                    'active_after'            => $activeAfter,
                ]
            );
        }
        if ($activeBefore && ! $activeAfter) {
            return new WP_Error(
                'h4bs_bridge_theme_upgrade_inactive',
                __('Help4 Blank was active before the update but is no longer active.', 'help4-builder-suite'),
                [
                    'status'                  => 500,
                    'target_version'          => $targetVersion,
                    'installed_version_after' => $installedAfter,
                ]
            );
        }

        return new WP_REST_Response(
            array_merge(
                $preflight,
                [
                    'ok'                      => true,
                    'upgraded'                => true,
                    'installed_version_after' => $installedAfter,
                    'active_after'            => $activeAfter,
                    'upgrader_result'         => $result,
                ]
            )
        );
    }

    /**
     * @return array{dir:string,part:string,meta:string,final:string,url:string}|WP_Error
     */
    private function packageStagingPaths(string $packageType, string $version, string $uploadId): array|WP_Error
    {
        $upload = wp_upload_dir(null, false);
        $baseDir = (string) ($upload['basedir'] ?? '');
        $baseUrl = (string) ($upload['baseurl'] ?? '');
        if ($baseDir === '' || $baseUrl === '') {
            return new WP_Error('h4bs_bridge_chunk_upload_dir', __('WordPress uploads are not available.', 'help4-builder-suite'), ['status' => 500]);
        }

        if (! wp_mkdir_p($baseDir) || ! wp_is_writable($baseDir)) {
            return new WP_Error('h4bs_bridge_chunk_upload_not_writable', __('WordPress uploads are not writable.', 'help4-builder-suite'), ['status' => 500]);
        }

        $chunkDir = trailingslashit($baseDir) . '.h4bs-release-chunks';
        if (! wp_mkdir_p($chunkDir) || ! wp_is_writable($chunkDir)) {
            return new WP_Error('h4bs_bridge_chunk_dir_not_writable', __('Release chunk staging directory is not writable.', 'help4-builder-suite'), ['status' => 500]);
        }

        if ($packageType === 'blank_theme') {
            $packageSlug = 'help4-blank';
        } elseif ($packageType === 'tutorial_media') {
            $packageSlug = 'help4-builder-tutorial-media';
        } else {
            $packageSlug = 'help4-builder-suite';
        }
        $filename = sanitize_file_name($packageSlug . '-' . $version . '.zip');
        $chunkName = sanitize_file_name($uploadId . '-' . $filename);

        return [
            'dir' => $chunkDir,
            'part' => trailingslashit($chunkDir) . $chunkName . '.part',
            'meta' => trailingslashit($chunkDir) . $chunkName . '.json',
            'final' => trailingslashit($baseDir) . $filename,
            'url' => trailingslashit($baseUrl) . $filename,
        ];
    }

    /**
     * @return array{bytes: string, source: string}|WP_Error
     */
    private function packageFromRequest(WP_REST_Request $request, string $packageType = 'builder_suite'): array|WP_Error
    {
        $packageUrl = esc_url_raw((string) ($request->get_param('package_url') ?? ''));
        if ($packageUrl !== '') {
            return $this->downloadPackage($packageUrl, $packageType);
        }

        $zipBase64 = (string) $request->get_param('zip_base64');
        $zipBytes = base64_decode($zipBase64, true);
        if (! is_string($zipBytes) || ! $this->isValidZipPackage($zipBytes)) {
            return new WP_Error('h4bs_bridge_bad_zip', __('Expected a base64 encoded zip package or an approved package_url.', 'help4-builder-suite'), ['status' => 400]);
        }

        if (strlen($zipBytes) > self::MAX_PACKAGE_BYTES) {
            return new WP_Error('h4bs_bridge_zip_too_large', __('Package is larger than the release bridge allows.', 'help4-builder-suite'), ['status' => 413]);
        }

        return [
            'bytes' => $zipBytes,
            'source' => 'inline_base64',
        ];
    }

    /**
     * @return array{bytes: string, source: string}|WP_Error
     */
    private function downloadPackage(string $packageUrl, string $packageType = 'builder_suite'): array|WP_Error
    {
        if (! $this->isAllowedPackageUrl($packageUrl, $packageType)) {
            return new WP_Error('h4bs_bridge_package_url_not_allowed', __('Package URL must point to an approved Help4 release host and ZIP path.', 'help4-builder-suite'), ['status' => 400]);
        }

        $localPackage = $this->localPackageFromUrl($packageUrl, $packageType);
        if (is_array($localPackage)) {
            return $localPackage;
        }

        $response = wp_remote_get(
            $packageUrl,
            [
                'timeout' => 45,
                'redirection' => 2,
                'limit_response_size' => self::MAX_PACKAGE_BYTES + 1,
                'headers' => [
                    'Accept' => 'application/zip,application/octet-stream,*/*;q=0.8',
                ],
            ]
        );

        if (is_wp_error($response)) {
            return new WP_Error('h4bs_bridge_package_download_failed', $response->get_error_message(), ['status' => 502]);
        }

        $code = (int) wp_remote_retrieve_response_code($response);
        if ($code < 200 || $code >= 300) {
            return new WP_Error('h4bs_bridge_package_download_status', sprintf('Package download returned HTTP %d.', $code), ['status' => 502]);
        }

        $body = (string) wp_remote_retrieve_body($response);
        if (! $this->isValidZipPackage($body)) {
            return new WP_Error('h4bs_bridge_package_download_bad_zip', __('Downloaded package was not a valid ZIP payload.', 'help4-builder-suite'), ['status' => 400]);
        }

        if (strlen($body) > self::MAX_PACKAGE_BYTES) {
            return new WP_Error('h4bs_bridge_package_download_too_large', __('Downloaded package is larger than the release bridge allows.', 'help4-builder-suite'), ['status' => 413]);
        }

        return [
            'bytes' => $body,
            'source' => $packageUrl,
        ];
    }

    /**
     * @return array{bytes: string, source: string}|null
     */
    private function localPackageFromUrl(string $packageUrl, string $packageType = 'builder_suite'): ?array
    {
        $parts = wp_parse_url($packageUrl);
        $host = strtolower((string) ($parts['host'] ?? ''));
        $homeHost = strtolower((string) wp_parse_url(home_url('/'), PHP_URL_HOST));
        $path = (string) ($parts['path'] ?? '');

        if ($host === '' || $homeHost === '' || $host !== $homeHost) {
            return null;
        }

        $relativePath = ltrim(rawurldecode($path), '/');
        $releaseDirectory = $packageType === 'blank_theme' ? 'help4-blank' : 'help4-builder-suite';
        $versionedPattern = $packageType === 'blank_theme'
            ? '#/(?:help4-blank-\d+\.\d+\.\d+)\.zip$#i'
            : '#/(?:help4-builder-suite-\d+\.\d+\.\d+)\.zip$#i';

        if (str_starts_with($relativePath, $releaseDirectory . '/')) {
            return $this->localPackageFromPath(
                trailingslashit(ABSPATH) . $relativePath,
                trailingslashit(ABSPATH) . $releaseDirectory,
                $packageUrl
            );
        }

        $upload = wp_get_upload_dir();
        $uploadBasePath = ltrim((string) wp_parse_url((string) ($upload['baseurl'] ?? ''), PHP_URL_PATH), '/');
        $uploadBasePath = $uploadBasePath !== '' ? trailingslashit($uploadBasePath) : 'wp-content/uploads/';
        if (
            str_starts_with($relativePath, $uploadBasePath)
            && preg_match($versionedPattern, '/' . $relativePath)
        ) {
            $uploadRelativePath = substr($relativePath, strlen($uploadBasePath));
            return $this->localPackageFromPath(
                trailingslashit((string) ($upload['basedir'] ?? '')) . $uploadRelativePath,
                (string) ($upload['basedir'] ?? ''),
                $packageUrl
            );
        }

        return null;
    }

    private function isAllowedPackageUrl(string $packageUrl, string $packageType = 'builder_suite'): bool
    {
        $parts = wp_parse_url($packageUrl);
        $scheme = strtolower((string) ($parts['scheme'] ?? ''));
        $host = strtolower((string) ($parts['host'] ?? ''));
        $path = (string) ($parts['path'] ?? '');

        if ($scheme !== 'https' || ! in_array($host, $this->packageHosts(), true)) {
            return false;
        }

        $releasePattern = $packageType === 'blank_theme'
            ? '#/(?:help4-beta/)?help4-blank/(?:latest|help4-blank(?:-\d+\.\d+\.\d+)?)\.zip$#i'
            : '#/(?:help4-beta/)?help4-builder-suite/(?:latest|help4-builder-suite(?:-\d+\.\d+\.\d+)?)\.zip$#i';
        $uploadFilePattern = $packageType === 'blank_theme'
            ? 'help4-blank-\d+\.\d+\.\d+\.zip'
            : 'help4-builder-suite-\d+\.\d+\.\d+\.zip';

        if (preg_match($releasePattern, $path)) {
            return true;
        }

        $homeHost = strtolower((string) wp_parse_url(home_url('/'), PHP_URL_HOST));
        if ($homeHost === '' || $host !== $homeHost) {
            return false;
        }

        $upload = wp_get_upload_dir();
        $uploadBasePath = (string) wp_parse_url((string) ($upload['baseurl'] ?? ''), PHP_URL_PATH);
        $uploadBasePath = $uploadBasePath !== '' ? rtrim($uploadBasePath, '/') : '/wp-content/uploads';

        return (bool) preg_match(
            '#^' . preg_quote($uploadBasePath, '#') . '(?:/\d{4}/\d{2})?/' . $uploadFilePattern . '$#i',
            $path
        );
    }

    /**
     * @return array{bytes: string, source: string}|null
     */
    private function localPackageFromPath(string $file, string $root, string $source): ?array
    {
        $realFile = realpath($file);
        $realRoot = realpath($root);

        if (
            ! is_string($realFile)
            || ! is_string($realRoot)
            || ! str_starts_with($realFile, trailingslashit($realRoot))
            || ! is_readable($realFile)
        ) {
            return null;
        }

        $bytes = (string) file_get_contents($realFile);
        if (! $this->isValidZipPackage($bytes) || strlen($bytes) > self::MAX_PACKAGE_BYTES) {
            return null;
        }

        return [
            'bytes' => $bytes,
            'source' => $source . ' (local file)',
        ];
    }

    private function isValidZipPackage(string $bytes): bool
    {
        return strlen($bytes) > 22 && str_starts_with($bytes, 'PK');
    }

    private function packageContainsBuilderSuite(string $bytes): bool
    {
        if (class_exists('ZipArchive')) {
            $tempFile = $this->createTempFile('h4bs-inspect-');
            if (! is_string($tempFile) || $tempFile === '') {
                return false;
            }

            file_put_contents($tempFile, $bytes);
            $zip = new \ZipArchive();
            $opened = $zip->open($tempFile);
            $contains = $opened === true && $zip->locateName('help4-builder-suite/help4-builder-suite.php') !== false;
            if ($opened === true) {
                $zip->close();
            }
            wp_delete_file($tempFile);
            return $contains;
        }

        return str_contains($bytes, 'help4-builder-suite/help4-builder-suite.php')
            || str_contains($bytes, 'Plugin Name: Help4 Builder Suite');
    }

    /**
     * @return array<string, string>
     */
    private function tutorialMediaExpectedFiles(): array
    {
        $catalogPath = defined('H4BS_PATH') ? H4BS_PATH . 'includes/Modules/Demos/tutorial-video-catalog.php' : '';
        $catalog = $catalogPath !== '' && is_readable($catalogPath) ? require $catalogPath : [];
        if (! is_array($catalog)) {
            return [];
        }

        $files = [];
        foreach ($catalog as $lesson) {
            if (! is_array($lesson)) {
                return [];
            }
            $filename = sanitize_file_name((string) ($lesson['filename'] ?? ''));
            $hashes = is_array($lesson['sha256'] ?? null) ? $lesson['sha256'] : [];
            $sha256 = strtolower(sanitize_text_field((string) ($hashes['video'] ?? '')));
            if (
                ! preg_match('/^[0-9]{2}-[a-z0-9-]+\.mp4$/', $filename)
                || ! preg_match('/^[a-f0-9]{64}$/', $sha256)
                || isset($files[$filename])
            ) {
                return [];
            }
            $files[$filename] = $sha256;
        }

        ksort($files);

        return $files !== [] && count($files) === count($catalog) ? $files : [];
    }

    private function packageContainsTutorialMedia(string $bytes): bool
    {
        $expectedFiles = $this->tutorialMediaExpectedFiles();
        if ($expectedFiles === [] || ! class_exists('ZipArchive')) {
            return false;
        }

        $tempFile = $this->createTempFile('h4bs-tutorial-media-inspect-');
        if (! is_string($tempFile) || $tempFile === '') {
            return false;
        }
        $written = file_put_contents($tempFile, $bytes, LOCK_EX);
        if (! is_int($written) || $written !== strlen($bytes)) {
            wp_delete_file($tempFile);
            return false;
        }

        $zip = new \ZipArchive();
        $opened = $zip->open($tempFile);
        $entryCount = $opened === true ? $zip->numFiles : 0;
        $valid = $opened === true && $entryCount > 0 && $entryCount <= count($expectedFiles);
        $found = [];
        if ($valid) {
            for ($index = 0; $index < $zip->numFiles; ++$index) {
                $stat = $zip->statIndex($index);
                $filename = is_array($stat) ? sanitize_file_name((string) ($stat['name'] ?? '')) : '';
                $rawName = is_array($stat) ? (string) ($stat['name'] ?? '') : '';
                $size = is_array($stat) ? absint($stat['size'] ?? 0) : 0;
                if (
                    $filename === ''
                    || $filename !== $rawName
                    || ! isset($expectedFiles[$filename])
                    || isset($found[$filename])
                    || $size < 1000
                    || $size > 5 * MB_IN_BYTES
                ) {
                    $valid = false;
                    break;
                }
                $contents = $zip->getFromIndex($index);
                if (! is_string($contents) || hash('sha256', $contents) !== $expectedFiles[$filename]) {
                    $valid = false;
                    break;
                }
                $found[$filename] = true;
            }
        }
        if ($opened === true) {
            $zip->close();
        }
        wp_delete_file($tempFile);

        return $valid && count($found) === $entryCount;
    }

    private function tutorialMediaAccessRules(string $siteHost): string
    {
        $corsRule = $siteHost === 'md.help4wordpress.com'
            ? "    Header always set Access-Control-Allow-Origin \"*\"\n"
            : '';

        return "Options -Indexes\n\n"
            . "<IfModule mod_mime.c>\n    AddType video/mp4 .mp4\n</IfModule>\n\n"
            . "<IfModule mod_headers.c>\n"
            . $corsRule
            . "    Header always set Cross-Origin-Resource-Policy \"cross-origin\"\n"
            . "    Header always set Cache-Control \"public, max-age=31536000, immutable\"\n"
            . "</IfModule>\n";
    }

    /**
     * @return array<int, string>
     */
    private function packageHosts(): array
    {
        $filtered = apply_filters('h4bs_release_bridge_package_hosts', self::PACKAGE_HOSTS);
        $hosts = is_array($filtered) ? $filtered : self::PACKAGE_HOSTS;

        return array_values(
            array_unique(
                array_filter(
                    array_map(
                        static function ($host): string {
                            $host = strtolower(trim(sanitize_text_field((string) $host)));
                            return preg_match('/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\.(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?))*$/', $host)
                                ? $host
                                : '';
                        },
                        $hosts
                    )
                )
            )
        );
    }

    private function packageBuilderSuiteVersion(string $bytes): string
    {
        if (! class_exists('ZipArchive')) {
            return '';
        }

        $tempFile = $this->createTempFile('h4bs-version-inspect-');
        if (! is_string($tempFile) || $tempFile === '') {
            return '';
        }

        $bytesWritten = file_put_contents($tempFile, $bytes);
        if (! is_int($bytesWritten) || $bytesWritten !== strlen($bytes)) {
            wp_delete_file($tempFile);
            return '';
        }

        $zip = new \ZipArchive();
        $opened = $zip->open($tempFile);
        $pluginHeader = $opened === true
            ? $zip->getFromName('help4-builder-suite/help4-builder-suite.php')
            : false;
        if ($opened === true) {
            $zip->close();
        }
        wp_delete_file($tempFile);

        if (! is_string($pluginHeader) || ! preg_match('/^[ \t\/*#@]*Version:\s*([^\r\n]+)/mi', $pluginHeader, $matches)) {
            return '';
        }

        $version = sanitize_text_field(trim((string) ($matches[1] ?? '')));
        return preg_match('/^\d+\.\d+\.\d+$/', $version) ? $version : '';
    }

    /**
     * @return array{writable:bool,checked:int,blocked:array<int,string>,truncated:bool}
     */
    private function builderSuiteWritablePreflight(string $pluginRoot = ''): array
    {
        $pluginRoot = $pluginRoot !== '' ? $pluginRoot : trailingslashit(WP_PLUGIN_DIR) . 'help4-builder-suite';
        $paths = [WP_PLUGIN_DIR, $pluginRoot];
        $scanFailed = false;

        if (is_dir($pluginRoot)) {
            try {
                $iterator = new \RecursiveIteratorIterator(
                    new \RecursiveDirectoryIterator($pluginRoot, \FilesystemIterator::SKIP_DOTS),
                    \RecursiveIteratorIterator::SELF_FIRST,
                    \RecursiveIteratorIterator::CATCH_GET_CHILD
                );
                foreach ($iterator as $entry) {
                    if ($entry instanceof \SplFileInfo) {
                        $paths[] = $entry->getPathname();
                    }
                }
            } catch (\UnexpectedValueException) {
                $scanFailed = true;
            }
        }

        $paths = array_values(array_unique(array_filter($paths, 'is_string')));
        $blocked = [];
        $blockedCount = 0;
        foreach ($paths as $path) {
            if (wp_is_writable($path)) {
                continue;
            }

            ++$blockedCount;
            if (count($blocked) < 25) {
                $blocked[] = $this->relativePluginPath($path);
            }
        }

        if ($scanFailed) {
            ++$blockedCount;
            if (count($blocked) < 25) {
                $blocked[] = 'help4-builder-suite/[unreadable tree]';
            }
        }

        return [
            'writable' => $blockedCount === 0,
            'checked' => count($paths),
            'blocked' => $blocked,
            'truncated' => $blockedCount > count($blocked),
        ];
    }

    private function relativePluginPath(string $path): string
    {
        $pluginsRoot = wp_normalize_path(trailingslashit(WP_PLUGIN_DIR));
        $normalizedPath = wp_normalize_path($path);
        if (str_starts_with($normalizedPath, $pluginsRoot)) {
            return ltrim(substr($normalizedPath, strlen($pluginsRoot)), '/');
        }

        return basename($normalizedPath);
    }

    private function packageContainsBlankTheme(string $bytes): bool
    {
        if (class_exists('ZipArchive')) {
            $tempFile = $this->createTempFile('h4bs-theme-inspect-');
            if (! is_string($tempFile) || $tempFile === '') {
                return false;
            }

            file_put_contents($tempFile, $bytes);
            $zip = new \ZipArchive();
            $opened = $zip->open($tempFile);
            $styleIndex = $opened === true ? $zip->locateName('help4-blank/style.css') : false;
            $style = $opened === true && $styleIndex !== false ? (string) $zip->getFromIndex((int) $styleIndex) : '';
            $contains = $opened === true
                && $styleIndex !== false
                && str_contains($style, 'Theme Name: Help4 Blank');
            if ($opened === true) {
                $zip->close();
            }
            wp_delete_file($tempFile);
            return $contains;
        }

        return str_contains($bytes, 'help4-blank/style.css')
            && str_contains($bytes, 'Theme Name: Help4 Blank');
    }

    private function packageBlankThemeVersion(string $bytes): string
    {
        if (! class_exists('ZipArchive')) {
            return '';
        }

        $tempFile = $this->createTempFile('h4bs-theme-version-inspect-');
        if (! is_string($tempFile) || $tempFile === '') {
            return '';
        }

        $bytesWritten = file_put_contents($tempFile, $bytes);
        if (! is_int($bytesWritten) || $bytesWritten !== strlen($bytes)) {
            wp_delete_file($tempFile);
            return '';
        }

        $zip = new \ZipArchive();
        $opened = $zip->open($tempFile);
        $themeHeader = $opened === true
            ? $zip->getFromName('help4-blank/style.css')
            : false;
        if ($opened === true) {
            $zip->close();
        }
        wp_delete_file($tempFile);

        if (! is_string($themeHeader) || ! preg_match('/^[ \t\/*#@]*Version:\s*([^\r\n]+)/mi', $themeHeader, $matches)) {
            return '';
        }

        $version = sanitize_text_field(trim((string) ($matches[1] ?? '')));
        return preg_match('/^\d+\.\d+\.\d+$/', $version) ? $version : '';
    }

    /**
     * @return array{writable:bool,checked:int,blocked:array<int,string>,truncated:bool}
     */
    private function blankThemeWritablePreflight(): array
    {
        $themesRoot = get_theme_root();
        $themeRoot = trailingslashit($themesRoot) . 'help4-blank';
        $paths = [$themesRoot];
        $scanFailed = false;

        if (is_dir($themeRoot)) {
            $paths[] = $themeRoot;
            try {
                $iterator = new \RecursiveIteratorIterator(
                    new \RecursiveDirectoryIterator($themeRoot, \FilesystemIterator::SKIP_DOTS),
                    \RecursiveIteratorIterator::SELF_FIRST,
                    \RecursiveIteratorIterator::CATCH_GET_CHILD
                );
                foreach ($iterator as $entry) {
                    if ($entry instanceof \SplFileInfo) {
                        $paths[] = $entry->getPathname();
                    }
                }
            } catch (\UnexpectedValueException) {
                $scanFailed = true;
            }
        }

        $paths = array_values(array_unique(array_filter($paths, 'is_string')));
        $blocked = [];
        $blockedCount = 0;
        foreach ($paths as $path) {
            if (wp_is_writable($path)) {
                continue;
            }

            ++$blockedCount;
            if (count($blocked) < 25) {
                $blocked[] = $this->relativeThemePath($path);
            }
        }

        if ($scanFailed) {
            ++$blockedCount;
            if (count($blocked) < 25) {
                $blocked[] = 'help4-blank/[unreadable tree]';
            }
        }

        return [
            'writable'  => $blockedCount === 0,
            'checked'   => count($paths),
            'blocked'   => $blocked,
            'truncated' => $blockedCount > count($blocked),
        ];
    }

    private function relativeThemePath(string $path): string
    {
        $themesRoot = wp_normalize_path(trailingslashit(get_theme_root()));
        $normalizedPath = wp_normalize_path($path);
        if (str_starts_with($normalizedPath, $themesRoot)) {
            return ltrim(substr($normalizedPath, strlen($themesRoot)), '/');
        }

        return basename($normalizedPath);
    }

    private function createTempFile(string $prefix): string
    {
        $this->loadUpgradeIncludes();

        if (function_exists('wp_tempnam')) {
            $tempFile = \wp_tempnam($prefix);
            if (is_string($tempFile) && $tempFile !== '') {
                return $tempFile;
            }
        }

        $tempDir = function_exists('get_temp_dir') ? \get_temp_dir() : sys_get_temp_dir();
        $tempFile = tempnam($tempDir, $prefix);
        return is_string($tempFile) ? $tempFile : '';
    }

    private function runPluginUpgrade(string $packagePath): array|WP_Error
    {
        $this->loadUpgradeIncludes();

        if (! class_exists('Plugin_Upgrader') || ! class_exists('WP_Upgrader_Skin')) {
            return new WP_Error('h4bs_bridge_upgrade_unavailable', __('WordPress plugin upgrader classes are unavailable.', 'help4-builder-suite'), ['status' => 500]);
        }

        $skin = class_exists('Automatic_Upgrader_Skin')
            ? new \Automatic_Upgrader_Skin()
            : new \WP_Upgrader_Skin([
                'title' => __('Help4 Builder Suite Release Bridge Upgrade', 'help4-builder-suite'),
            ]);
        $upgrader = new \Plugin_Upgrader($skin);
        ob_start();
        try {
            $result = $upgrader->install(
                $packagePath,
                [
                    'overwrite_package' => 'update-plugin',
                    'clear_update_cache' => true,
                ]
            );
        } finally {
            $installerOutput = trim(wp_strip_all_tags((string) ob_get_clean()));
        }

        if (is_wp_error($result)) {
            return $result;
        }

        if ($result === false) {
            $messages = method_exists($skin, 'get_errors') && is_array($skin->get_errors()) ? $skin->get_errors() : [];
            if ($installerOutput !== '') {
                $messages[] = $installerOutput;
            }
            return new WP_Error('h4bs_bridge_upgrade_failed', implode(' ', array_map('wp_strip_all_tags', $messages)) ?: __('Plugin upgrader did not complete.', 'help4-builder-suite'), ['status' => 500]);
        }

        if (function_exists('wp_clean_plugins_cache')) {
            wp_clean_plugins_cache(true);
        }

        return [
            'destination' => is_string($upgrader->result['destination'] ?? null) ? $upgrader->result['destination'] : '',
            'destination_name' => is_string($upgrader->result['destination_name'] ?? null) ? $upgrader->result['destination_name'] : '',
            'source' => is_string($upgrader->result['source'] ?? null) ? $upgrader->result['source'] : '',
            'installer_output' => $installerOutput,
        ];
    }

    private function runBlankThemeUpgrade(string $packagePath): array|WP_Error
    {
        $this->loadUpgradeIncludes();

        if (! class_exists('Theme_Upgrader') || ! class_exists('WP_Upgrader_Skin')) {
            return new WP_Error('h4bs_bridge_theme_upgrade_unavailable', __('WordPress theme upgrader classes are unavailable.', 'help4-builder-suite'), ['status' => 500]);
        }

        $skin = class_exists('Automatic_Upgrader_Skin')
            ? new \Automatic_Upgrader_Skin()
            : new \WP_Upgrader_Skin([
                'title' => __('Help4 Blank Release Bridge Upgrade', 'help4-builder-suite'),
            ]);
        $upgrader = new \Theme_Upgrader($skin);
        ob_start();
        try {
            $result = $upgrader->install(
                $packagePath,
                [
                    'overwrite_package' => true,
                    'clear_update_cache' => true,
                ]
            );
        } finally {
            $installerOutput = trim(wp_strip_all_tags((string) ob_get_clean()));
        }

        if (is_wp_error($result)) {
            return $result;
        }
        if ($result === false) {
            $messages = method_exists($skin, 'get_errors') && is_array($skin->get_errors()) ? $skin->get_errors() : [];
            if ($installerOutput !== '') {
                $messages[] = $installerOutput;
            }
            return new WP_Error('h4bs_bridge_theme_upgrade_failed', implode(' ', array_map('wp_strip_all_tags', $messages)) ?: __('Theme upgrader did not complete.', 'help4-builder-suite'), ['status' => 500]);
        }

        wp_clean_themes_cache(true);
        return [
            'destination'      => is_string($upgrader->result['destination'] ?? null) ? $upgrader->result['destination'] : '',
            'destination_name' => is_string($upgrader->result['destination_name'] ?? null) ? $upgrader->result['destination_name'] : '',
            'source'           => is_string($upgrader->result['source'] ?? null) ? $upgrader->result['source'] : '',
            'installer_output' => $installerOutput,
        ];
    }

    private function runBlankThemeAtomicSwap(string $packagePath, string $targetVersion): array|WP_Error
    {
        $this->loadUpgradeIncludes();
        if (! function_exists('WP_Filesystem') || ! WP_Filesystem()) {
            return new WP_Error('h4bs_bridge_theme_filesystem_unavailable', __('WordPress could not initialize its filesystem API.', 'help4-builder-suite'), ['status' => 500]);
        }

        global $wp_filesystem;
        if (! is_object($wp_filesystem) || ! method_exists($wp_filesystem, 'move') || ! method_exists($wp_filesystem, 'delete')) {
            return new WP_Error('h4bs_bridge_theme_filesystem_invalid', __('WordPress did not provide the required filesystem operations.', 'help4-builder-suite'), ['status' => 500]);
        }

        $themesRoot = untrailingslashit(get_theme_root());
        $themeRoot = trailingslashit($themesRoot) . 'help4-blank';
        $stageRoot = trailingslashit($themesRoot) . '.h4bs-help4-blank-stage-' . strtolower(wp_generate_password(8, false, false));
        $stageTheme = trailingslashit($stageRoot) . 'help4-blank';
        $rollbackRoot = trailingslashit($themesRoot) . '.help4-blank-release-rollback';
        $rollbackDeleteMode = defined('FS_CHMOD_DIR') ? FS_CHMOD_DIR : 0755;

        if (is_dir($rollbackRoot)) {
            $wp_filesystem->chmod($rollbackRoot, $rollbackDeleteMode, true);
            if (! $wp_filesystem->delete($rollbackRoot, true)) {
                return new WP_Error('h4bs_bridge_theme_rollback_exists', __('The prior Help4 Blank rollback directory could not be cleared.', 'help4-builder-suite'), ['status' => 500]);
            }
        }
        if (! wp_mkdir_p($stageRoot)) {
            return new WP_Error('h4bs_bridge_theme_stage_failed', __('Could not create a temporary theme staging directory.', 'help4-builder-suite'), ['status' => 500]);
        }

        $unpacked = unzip_file($packagePath, $stageRoot);
        if (is_wp_error($unpacked)) {
            $wp_filesystem->delete($stageRoot, true);
            return $unpacked;
        }

        $stylePath = trailingslashit($stageTheme) . 'style.css';
        $style = is_readable($stylePath) ? (string) file_get_contents($stylePath) : '';
        if (
            $style === ''
            || ! preg_match('/^[ \t\/*#@]*Version:\s*([^\r\n]+)/mi', $style, $matches)
            || trim((string) ($matches[1] ?? '')) !== $targetVersion
        ) {
            $wp_filesystem->delete($stageRoot, true);
            return new WP_Error('h4bs_bridge_theme_stage_identity', __('The staged Help4 Blank theme did not preserve the requested version.', 'help4-builder-suite'), ['status' => 500]);
        }

        if (is_dir($themeRoot) && ! $wp_filesystem->move($themeRoot, $rollbackRoot, false)) {
            $wp_filesystem->delete($stageRoot, true);
            return new WP_Error('h4bs_bridge_theme_swap_backup', __('Could not move the installed Help4 Blank theme into the bounded rollback directory.', 'help4-builder-suite'), ['status' => 500]);
        }

        if (! $wp_filesystem->move($stageTheme, $themeRoot, false)) {
            if (is_dir($rollbackRoot)) {
                $wp_filesystem->move($rollbackRoot, $themeRoot, false);
            }
            $wp_filesystem->delete($stageRoot, true);
            return new WP_Error('h4bs_bridge_theme_swap_install', __('Could not move the staged Help4 Blank theme into place; the prior tree was restored when possible.', 'help4-builder-suite'), ['status' => 500]);
        }

        $wp_filesystem->delete($stageRoot, true);
        if (is_dir($rollbackRoot)) {
            $wp_filesystem->chmod($rollbackRoot, $rollbackDeleteMode, true);
        }
        $rollbackRemoved = ! is_dir($rollbackRoot) || $wp_filesystem->delete($rollbackRoot, true);
        return [
            'destination'      => $themeRoot,
            'destination_name' => 'help4-blank',
            'source'           => $stageTheme,
            'installer_output' => '',
            'strategy'         => 'atomic_directory_swap',
            'rollback_removed' => $rollbackRemoved,
            'rollback_retained' => ! $rollbackRemoved,
        ];
    }

    private function loadUpgradeIncludes(): void
    {
        foreach (
            [
                ABSPATH . 'wp-admin/includes/file.php',
                ABSPATH . 'wp-admin/includes/plugin.php',
                ABSPATH . 'wp-admin/includes/class-wp-upgrader.php',
                ABSPATH . 'wp-admin/includes/class-wp-upgrader-skin.php',
                ABSPATH . 'wp-admin/includes/class-plugin-upgrader.php',
                ABSPATH . 'wp-admin/includes/class-theme-upgrader.php',
            ] as $include
        ) {
            if (is_readable($include)) {
                require_once $include;
            }
        }
    }

    private function isBuilderSuiteActive(): bool
    {
        if (! function_exists('is_plugin_active')) {
            $this->loadUpgradeIncludes();
        }

        return function_exists('is_plugin_active') && is_plugin_active(plugin_basename(H4BS_FILE));
    }

    private function installedBuilderSuiteVersion(): string
    {
        if (! function_exists('get_plugin_data')) {
            $this->loadUpgradeIncludes();
        }

        $pluginPath = WP_PLUGIN_DIR . '/help4-builder-suite/help4-builder-suite.php';
        if (! is_readable($pluginPath) || ! function_exists('get_plugin_data')) {
            return '';
        }

        $data = get_plugin_data($pluginPath, false, false);
        return is_array($data) ? sanitize_text_field((string) ($data['Version'] ?? '')) : '';
    }

    private function installedBlankThemeVersion(): string
    {
        $theme = wp_get_theme('help4-blank');
        if (! $theme->exists()) {
            return '';
        }

        return sanitize_text_field((string) $theme->get('Version'));
    }

    private function updateAllowedMeta(int $postId, array $meta): void
    {
        $allowed = [
            '_h4_seo_title',
            '_h4_seo_description',
            '_h4_seo_canonical',
            '_h4_seo_noindex',
            '_h4_seo_social_title',
            '_h4_seo_social_description',
            '_h4_seo_social_image_url',
            '_h4_seo_social_image_alt',
            '_h4_seo_schema_type',
            '_h4_builder_enabled',
            '_h4_builder_mode',
            '_h4_builder_data',
            '_h4_builder_layout',
            '_h4_builder_page_layout',
        ];

        foreach ($meta as $key => $value) {
            $key = sanitize_key((string) $key);
            if (! in_array($key, $allowed, true)) {
                continue;
            }
            if (is_array($value)) {
                $json = wp_json_encode($value, JSON_UNESCAPED_SLASHES);
                update_post_meta($postId, $key, $key === '_h4_builder_layout' ? wp_slash((string) $json) : $json);
                continue;
            }
            update_post_meta($postId, $key, sanitize_text_field((string) $value));
        }
    }

    /**
     * @param array<int, mixed> $blocks
     * @return array{count:int, appended:int}
     */
    private function appendBuilderLayoutBlocks(int $postId, array $blocks, string $prefix): array
    {
        $raw = (string) get_post_meta($postId, '_h4_builder_layout', true);
        $layout = json_decode($raw, true);
        if (! is_array($layout)) {
            $layout = [];
        }

        $layout = array_values(array_filter($layout, static function ($block) use ($prefix): bool {
            if (! is_array($block)) {
                return false;
            }

            return ! str_starts_with((string) ($block['id'] ?? ''), $prefix);
        }));

        $append = [];
        foreach ($blocks as $block) {
            if (! is_array($block)) {
                continue;
            }

            $id = sanitize_key((string) ($block['id'] ?? ''));
            $type = sanitize_key((string) ($block['type'] ?? ''));
            if ($id === '' || $type === '') {
                continue;
            }

            $append[] = [
                'id'         => $id,
                'type'       => $type,
                'settings'   => is_array($block['settings'] ?? null) ? $block['settings'] : [],
                'custom_css' => is_string($block['custom_css'] ?? null) ? (string) $block['custom_css'] : '',
                'custom_js'  => is_string($block['custom_js'] ?? null) ? (string) $block['custom_js'] : '',
            ];
        }

        if ($append === []) {
            return [
                'count'    => count($layout),
                'appended' => 0,
            ];
        }

        $layout = array_merge($layout, $append);
        update_post_meta($postId, '_h4_builder_layout', wp_slash((string) wp_json_encode($layout, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)));

        return [
            'count'    => count($layout),
            'appended' => count($append),
        ];
    }

    private function findPostIdBySlug(string $slug, string $postType): int
    {
        $post = get_page_by_path($slug, OBJECT, $postType);

        return $post instanceof \WP_Post ? (int) $post->ID : 0;
    }

    private function normalizeManifest(string $manifest): string
    {
        $decoded = json_decode($manifest, true);
        if (! is_array($decoded)) {
            return '';
        }

        $json = wp_json_encode($decoded, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
        if (! is_string($json)) {
            return '';
        }

        $home = home_url('/');
        return str_replace(
            [
                'https://md.help4wordpress.com/',
                'http://md.help4wordpress.com/',
                'https://help4wordpress.com/',
                'http://help4wordpress.com/',
            ],
            [
                $home,
                $home,
                $home,
                $home,
            ],
            $json
        ) . "\n";
    }

    private function syncStaticReleaseFiles(string $releaseDir, string $version): void
    {
        $staticDir = trailingslashit(ABSPATH) . 'help4-builder-suite';
        if (! wp_mkdir_p($staticDir)) {
            return;
        }

        foreach (['latest.zip', 'help4-builder-suite.zip', 'help4-builder-suite-' . $version . '.zip', 'manifest.json', 'changelog.txt'] as $name) {
            $source = trailingslashit($releaseDir) . $name;
            if (is_readable($source)) {
                // phpcs:ignore PluginCheck.CodeAnalysis.WriteFile.ABSPATHDetected -- Mirrors release artifacts to the public static release path.
                copy($source, trailingslashit($staticDir) . $name);
            }
        }
    }

    /**
     * @return array{ok:bool, files:array<string, array<string, mixed>>}
     */
    private function syncStaticThemeReleaseFiles(string $releaseDir, string $version): array
    {
        $staticDir = trailingslashit(ABSPATH) . 'help4-blank';
        if (! wp_mkdir_p($staticDir)) {
            return ['ok' => false, 'files' => ['directory' => ['ok' => false, 'path' => $staticDir]]];
        }

        if (! function_exists('WP_Filesystem')) {
            require_once ABSPATH . 'wp-admin/includes/file.php';
        }
        global $wp_filesystem;
        if (! WP_Filesystem() || ! $wp_filesystem) {
            return ['ok' => false, 'files' => ['filesystem' => ['ok' => false, 'error' => 'filesystem_unavailable']]];
        }

        $results = [];
        foreach (['latest.zip', 'help4-blank.zip', 'help4-blank-' . $version . '.zip', 'manifest.json', 'changelog.txt'] as $name) {
            $source = trailingslashit($releaseDir) . $name;
            $destination = trailingslashit($staticDir) . $name;
            if (! is_readable($source)) {
                $results[$name] = ['ok' => false, 'error' => 'source_not_readable'];
                continue;
            }

            $temporary = $destination . '.tmp-' . wp_generate_uuid4();
            $copied = $wp_filesystem->copy($source, $temporary, true, FS_CHMOD_FILE);
            if (! $copied) {
                $results[$name] = ['ok' => false, 'error' => 'temporary_copy_failed'];
                continue;
            }

            $replaced = $wp_filesystem->move($temporary, $destination, true);
            if (! $replaced) {
                wp_delete_file($temporary);
                $results[$name] = ['ok' => false, 'error' => 'atomic_replace_failed'];
                continue;
            }

            clearstatcache(true, $destination);
            $sourceHash = hash_file('sha256', $source);
            $destinationHash = is_readable($destination) ? hash_file('sha256', $destination) : false;
            $verified = is_string($sourceHash) && $sourceHash !== '' && hash_equals($sourceHash, (string) $destinationHash);
            $results[$name] = [
                'ok' => $verified,
                'bytes' => is_file($destination) ? (int) filesize($destination) : 0,
                'sha256' => is_string($destinationHash) ? $destinationHash : '',
                'error' => $verified ? '' : 'hash_verification_failed',
            ];
        }

        $ok = $results !== [] && ! in_array(false, array_map(static fn(array $result): bool => ! empty($result['ok']), $results), true);
        return ['ok' => $ok, 'files' => $results];
    }

    private function releaseAssetAttachmentId(string $key): int
    {
        $attachmentId = absint(get_option($this->releaseAssetOptionName($key), 0));
        if ($attachmentId > 0 && get_post_type($attachmentId) === 'attachment') {
            return $attachmentId;
        }

        return 0;
    }

    private function rememberReleaseAssetAttachment(string $key, int $attachmentId): void
    {
        if ($attachmentId <= 0) {
            return;
        }

        update_option($this->releaseAssetOptionName($key), $attachmentId, false);
    }

    private function releaseAssetOptionName(string $key): string
    {
        return 'h4bs_release_asset_' . md5($key);
    }

    private function registerReleaseAttachments(string $relativeBase, array $assets, string $version): void
    {
        $map = [
            'latest.zip' => ['help4-builder-suite-latest', 'Latest'],
            'help4-builder-suite.zip' => ['help4-builder-suite-alias', 'Stable Alias'],
            'help4-builder-suite-' . $version . '.zip' => ['help4-builder-suite-version', 'Versioned Package'],
            'manifest.json' => ['help4-builder-suite-manifest', 'Manifest'],
            'changelog.txt' => ['help4-builder-suite-changelog', 'Changelog'],
        ];

        foreach ($map as $file => [$key, $label]) {
            if (empty($assets[$file]['path'])) {
                continue;
            }

            $relative = $relativeBase . '/' . $file;
            $attachment = [
                'post_mime_type' => $this->mimeForFile($file),
                'post_title'     => 'Help4 Builder Suite ' . $version . ' ' . $label,
                'post_content'   => '',
                'post_status'    => 'inherit',
                'guid'           => (string) $assets[$file]['url'],
            ];

            $attachmentId = $this->releaseAssetAttachmentId($key);
            if ($attachmentId > 0) {
                $attachment['ID'] = $attachmentId;
                wp_update_post($attachment);
            } else {
                $attachmentId = (int) wp_insert_attachment($attachment, (string) $assets[$file]['path']);
            }

            update_post_meta($attachmentId, '_wp_attached_file', $relative);
            update_post_meta($attachmentId, '_h4_release_asset_key', $key);
            $this->rememberReleaseAssetAttachment($key, $attachmentId);
        }
    }

    private function registerThemeReleaseAttachments(string $relativeBase, array $assets, string $version): void
    {
        $map = [
            'latest.zip' => ['help4-blank-latest', 'Latest'],
            'help4-blank.zip' => ['help4-blank-alias', 'Stable Alias'],
            'help4-blank-' . $version . '.zip' => ['help4-blank-version', 'Versioned Package'],
            'manifest.json' => ['help4-blank-manifest', 'Manifest'],
            'changelog.txt' => ['help4-blank-changelog', 'Changelog'],
        ];

        foreach ($map as $file => [$key, $label]) {
            if (empty($assets[$file]['path'])) {
                continue;
            }

            $relative = $relativeBase . '/' . $file;
            $attachment = [
                'post_mime_type' => $this->mimeForFile($file),
                'post_title'     => 'Help4 Blank ' . $version . ' ' . $label,
                'post_content'   => '',
                'post_status'    => 'inherit',
                'guid'           => (string) $assets[$file]['url'],
            ];

            $attachmentId = $this->releaseAssetAttachmentId($key);
            if ($attachmentId > 0) {
                $attachment['ID'] = $attachmentId;
                wp_update_post($attachment);
            } else {
                $attachmentId = (int) wp_insert_attachment($attachment, (string) $assets[$file]['path']);
            }

            update_post_meta($attachmentId, '_wp_attached_file', $relative);
            update_post_meta($attachmentId, '_h4_release_asset_key', $key);
            $this->rememberReleaseAssetAttachment($key, $attachmentId);
        }
    }

    private function registerReleaseRedirects(string $version): array
    {
        $latest = '/wp-content/uploads/help4-releases/help4-builder-suite/' . $version . '/latest.zip';
        $alias = '/wp-content/uploads/help4-releases/help4-builder-suite/' . $version . '/help4-builder-suite.zip';
        $versioned = '/wp-content/uploads/help4-releases/help4-builder-suite/' . $version . '/help4-builder-suite-' . $version . '.zip';

        $redirects = [
            '/latest.zip' => $latest,
            '/help4-builder-suite/latest.zip' => $latest,
            '/help4-builder-suite.zip' => $alias,
            '/help4-builder-suite/help4-builder-suite.zip' => $alias,
            '/help4-builder-suite-' . $version . '.zip' => $versioned,
            '/help4-builder-suite/help4-builder-suite-' . $version . '.zip' => $versioned,
            '/help4-builder-suite/manifest.json' => '/wp-content/uploads/help4-releases/help4-builder-suite/' . $version . '/manifest.json',
            '/help4-builder-suite/changelog.txt' => '/wp-content/uploads/help4-releases/help4-builder-suite/' . $version . '/changelog.txt',
        ];

        $result = [];
        foreach ($redirects as $source => $target) {
            $result[$source] = $this->upsertRedirect($source, $target);
        }

        return $result;
    }

    private function registerThemeReleaseRedirects(string $version): array
    {
        $latest = '/wp-content/uploads/help4-releases/help4-blank/' . $version . '/latest.zip';
        $alias = '/wp-content/uploads/help4-releases/help4-blank/' . $version . '/help4-blank.zip';
        $versioned = '/wp-content/uploads/help4-releases/help4-blank/' . $version . '/help4-blank-' . $version . '.zip';

        $redirects = [
            '/help4-blank/latest.zip' => $latest,
            '/help4-blank.zip' => $alias,
            '/help4-blank/help4-blank.zip' => $alias,
            '/help4-blank-' . $version . '.zip' => $versioned,
            '/help4-blank/help4-blank-' . $version . '.zip' => $versioned,
            '/help4-blank/manifest.json' => '/wp-content/uploads/help4-releases/help4-blank/' . $version . '/manifest.json',
            '/help4-blank/changelog.txt' => '/wp-content/uploads/help4-releases/help4-blank/' . $version . '/changelog.txt',
        ];

        $result = [];
        foreach ($redirects as $source => $target) {
            $result[$source] = $this->upsertRedirect($source, $target);
        }

        return $result;
    }

    private function upsertRedirect(string $source, string $target): string
    {
        global $wpdb;

        $table = $wpdb->prefix . 'h4_redirects';
        $cacheGroup = 'h4bs_release_bridge';
        $tableCacheKey = 'table_exists_' . hash('sha256', $table);
        $exists = wp_cache_get($tableCacheKey, $cacheGroup);
        if (! is_string($exists)) {
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Cached schema guard for optional Help4 redirects table.
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Cached schema guard for optional Help4 redirects table.
            $exists = (string) $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $wpdb->esc_like($table)));
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Companion cache write for the guarded schema check above.
            wp_cache_set($tableCacheKey, $exists, $cacheGroup, MINUTE_IN_SECONDS * 10);
            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery
        }
        if ($exists !== $table) {
            return 'skipped_no_table';
        }

        $idCacheKey = 'redirect_id_' . hash('sha256', $table . '|' . $source);
        $cachedId = wp_cache_get($idCacheKey, $cacheGroup);
        if (is_int($cachedId)) {
            $id = $cachedId;
        } else {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Cached lookup against native Help4 redirects table.
            $id = (int) $wpdb->get_var($wpdb->prepare("SELECT id FROM %i WHERE source_path = %s AND match_type = 'exact' LIMIT 1", $table, $source));
            wp_cache_set($idCacheKey, $id, $cacheGroup, MINUTE_IN_SECONDS * 10);
        }
        $data = [
            'source_path' => $source,
            'match_type'  => 'exact',
            'target_url'  => $target,
            'status'      => 'active',
            'http_code'   => 301,
        ];

        if ($id > 0) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Updates native Help4 redirects table for release-path compatibility redirects.
            $wpdb->update($table, $data, ['id' => $id], ['%s', '%s', '%s', '%s', '%d'], ['%d']);
            wp_cache_set($idCacheKey, $id, $cacheGroup, MINUTE_IN_SECONDS * 10);
            return 'updated';
        }

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Inserts native Help4 redirects table rows for release-path compatibility redirects.
        $wpdb->insert($table, $data, ['%s', '%s', '%s', '%s', '%d']);
        if (! empty($wpdb->insert_id)) {
            wp_cache_set($idCacheKey, (int) $wpdb->insert_id, $cacheGroup, MINUTE_IN_SECONDS * 10);
        }
        return 'inserted';
    }

    private function mimeForFile(string $file): string
    {
        return match (strtolower(pathinfo($file, PATHINFO_EXTENSION))) {
            'zip' => 'application/zip',
            'json' => 'application/json',
            'txt' => 'text/plain',
            default => 'application/octet-stream',
        };
    }
}
