<?php

declare(strict_types=1);

namespace WPStack\Sandbox\WordPress;

use DateTimeImmutable;
use DateTimeZone;
use WPStack\Sandbox\Domain\LaunchPolicy;
use WPStack\Sandbox\Domain\PluginCatalog;
use WPStack\Sandbox\Domain\SandboxSlug;
use WPStack\Sandbox\Domain\SessionPolicy;
use WPStack\Sandbox\Infrastructure\SessionRepository;
use WPStack\Sandbox\Infrastructure\SettingsRepository;
use WPStack\Sandbox\Security\TokenService;
use WPStack\Sandbox\Security\VisitorIdentifier;

final class SandboxPlugin
{
    private SessionRepository $sessions;
    private PluginCatalog $catalog;
    private TokenService $tokens;
    private VisitorIdentifier $visitors;
    private SettingsRepository $settings;
    private LicenseManager $licenses;

    public function __construct()
    {
        $secret = wp_salt('auth') . '|' . get_current_network_id();
        $this->sessions = new SessionRepository();
        $this->settings = new SettingsRepository();
        $this->catalog = new PluginCatalog($this->settings->products());
        $this->licenses = new LicenseManager();
        $this->tokens = new TokenService($secret);
        $this->visitors = new VisitorIdentifier($secret);
    }

    public function boot(): void
    {
        add_filter('cron_schedules', [$this, 'cronSchedules']);
        add_action('init', [$this, 'ensureCleanupSchedule']);
        add_action('wpstack_demo_cleanup', [$this, 'cleanup']);
        add_action('wpstack_demo_delete_session', [$this, 'deleteSession']);

        add_shortcode('wpstack_demo_board', [$this, 'renderBoard']);
        add_action('wp_enqueue_scripts', [$this, 'enqueuePublicAssets']);
        add_filter('template_include', [$this, 'publicTemplate'], 99);
        add_action('template_redirect', [$this, 'maybeLogin'], 1);

        add_action('admin_post_nopriv_wpstack_demo_launch', [$this, 'launch']);
        add_action('admin_post_wpstack_demo_launch', [$this, 'launch']);
        add_action('admin_post_nopriv_wpstack_demo_extend', [$this, 'extend']);
        add_action('admin_post_wpstack_demo_extend', [$this, 'extend']);
        add_action('admin_post_nopriv_wpstack_demo_expire', [$this, 'expire']);
        add_action('admin_post_wpstack_demo_expire', [$this, 'expire']);
        add_action('admin_post_wpstack_demo_reset', [$this, 'reset']);
        add_action('admin_post_wpstack_sandbox_save_products', [$this, 'saveProducts']);
        add_action('admin_post_wpstack_sandbox_save_branding', [$this, 'saveBranding']);

        add_action('admin_notices', [$this, 'renderSessionBanner']);
        add_action('admin_footer', [$this, 'renderSessionDock'], 1000);
        add_action('admin_menu', [$this, 'registerDemoCenter']);
        add_action('admin_menu', [$this, 'restrictDemoMenus'], 999);
        add_action('network_admin_menu', [$this, 'registerNetworkPage']);

        add_filter('pre_wp_mail', [$this, 'blockDemoEmail'], 10, 2);
        add_filter('pre_http_request', [$this, 'blockDemoExternalRequests'], 10, 3);
        add_filter('wp_robots', [$this, 'noIndexRobots']);
        add_filter('show_admin_bar', [$this, 'hideFrontendAdminBar']);
    }

    /**
     * @param array<string, array<string, int|string>> $schedules
     * @return array<string, array<string, int|string>>
     */
    public function cronSchedules(array $schedules): array
    {
        $schedules['wpstack_five_minutes'] = [
            'interval' => 300,
            'display' => __('Every five minutes', 'wpstack-sandbox'),
        ];

        return $schedules;
    }

    public function ensureCleanupSchedule(): void
    {
        if (!wp_next_scheduled('wpstack_demo_cleanup')) {
            wp_schedule_event(time() + 300, 'wpstack_five_minutes', 'wpstack_demo_cleanup');
        }
    }

    public function enqueuePublicAssets(): void
    {
        if (is_main_site() && is_front_page()) {
            wp_enqueue_style(
                'wpstack-demo-board',
                WPSTACK_SANDBOX_URL . 'assets/demo.css',
                [],
                WPSTACK_SANDBOX_VERSION
            );
        }
    }

    public function publicTemplate(string $template): string
    {
        if (is_main_site() && is_front_page()) {
            return WPSTACK_SANDBOX_DIR . 'templates/demo-board.php';
        }

        return $template;
    }

    public function renderBoard(): string
    {
        if (!is_main_site()) {
            return '';
        }

        $error = isset($_GET['demo_error'])
            ? sanitize_key(wp_unslash($_GET['demo_error']))
            : '';
        $messages = [
            'invalid_plugin' => 'That demo is not available.',
            'active_session' => 'You already have an active sandbox. Manage it below.',
            'session_expired' => 'Your previous sandbox has expired. You can now test another plugin.',
            'rate_limit' => 'Too many demos were launched recently. Please try again later.',
            'capacity' => 'All demo spaces are currently in use. Please try again shortly.',
            'provision_failed' => 'The sandbox could not be created. Please try again shortly.',
            'invalid_request' => 'The launch request could not be verified.',
        ];

        $activeSession = $this->sessions->findActiveByVisitor($this->visitorHash());

        ob_start();
        ?>
        <main class="wpsd-shell">
            <section class="wpsd-hero">
                <p class="wpsd-eyebrow">WPSTACK LIVE LAB</p>
                <h1>Test every plugin.<br><span>Risk nothing.</span></h1>
                <p class="wpsd-lead">Launch a private WordPress sandbox with realistic sample content. No signup, no installation, and no changes to your website.</p>
                <div class="wpsd-meta">
                    <span>Configurable sessions</span>
                    <span>Private workspace</span>
                    <span>Resets cleanly</span>
                </div>
            </section>
            <?php if ($error && isset($messages[$error])) : ?>
                <div class="wpsd-alert" role="alert"><?php echo esc_html($messages[$error]); ?></div>
            <?php endif; ?>
            <?php if ($activeSession) : ?>
                <?php
                $expires = strtotime((string) $activeSession['expires_at']);
                $created = new DateTimeImmutable((string) $activeSession['created_at'], new DateTimeZone('UTC'));
                $sessionPolicy = $this->policyForSession($activeSession);
                $maximum = $sessionPolicy->capExpiry(
                    $created,
                    $created->modify('+' . (int) $activeSession['maximum_minutes'] . ' minutes')
                )->getTimestamp();
                $canExtend = $expires < $maximum;
                ?>
                <section class="wpsd-active" aria-labelledby="wpsd-active-title">
                    <div>
                        <p class="wpsd-eyebrow">ACTIVE SANDBOX</p>
                        <h2 id="wpsd-active-title"><?php echo esc_html((string) ($this->catalog->get((string) $activeSession['plugin_slug'])['name'] ?? 'Plugin demo')); ?></h2>
                        <p class="wpsd-countdown" data-wpsd-countdown data-expires="<?php echo esc_attr((string) ($expires * 1000)); ?>">Calculating time remaining…</p>
                        <p class="wpsd-limit">
                            <?php
                            if ((bool) $activeSession['extensions_allowed']) {
                                echo esc_html(
                                    sprintf(
                                        __('Extend by %1$d minutes at a time, up to %2$d minutes total.', 'wpstack-sandbox'),
                                        (int) $activeSession['extension_minutes'],
                                        (int) $activeSession['maximum_minutes']
                                    )
                                );
                            } else {
                                esc_html_e('Extensions are disabled for this product.', 'wpstack-sandbox');
                            }
                            ?>
                        </p>
                    </div>
                    <div class="wpsd-active__actions">
                        <?php if (
                            is_user_logged_in()
                            && get_current_user_id() === (int) $activeSession['user_id']
                        ) : ?>
                            <a class="wpsd-button" href="<?php echo esc_url(get_site_url((int) $activeSession['blog_id'], '/wp-admin/', 'https')); ?>">Return to sandbox</a>
                        <?php endif; ?>
                        <?php if ($canExtend && (bool) $activeSession['extensions_allowed']) : ?>
                            <a class="wpsd-button wpsd-button--light" href="<?php echo esc_url(wp_nonce_url(admin_url('admin-post.php?action=wpstack_demo_extend'), 'wpstack_demo_extend')); ?>">
                                <?php echo esc_html(sprintf(__('Extend +%d minutes', 'wpstack-sandbox'), (int) $activeSession['extension_minutes'])); ?>
                            </a>
                        <?php elseif ((bool) $activeSession['extensions_allowed']) : ?>
                            <span class="wpsd-limit-reached"><?php echo esc_html(sprintf(__('%d-minute limit reached', 'wpstack-sandbox'), (int) $activeSession['maximum_minutes'])); ?></span>
                        <?php endif; ?>
                        <a class="wpsd-button wpsd-button--danger" href="<?php echo esc_url(wp_nonce_url(admin_url('admin-post.php?action=wpstack_demo_expire'), 'wpstack_demo_expire')); ?>" onclick="return confirm('Expire this sandbox now? Its data will be permanently deleted.')">Expire now &amp; test another</a>
                    </div>
                </section>
                <?php $this->renderCountdownScript(); ?>
            <?php endif; ?>
            <section class="wpsd-grid" aria-label="<?php esc_attr_e('Available plugin demos', 'wpstack-sandbox'); ?>">
                <?php foreach ($this->catalog->all() as $slug => $plugin) : ?>
                    <article class="wpsd-card" style="--accent: <?php echo esc_attr($plugin['accent']); ?>">
                        <div class="wpsd-mark"><?php echo esc_html(strtoupper(substr($plugin['name'], 0, 1))); ?></div>
                        <div>
                            <h2><?php echo esc_html($plugin['name']); ?></h2>
                            <p><?php echo esc_html($plugin['description']); ?></p>
                        </div>
                        <form action="<?php echo esc_url(admin_url('admin-post.php')); ?>" method="post">
                            <input type="hidden" name="action" value="wpstack_demo_launch">
                            <input type="hidden" name="plugin" value="<?php echo esc_attr($slug); ?>">
                            <?php wp_nonce_field('wpstack_demo_launch', 'wpstack_demo_nonce'); ?>
                            <button type="submit">Launch live demo <span aria-hidden="true">↗</span></button>
                        </form>
                    </article>
                <?php endforeach; ?>
                <?php if ($this->catalog->all() === []) : ?>
                    <article class="wpsd-card">
                        <div>
                            <h2><?php esc_html_e('No demos configured yet', 'wpstack-sandbox'); ?></h2>
                            <p><?php esc_html_e('A Network Administrator can add the first demo product from WPStack Sandbox Manager.', 'wpstack-sandbox'); ?></p>
                        </div>
                    </article>
                <?php endif; ?>
            </section>
            <p class="wpsd-footnote">Sandboxes are automatically and permanently deleted after expiry.</p>
        </main>
        <?php
        return (string) ob_get_clean();
    }

    public function launch(): void
    {
        if (
            !isset($_POST['wpstack_demo_nonce'])
            || !wp_verify_nonce(
                sanitize_text_field(wp_unslash($_POST['wpstack_demo_nonce'])),
                'wpstack_demo_launch'
            )
        ) {
            $this->redirectBoard('invalid_request');
        }

        $slug = isset($_POST['plugin']) ? sanitize_key(wp_unslash($_POST['plugin'])) : '';
        if (!$this->catalog->has($slug)) {
            $this->redirectBoard('invalid_plugin');
        }

        $visitorHash = $this->visitorHash();
        $policy = new LaunchPolicy();
        $activeCount = $this->sessions->activeCount($visitorHash);
        if ($activeCount > 0) {
            $existing = $this->sessions->findActiveByVisitor($visitorHash);
            if (
                $existing
                && is_user_logged_in()
                && get_current_user_id() === (int) $existing['user_id']
            ) {
                $existingPlugin = $this->catalog->get((string) $existing['plugin_slug']);
                $landing = $existingPlugin
                    ? $existingPlugin['landing']
                    : '/wp-admin/admin.php?page=wpstack-demo-center';
                wp_safe_redirect(
                    get_site_url((int) $existing['blog_id'], $landing, 'https')
                );
                exit;
            }
        }

        $reason = $policy->denialReason(
            $activeCount,
            $this->sessions->recentLaunchCount($visitorHash),
            $this->sessions->globalActiveCount()
        );
        if ($reason !== null) {
            $this->redirectBoard($reason);
        }

        try {
            $result = $this->provision($slug, $visitorHash);
            wp_safe_redirect($result['login_url']);
            exit;
        } catch (\Throwable $error) {
            error_log('WPStack sandbox provisioning failed: ' . $error->getMessage());
            $this->redirectBoard('provision_failed');
        }
    }

    public function maybeLogin(): void
    {
        if (!is_main_site() || empty($_GET['wpstack_demo_login'])) {
            return;
        }

        $raw = sanitize_text_field(wp_unslash($_GET['wpstack_demo_login']));
        $session = $this->sessions->findByLoginHash($this->tokens->hash($raw));
        if (
            !$session
            || $session['status'] !== 'active'
            || (int) $session['login_used'] !== 0
            || strtotime((string) $session['expires_at']) <= time()
            || !$this->sessions->consumeLogin((string) $session['session_id'])
        ) {
            wp_die(
                esc_html__('This demo login has expired or was already used.', 'wpstack-sandbox'),
                esc_html__('Demo unavailable', 'wpstack-sandbox'),
                ['response' => 410]
            );
        }

        $userId = (int) $session['user_id'];
        wp_set_current_user($userId);
        wp_set_auth_cookie($userId, false, is_ssl());

        $plugin = $this->catalog->get((string) $session['plugin_slug']);
        $landing = $plugin ? $plugin['landing'] : '/wp-admin/admin.php?page=wpstack-demo-center';
        wp_safe_redirect(get_site_url((int) $session['blog_id'], $landing, 'https'));
        exit;
    }

    public function registerDemoCenter(): void
    {
        if (!$this->isDemoSite()) {
            return;
        }

        add_menu_page(
            __('WPStack Demo Center', 'wpstack-sandbox'),
            __('Demo Center', 'wpstack-sandbox'),
            'read',
            'wpstack-demo-center',
            [$this, 'renderDemoCenter'],
            'dashicons-superhero',
            2
        );
    }

    public function renderDemoCenter(): void
    {
        $session = $this->currentSession();
        ?>
        <div class="wrap">
            <h1><?php esc_html_e('Product Demo', 'wpstack-sandbox'); ?></h1>
            <p><?php esc_html_e('This is a private, disposable WordPress site prepared for product testing.', 'wpstack-sandbox'); ?></p>
            <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:16px;margin-top:24px">
                <?php
                $selected = $session
                    ? $this->catalog->get((string) $session['plugin_slug'])
                    : null;
                foreach ($selected ? [$selected] : [] as $plugin) :
                ?>
                    <div class="card" style="max-width:none;border-left:4px solid <?php echo esc_attr($plugin['accent']); ?>">
                        <h2><?php echo esc_html($plugin['name']); ?></h2>
                        <p><?php echo esc_html($plugin['description']); ?></p>
                        <a class="button button-primary" href="<?php echo esc_url(admin_url(substr($plugin['landing'], strlen('/wp-admin/')))); ?>">
                            <?php esc_html_e('Open plugin', 'wpstack-sandbox'); ?>
                        </a>
                    </div>
                <?php endforeach; ?>
            </div>
            <?php if ($session) : ?>
                <p style="margin-top:24px"><strong><?php esc_html_e('Expires:', 'wpstack-sandbox'); ?></strong> <?php echo esc_html(get_date_from_gmt((string) $session['expires_at'], 'M j, Y g:i a')); ?></p>
            <?php endif; ?>
        </div>
        <?php
    }

    public function renderSessionBanner(): void
    {
        $session = $this->currentSession();
        if (!$session) {
            return;
        }

        $remaining = max(0, strtotime((string) $session['expires_at']) - time());
        ?>
        <div class="notice notice-info wpstack-demo-session">
            <p>
                <strong><?php esc_html_e('WPStack private demo', 'wpstack-sandbox'); ?></strong>
                — <?php echo esc_html(sprintf(__('expires in %d minutes', 'wpstack-sandbox'), (int) ceil($remaining / 60))); ?>
                <a class="button button-small" href="<?php echo esc_url(wp_nonce_url(admin_url('admin-post.php?action=wpstack_demo_reset'), 'wpstack_demo_reset')); ?>">
                    <?php esc_html_e('Reset demo', 'wpstack-sandbox'); ?>
                </a>
                <?php if ((bool) $session['extensions_allowed']) : ?>
                    <a class="button button-small" href="<?php echo esc_url(wp_nonce_url(admin_url('admin-post.php?action=wpstack_demo_extend'), 'wpstack_demo_extend')); ?>">
                        <?php echo esc_html(sprintf(__('Extend +%d minutes', 'wpstack-sandbox'), (int) $session['extension_minutes'])); ?>
                    </a>
                <?php endif; ?>
                <a class="button button-small" href="<?php echo esc_url(wp_nonce_url(admin_url('admin-post.php?action=wpstack_demo_expire'), 'wpstack_demo_expire')); ?>">
                    <?php esc_html_e('Expire now', 'wpstack-sandbox'); ?>
                </a>
            </p>
        </div>
        <?php
    }

    public function renderSessionDock(): void
    {
        $session = $this->currentSession();
        if (!$session) {
            return;
        }

        $expires = strtotime((string) $session['expires_at']);
        ?>
        <style>
            body.wp-admin { padding-bottom: 58px !important; }
            .wpsd-session-dock {
                position: fixed;
                z-index: 100000;
                right: 16px;
                bottom: 14px;
                left: 176px;
                min-height: 44px;
                box-sizing: border-box;
                display: flex;
                align-items: center;
                justify-content: space-between;
                gap: 14px;
                padding: 8px 10px 8px 16px;
                border-radius: 8px;
                background: #111;
                color: #fff;
                box-shadow: 0 8px 30px rgba(0,0,0,.28);
                font: 600 13px/1.3 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
            }
            .wpsd-session-dock__actions { display: flex; gap: 6px; }
            .wpsd-session-dock .button { margin: 0; }
            @media (max-width: 782px) {
                .wpsd-session-dock { left: 10px; right: 10px; bottom: 10px; }
                .wpsd-session-dock__label { display: none; }
            }
        </style>
        <aside class="wpsd-session-dock" aria-label="<?php esc_attr_e('Demo session controls', 'wpstack-sandbox'); ?>">
            <span>
                <span class="wpsd-session-dock__label"><?php esc_html_e('WPStack private demo · ', 'wpstack-sandbox'); ?></span>
                <span id="wpsd-session-remaining" data-expires="<?php echo esc_attr((string) ($expires * 1000)); ?>">
                    <?php esc_html_e('Calculating time remaining…', 'wpstack-sandbox'); ?>
                </span>
            </span>
            <span class="wpsd-session-dock__actions">
                <a class="button button-small" href="<?php echo esc_url(wp_nonce_url(admin_url('admin-post.php?action=wpstack_demo_reset'), 'wpstack_demo_reset')); ?>">
                    <?php esc_html_e('Reset', 'wpstack-sandbox'); ?>
                </a>
                <?php if ((bool) $session['extensions_allowed']) : ?>
                    <a class="button button-small" href="<?php echo esc_url(wp_nonce_url(admin_url('admin-post.php?action=wpstack_demo_extend'), 'wpstack_demo_extend')); ?>">
                        <?php echo esc_html(sprintf(__('+%d min', 'wpstack-sandbox'), (int) $session['extension_minutes'])); ?>
                    </a>
                <?php endif; ?>
                <a class="button button-small" href="<?php echo esc_url(wp_nonce_url(admin_url('admin-post.php?action=wpstack_demo_expire'), 'wpstack_demo_expire')); ?>" onclick="return confirm('Expire this sandbox now?')">
                    <?php esc_html_e('Expire', 'wpstack-sandbox'); ?>
                </a>
            </span>
        </aside>
        <script>
        (() => {
            const output = document.getElementById('wpsd-session-remaining');
            if (!output) return;
            const expires = Number(output.dataset.expires);
            const update = () => {
                const seconds = Math.max(0, Math.ceil((expires - Date.now()) / 1000));
                const minutes = Math.floor(seconds / 60);
                const remainder = seconds % 60;
                output.textContent = `${minutes}:${String(remainder).padStart(2, '0')} remaining`;
            };
            update();
            window.setInterval(update, 1000);
        })();
        </script>
        <?php
    }

    public function extend(): void
    {
        check_admin_referer('wpstack_demo_extend');
        $session = $this->requireControllableSession();
        $policy = $this->policyForSession($session);
        $current = new DateTimeImmutable((string) $session['expires_at'], new DateTimeZone('UTC'));
        $created = new DateTimeImmutable((string) $session['created_at'], new DateTimeZone('UTC'));
        $requested = $policy->extendedExpiry($current);
        $extended = $requested ? $policy->capExpiry($created, $requested) : $current;

        if ($extended > $current) {
            $this->sessions->updateExpiry(
                (string) $session['session_id'],
                $extended->format('Y-m-d H:i:s')
            );
        }

        wp_safe_redirect($this->controlRedirect($session));
        exit;
    }

    public function expire(): void
    {
        check_admin_referer('wpstack_demo_expire');
        $session = $this->requireControllableSession();
        $this->sessions->setStatus((string) $session['session_id'], 'expired');
        $this->scheduleDeletion((string) $session['session_id'], 1);

        if (
            is_user_logged_in()
            && get_current_user_id() === (int) $session['user_id']
        ) {
            wp_clear_auth_cookie();
        }

        wp_safe_redirect(add_query_arg('demo_error', 'session_expired', network_home_url('/')));
        exit;
    }

    public function reset(): void
    {
        check_admin_referer('wpstack_demo_reset');
        $old = $this->requireCurrentSession();
        $this->sessions->setStatus((string) $old['session_id'], 'replaced');

        try {
            $replacement = $this->provision(
                (string) $old['plugin_slug'],
                (string) $old['visitor_hash']
            );
            $this->sessions->incrementReset((string) $old['session_id']);
            $this->scheduleDeletion((string) $old['session_id'], 60);
            wp_clear_auth_cookie();
            wp_safe_redirect($replacement['login_url']);
            exit;
        } catch (\Throwable $error) {
            $this->sessions->setStatus((string) $old['session_id'], 'active');
            error_log('WPStack sandbox reset failed: ' . $error->getMessage());
            wp_safe_redirect(admin_url('admin.php?page=wpstack-demo-center'));
            exit;
        }
    }

    public function cleanup(): void
    {
        foreach ($this->sessions->dueForCleanup() as $session) {
            $this->deleteSession((string) $session['session_id']);
        }
    }

    public function deleteSession(string $sessionId): void
    {
        foreach ($this->sessions->recent(200) as $session) {
            if ((string) $session['session_id'] !== $sessionId) {
                continue;
            }

            $blogId = (int) $session['blog_id'];
            $userId = (int) $session['user_id'];
            if ($blogId > 1 && get_site($blogId) && !is_main_site($blogId)) {
                require_once ABSPATH . 'wp-admin/includes/ms.php';
                wpmu_delete_blog($blogId, true);
            }

            if ($userId > 0 && get_userdata($userId)) {
                require_once ABSPATH . 'wp-admin/includes/ms.php';
                wpmu_delete_user($userId);
            }

            $this->sessions->setStatus($sessionId, 'deleted');
            break;
        }
    }

    public function restrictDemoMenus(): void
    {
        if (!$this->isDemoSite()) {
            return;
        }

        remove_menu_page('plugins.php');
        remove_menu_page('themes.php');
        remove_menu_page('users.php');
        remove_submenu_page('options-general.php', 'options-permalink.php');
    }

    /**
     * @param null|bool|\WP_Error $return
     * @param array<string, mixed> $attributes
     * @return null|bool|\WP_Error
     */
    public function blockDemoEmail($return, array $attributes)
    {
        return $this->isDemoSite() ? false : $return;
    }

    /**
     * @param false|array<string, mixed>|\WP_Error $preempt
     * @param array<string, mixed> $arguments
     * @return false|array<string, mixed>|\WP_Error
     */
    public function blockDemoExternalRequests($preempt, array $arguments, string $url)
    {
        if (!$this->isDemoSite()) {
            return $preempt;
        }

        $host = wp_parse_url($url, PHP_URL_HOST);
        if ($host === wp_parse_url(network_home_url(), PHP_URL_HOST)) {
            return $preempt;
        }

        return new \WP_Error('wpstack_demo_external_blocked', __('External requests are disabled in demo sandboxes.', 'wpstack-sandbox'));
    }

    /**
     * @param array<string, bool> $robots
     * @return array<string, bool>
     */
    public function noIndexRobots(array $robots): array
    {
        if ($this->isDemoSite() || is_main_site()) {
            $robots['noindex'] = true;
            $robots['nofollow'] = true;
        }

        return $robots;
    }

    public function hideFrontendAdminBar(bool $show): bool
    {
        if ($this->isDemoSite()) {
            return false;
        }

        if (
            is_user_logged_in()
            && get_user_meta(get_current_user_id(), 'wpstack_demo_session', true)
        ) {
            return false;
        }

        return $show;
    }

    public function registerNetworkPage(): void
    {
        add_menu_page(
            __('Demo Sandboxes', 'wpstack-sandbox'),
            __('Demo Sandboxes', 'wpstack-sandbox'),
            'manage_network',
            'wpstack-sandboxes',
            [$this, 'renderNetworkPage'],
            'dashicons-superhero',
            25
        );
    }

    public function renderNetworkPage(): void
    {
        if (!current_user_can('manage_network')) {
            wp_die(esc_html__('You cannot access this page.', 'wpstack-sandbox'));
        }

        require_once ABSPATH . 'wp-admin/includes/plugin.php';
        $installedPlugins = get_plugins();
        unset($installedPlugins[plugin_basename(WPSTACK_SANDBOX_FILE)]);
        $products = $this->settings->products();
        $products[] = [
            'slug' => '',
            'name' => '',
            'description' => '',
            'file' => '',
            'landing' => '/wp-admin/',
            'accent' => '#2563eb',
            'initial_minutes' => 6,
            'extension_minutes' => 6,
            'maximum_minutes' => 50,
            'extensions_allowed' => true,
            'enabled' => true,
            'dependencies' => [],
        ];
        $entitlement = $this->licenses->entitlement();
        $branding = $this->settings->branding();
        ?>
        <div class="wrap">
            <h1><?php esc_html_e('WPStack Sandbox Manager', 'wpstack-sandbox'); ?></h1>
            <?php if (isset($_GET['updated'])) : ?>
                <div class="notice notice-success is-dismissible"><p><?php esc_html_e('Sandbox settings saved.', 'wpstack-sandbox'); ?></p></div>
            <?php endif; ?>
            <p>
                <?php echo esc_html(sprintf(__('Plan: %s', 'wpstack-sandbox'), ucfirst($entitlement->plan()))); ?>
                <?php if ($entitlement->plan() === 'community') : ?>
                    — <?php esc_html_e('Community supports one demo product. Upgrade to Pro for unlimited products, branding, analytics, and webhooks.', 'wpstack-sandbox'); ?>
                <?php endif; ?>
            </p>

            <h2><?php esc_html_e('Demo products', 'wpstack-sandbox'); ?></h2>
            <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
                <input type="hidden" name="action" value="wpstack_sandbox_save_products">
                <?php wp_nonce_field('wpstack_sandbox_save_products'); ?>
                <table class="widefat striped">
                    <thead>
                    <tr>
                        <th><?php esc_html_e('Product', 'wpstack-sandbox'); ?></th>
                        <th><?php esc_html_e('Plugin and landing page', 'wpstack-sandbox'); ?></th>
                        <th><?php esc_html_e('Session policy', 'wpstack-sandbox'); ?></th>
                        <th><?php esc_html_e('Status', 'wpstack-sandbox'); ?></th>
                    </tr>
                    </thead>
                    <tbody>
                    <?php foreach ($products as $index => $product) : ?>
                        <tr>
                            <td style="min-width:220px">
                                <p><input class="regular-text" name="products[<?php echo esc_attr((string) $index); ?>][name]" value="<?php echo esc_attr((string) $product['name']); ?>" placeholder="Product name"></p>
                                <p><input class="regular-text" name="products[<?php echo esc_attr((string) $index); ?>][slug]" value="<?php echo esc_attr((string) $product['slug']); ?>" placeholder="product-slug"></p>
                                <p><textarea class="large-text" rows="3" name="products[<?php echo esc_attr((string) $index); ?>][description]" placeholder="Short description"><?php echo esc_textarea((string) $product['description']); ?></textarea></p>
                                <p><label><?php esc_html_e('Accent', 'wpstack-sandbox'); ?> <input type="color" name="products[<?php echo esc_attr((string) $index); ?>][accent]" value="<?php echo esc_attr((string) $product['accent']); ?>"></label></p>
                            </td>
                            <td style="min-width:260px">
                                <p><select class="widefat" name="products[<?php echo esc_attr((string) $index); ?>][file]">
                                    <option value=""><?php esc_html_e('Select an installed plugin…', 'wpstack-sandbox'); ?></option>
                                    <?php foreach ($installedPlugins as $file => $data) : ?>
                                        <option value="<?php echo esc_attr($file); ?>" <?php selected((string) $product['file'], $file); ?>><?php echo esc_html((string) $data['Name'] . ' — ' . $file); ?></option>
                                    <?php endforeach; ?>
                                </select></p>
                                <p><input class="widefat" name="products[<?php echo esc_attr((string) $index); ?>][landing]" value="<?php echo esc_attr((string) $product['landing']); ?>" placeholder="/wp-admin/admin.php?page=plugin"></p>
                                <p><input class="widefat" name="products[<?php echo esc_attr((string) $index); ?>][dependencies]" value="<?php echo esc_attr(implode(',', (array) ($product['dependencies'] ?? []))); ?>" placeholder="Optional dependency plugin files"></p>
                            </td>
                            <td style="min-width:220px">
                                <p><label><?php esc_html_e('Initial minutes', 'wpstack-sandbox'); ?><br><input type="number" min="1" max="1440" name="products[<?php echo esc_attr((string) $index); ?>][initial_minutes]" value="<?php echo esc_attr((string) $product['initial_minutes']); ?>"></label></p>
                                <p><label><?php esc_html_e('Extension minutes', 'wpstack-sandbox'); ?><br><input type="number" min="1" max="1440" name="products[<?php echo esc_attr((string) $index); ?>][extension_minutes]" value="<?php echo esc_attr((string) $product['extension_minutes']); ?>"></label></p>
                                <p><label><?php esc_html_e('Maximum total minutes', 'wpstack-sandbox'); ?><br><input type="number" min="1" max="1440" name="products[<?php echo esc_attr((string) $index); ?>][maximum_minutes]" value="<?php echo esc_attr((string) $product['maximum_minutes']); ?>"></label></p>
                                <label><input type="checkbox" name="products[<?php echo esc_attr((string) $index); ?>][extensions_allowed]" value="1" <?php checked((bool) $product['extensions_allowed']); ?>> <?php esc_html_e('Allow extensions', 'wpstack-sandbox'); ?></label>
                            </td>
                            <td>
                                <p><label><input type="checkbox" name="products[<?php echo esc_attr((string) $index); ?>][enabled]" value="1" <?php checked((bool) $product['enabled']); ?>> <?php esc_html_e('Enabled', 'wpstack-sandbox'); ?></label></p>
                                <?php if ((string) $product['slug'] !== '') : ?>
                                    <p><label><input type="checkbox" name="products[<?php echo esc_attr((string) $index); ?>][remove]" value="1"> <?php esc_html_e('Remove', 'wpstack-sandbox'); ?></label></p>
                                <?php else : ?>
                                    <em><?php esc_html_e('New product', 'wpstack-sandbox'); ?></em>
                                <?php endif; ?>
                            </td>
                        </tr>
                    <?php endforeach; ?>
                    </tbody>
                </table>
                <?php submit_button(__('Save demo products', 'wpstack-sandbox')); ?>
            </form>

            <h2><?php esc_html_e('Branding', 'wpstack-sandbox'); ?></h2>
            <form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
                <input type="hidden" name="action" value="wpstack_sandbox_save_branding">
                <?php wp_nonce_field('wpstack_sandbox_save_branding'); ?>
                <table class="form-table">
                    <tr><th><label for="wpsd-brand"><?php esc_html_e('Brand name', 'wpstack-sandbox'); ?></label></th><td><input id="wpsd-brand" class="regular-text" name="branding[brand_name]" value="<?php echo esc_attr($branding['brand_name']); ?>" <?php disabled(!$entitlement->allows('white_label')); ?>></td></tr>
                    <tr><th><label for="wpsd-url"><?php esc_html_e('Brand URL', 'wpstack-sandbox'); ?></label></th><td><input id="wpsd-url" type="url" class="regular-text" name="branding[brand_url]" value="<?php echo esc_attr($branding['brand_url']); ?>" <?php disabled(!$entitlement->allows('white_label')); ?>></td></tr>
                    <tr><th><label for="wpsd-label"><?php esc_html_e('Lab label', 'wpstack-sandbox'); ?></label></th><td><input id="wpsd-label" class="regular-text" name="branding[lab_label]" value="<?php echo esc_attr($branding['lab_label']); ?>" <?php disabled(!$entitlement->allows('white_label')); ?>></td></tr>
                </table>
                <?php if ($entitlement->allows('white_label')) : ?>
                    <?php submit_button(__('Save branding', 'wpstack-sandbox')); ?>
                <?php else : ?>
                    <p><?php esc_html_e('Custom branding is available with Pro.', 'wpstack-sandbox'); ?></p>
                <?php endif; ?>
            </form>

            <h2><?php esc_html_e('Recent sessions', 'wpstack-sandbox'); ?></h2>
            <table class="widefat striped">
                <thead><tr><th>Session</th><th>Plugin</th><th>Site</th><th>Status</th><th>Created</th><th>Expires</th></tr></thead>
                <tbody>
                <?php foreach ($this->sessions->recent() as $session) : ?>
                    <tr>
                        <td><code><?php echo esc_html(substr((string) $session['session_id'], 0, 8)); ?></code></td>
                        <td><?php echo esc_html((string) $session['plugin_slug']); ?></td>
                        <td><?php echo esc_html((string) $session['blog_id']); ?></td>
                        <td><?php echo esc_html((string) $session['status']); ?></td>
                        <td><?php echo esc_html((string) $session['created_at']); ?></td>
                        <td><?php echo esc_html((string) $session['expires_at']); ?></td>
                    </tr>
                <?php endforeach; ?>
                </tbody>
            </table>
        </div>
        <?php
    }

    public function saveProducts(): void
    {
        if (!current_user_can('manage_network')) {
            wp_die(esc_html__('You cannot manage sandbox products.', 'wpstack-sandbox'), '', ['response' => 403]);
        }
        check_admin_referer('wpstack_sandbox_save_products');
        $rawProducts = isset($_POST['products']) && is_array($_POST['products'])
            ? wp_unslash($_POST['products'])
            : [];
        require_once ABSPATH . 'wp-admin/includes/plugin.php';
        $installed = array_keys(get_plugins());
        $products = [];
        foreach ($rawProducts as $raw) {
            if (!is_array($raw) || !empty($raw['remove']) || empty($raw['name']) || empty($raw['file'])) {
                continue;
            }
            if (!in_array((string) $raw['file'], $installed, true)) {
                continue;
            }
            $dependencies = array_map(
                'trim',
                explode(',', (string) ($raw['dependencies'] ?? ''))
            );
            $raw['dependencies'] = array_values(
                array_intersect(array_filter($dependencies), $installed)
            );
            $raw['extensions_allowed'] = !empty($raw['extensions_allowed']);
            $raw['enabled'] = !empty($raw['enabled']);
            $products[] = $raw;
        }
        $this->settings->saveProducts($products, $this->licenses->entitlement());
        wp_safe_redirect(add_query_arg('updated', '1', network_admin_url('admin.php?page=wpstack-sandboxes')));
        exit;
    }

    public function saveBranding(): void
    {
        if (!current_user_can('manage_network')) {
            wp_die(esc_html__('You cannot manage sandbox branding.', 'wpstack-sandbox'), '', ['response' => 403]);
        }
        check_admin_referer('wpstack_sandbox_save_branding');
        if (!$this->licenses->entitlement()->allows('white_label')) {
            wp_die(esc_html__('Custom branding requires Pro.', 'wpstack-sandbox'), '', ['response' => 403]);
        }
        $branding = isset($_POST['branding']) && is_array($_POST['branding'])
            ? wp_unslash($_POST['branding'])
            : [];
        $this->settings->saveBranding($branding);
        wp_safe_redirect(add_query_arg('updated', '1', network_admin_url('admin.php?page=wpstack-sandboxes')));
        exit;
    }

    /**
     * @return array{login_url:string,session_id:string}
     */
    private function provision(string $pluginSlug, string $visitorHash): array
    {
        $plugin = $this->catalog->get($pluginSlug);
        if (!$plugin) {
            throw new \InvalidArgumentException('Unknown plugin.');
        }

        $sessionId = wp_generate_uuid4();
        $loginToken = $this->tokens->generate();
        $created = new DateTimeImmutable('now', new DateTimeZone('UTC'));
        $policy = $this->policyForProduct($plugin);
        $expires = $policy->initialExpiry($created);
        $slug = (new SandboxSlug())->generate();
        $username = 'demo_' . strtolower(wp_generate_password(10, false, false));
        $userId = 0;
        $blogId = 0;

        if (
            !$this->sessions->insert(
                [
                    'session_id' => $sessionId,
                    'plugin_slug' => $pluginSlug,
                    'visitor_hash' => $visitorHash,
                    'login_hash' => $this->tokens->hash($loginToken),
                    'status' => 'provisioning',
                    'created_at' => $created->format('Y-m-d H:i:s'),
                    'expires_at' => $expires->format('Y-m-d H:i:s'),
                    'initial_minutes' => (int) $plugin['initial_minutes'],
                    'extension_minutes' => (int) $plugin['extension_minutes'],
                    'maximum_minutes' => (int) $plugin['maximum_minutes'],
                    'extensions_allowed' => (int) (bool) $plugin['extensions_allowed'],
                ]
            )
        ) {
            throw new \RuntimeException('Session could not be recorded.');
        }

        try {
            $userId = wp_insert_user(
                [
                    'user_login' => $username,
                    'user_pass' => wp_generate_password(32, true, true),
                    'user_email' => $username . '@demo.invalid',
                    'display_name' => 'WPStack Demo User',
                    'role' => '',
                ]
            );
            if (is_wp_error($userId)) {
                throw new \RuntimeException($userId->get_error_message());
            }

            $network = get_network();
            $path = trailingslashit($network->path) . $slug . '/';
            $blogId = wpmu_create_blog(
                $network->domain,
                $path,
                'WPStack Private Demo',
                (int) $userId,
                ['public' => 0],
                get_current_network_id()
            );
            if (is_wp_error($blogId)) {
                throw new \RuntimeException($blogId->get_error_message());
            }

            update_site_meta((int) $blogId, 'wpstack_demo', 1);
            update_user_meta((int) $userId, 'wpstack_demo_session', $sessionId);
            (new DemoSeeder())->seed((int) $blogId, $pluginSlug, $plugin);
            $this->sessions->markActive($sessionId, (int) $blogId, (int) $userId);
        } catch (\Throwable $error) {
            if ((int) $blogId > 1 && get_site((int) $blogId)) {
                require_once ABSPATH . 'wp-admin/includes/ms.php';
                wpmu_delete_blog((int) $blogId, true);
            }
            if ((int) $userId > 0 && get_userdata((int) $userId)) {
                require_once ABSPATH . 'wp-admin/includes/ms.php';
                wpmu_delete_user((int) $userId);
            }
            $this->sessions->markFailed($sessionId, $error->getMessage());
            throw $error;
        }

        return [
            'session_id' => $sessionId,
            'login_url' => add_query_arg('wpstack_demo_login', rawurlencode($loginToken), network_home_url('/')),
        ];
    }

    private function visitorHash(): string
    {
        $address = isset($_SERVER['REMOTE_ADDR'])
            ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR']))
            : '';
        $cookieName = 'wpstack_sandbox_visitor';
        $token = isset($_COOKIE[$cookieName])
            ? sanitize_text_field(wp_unslash($_COOKIE[$cookieName]))
            : '';
        if (!preg_match('/^[a-f0-9]{64}$/', $token)) {
            $token = bin2hex(random_bytes(32));
            setcookie(
                $cookieName,
                $token,
                [
                    'expires' => time() + YEAR_IN_SECONDS,
                    'path' => '/',
                    'secure' => is_ssl(),
                    'httponly' => true,
                    'samesite' => 'Lax',
                ]
            );
            $_COOKIE[$cookieName] = $token;
        }

        $visitorHash = $this->visitors->fromTokenAndAddress($token, $address);
        $legacy = $this->sessions->findActiveByVisitor(
            $this->visitors->fromAddress($address)
        );
        if ($legacy) {
            $this->sessions->updateVisitorHash(
                (string) $legacy['session_id'],
                $visitorHash
            );
        }

        return $visitorHash;
    }

    private function isDemoSite(): bool
    {
        return !is_main_site() && (int) get_site_meta(get_current_blog_id(), 'wpstack_demo', true) === 1;
    }

    /**
     * @return array<string, mixed>|null
     */
    private function currentSession(): ?array
    {
        if (!$this->isDemoSite() || !is_user_logged_in()) {
            return null;
        }

        return $this->sessions->findActiveByUser(get_current_user_id());
    }

    /**
     * @return array<string, mixed>
     */
    private function requireCurrentSession(): array
    {
        $session = $this->currentSession();
        if (!$session) {
            wp_die(
                esc_html__('Your demo session is no longer active.', 'wpstack-sandbox'),
                esc_html__('Demo expired', 'wpstack-sandbox'),
                ['response' => 410]
            );
        }

        return $session;
    }

    /**
     * @return array<string, mixed>
     */
    private function requireControllableSession(): array
    {
        $session = $this->currentSession();
        if (!$session && is_main_site()) {
            $session = $this->sessions->findActiveByVisitor($this->visitorHash());
        }

        if (!$session) {
            wp_die(
                esc_html__('Your demo session is no longer active.', 'wpstack-sandbox'),
                esc_html__('Demo expired', 'wpstack-sandbox'),
                ['response' => 410]
            );
        }

        return $session;
    }

    /**
     * @param array<string, mixed> $session
     */
    private function controlRedirect(array $session): string
    {
        if ($this->isDemoSite()) {
            return admin_url('admin.php?page=wpstack-demo-center');
        }

        return network_home_url('/');
    }

    private function renderCountdownScript(): void
    {
        ?>
        <script>
        (() => {
            const output = document.querySelector('[data-wpsd-countdown]');
            if (!output) return;
            const expires = Number(output.dataset.expires);
            const update = () => {
                const seconds = Math.max(0, Math.ceil((expires - Date.now()) / 1000));
                const minutes = Math.floor(seconds / 60);
                const remainder = seconds % 60;
                output.textContent = `${minutes}:${String(remainder).padStart(2, '0')} remaining`;
            };
            update();
            window.setInterval(update, 1000);
        })();
        </script>
        <?php
    }

    private function scheduleDeletion(string $sessionId, int $delay): void
    {
        $mainSiteId = get_main_site_id();
        $switched = get_current_blog_id() !== $mainSiteId;
        if ($switched) {
            switch_to_blog($mainSiteId);
        }

        wp_schedule_single_event(
            time() + max(1, $delay),
            'wpstack_demo_delete_session',
            [$sessionId]
        );

        if ($switched) {
            restore_current_blog();
        }
    }

    private function redirectBoard(string $error): void
    {
        wp_safe_redirect(add_query_arg('demo_error', sanitize_key($error), network_home_url('/')));
        exit;
    }

    /**
     * @param array<string, mixed> $product
     */
    private function policyForProduct(array $product): SessionPolicy
    {
        return new SessionPolicy(
            (int) ($product['initial_minutes'] ?? SessionPolicy::INITIAL_MINUTES),
            (int) ($product['extension_minutes'] ?? SessionPolicy::EXTENSION_MINUTES),
            (int) ($product['maximum_minutes'] ?? SessionPolicy::MAXIMUM_MINUTES),
            (bool) ($product['extensions_allowed'] ?? true)
        );
    }

    /**
     * @param array<string, mixed> $session
     */
    private function policyForSession(array $session): SessionPolicy
    {
        return new SessionPolicy(
            (int) ($session['initial_minutes'] ?? SessionPolicy::INITIAL_MINUTES),
            (int) ($session['extension_minutes'] ?? SessionPolicy::EXTENSION_MINUTES),
            (int) ($session['maximum_minutes'] ?? SessionPolicy::MAXIMUM_MINUTES),
            (bool) ($session['extensions_allowed'] ?? true)
        );
    }
}
