<?php

namespace WPSN\Modules\Licensing\Admin;

use WPSN\Admin\SettingsRepository;
use WPSN\Core\Capabilities;
use WPSN\Core\Container;
use WPSN\Modules\Licensing\BillingCenterData;
use WPSN\Modules\Licensing\BillingSyncService;
use WPSN\Modules\Licensing\LicenseSecretsRepository;
use WPSN\Modules\Licensing\LicenseService;
use WPSN\Modules\Licensing\LicenseSignatureVerifier;
use WPSN\Modules\Licensing\PlanMatrix;
use WPSN\Modules\Licensing\SelfServeManagementService;
use WPSN\Modules\Licensing\SelfServeUpgradeService;
use WPSN\Modules\Licensing\ServerContract;

defined('ABSPATH') || exit;

final class LicensingAdmin
{
    private const PAGE_SLUG = 'wpsn-license';

    private Container $container;
    private LicenseService $service;
    private BillingSyncService $billingSync;
    private SelfServeUpgradeService $selfServe;
    private SelfServeManagementService $management;
    private SettingsRepository $settingsRepo;

    public function __construct(Container $container, LicenseService $service)
    {
        $this->container = $container;
        $this->service = $service;
        $this->billingSync = new BillingSyncService($container->get('settings'), $container->get('logger'));
        $this->selfServe = new SelfServeUpgradeService($container->get('settings'), $container->get('logger'));
        $this->management = new SelfServeManagementService($container->get('settings'), $container->get('logger'));
        $this->settingsRepo = $container->get('settings_repo');
    }

    public function register(): void
    {
        add_action('admin_menu', [$this, 'addMenu']);

        add_action('admin_post_wpsn_license_save', [$this, 'handleSave']);
        add_action('admin_post_wpsn_license_clear', [$this, 'handleClear']);
        add_action('admin_post_wpsn_license_verify', [$this, 'handleVerify']);
        add_action('admin_post_wpsn_license_clear_cache', [$this, 'handleClearCache']);
        add_action('admin_post_wpsn_billing_sync', [$this, 'handleBillingSync']);
        add_action('admin_post_wpsn_billing_clear_cache', [$this, 'handleClearBillingCache']);
        add_action('admin_post_wpsn_self_serve_catalog_refresh', [$this, 'handleSelfServeCatalogRefresh']);
        add_action('admin_post_wpsn_self_serve_catalog_clear', [$this, 'handleSelfServeCatalogClear']);
        add_action('admin_post_wpsn_self_serve_checkout', [$this, 'handleSelfServeCheckout']);
        add_action('admin_post_wpsn_self_serve_checkout_clear', [$this, 'handleSelfServeCheckoutClear']);
        add_action('admin_post_wpsn_self_serve_action', [$this, 'handleSelfServeAction']);
        add_action('admin_post_wpsn_self_serve_manage_refresh', [$this, 'handleSelfServeManagementRefresh']);
        add_action('admin_post_wpsn_self_serve_manage_clear', [$this, 'handleSelfServeManagementClear']);
        add_action('admin_post_wpsn_self_serve_manage_update_customer', [$this, 'handleSelfServeManagementUpdateCustomer']);
        add_action('admin_post_wpsn_self_serve_manage_change_plan', [$this, 'handleSelfServeManagementChangePlan']);
        add_action('admin_post_wpsn_self_serve_manage_usage', [$this, 'handleSelfServeManagementUsage']);
        add_action('admin_post_wpsn_self_serve_manage_subscription', [$this, 'handleSelfServeManagementSubscription']);
        add_action('admin_post_wpsn_self_serve_manage_payment_method', [$this, 'handleSelfServeManagementPaymentMethod']);
        add_action('admin_post_wpsn_self_serve_manage_site_assignment', [$this, 'handleSelfServeManagementSiteAssignment']);
        add_action('admin_post_wpsn_self_serve_manage_seat_assignment', [$this, 'handleSelfServeManagementSeatAssignment']);

        add_action('admin_notices', [$this, 'adminNotices']);
    }

    public function addMenu(): void
    {
        // Agency mode: allow hiding upgrade/licensing UI for client sites.
        $hideUpgrade = (bool) $this->container->get('settings')->get('white_label.hide_upgrade_ui', false);
        if ($hideUpgrade) {
            return;
        }

        add_management_page(
            __('SEO License', 'wp-seo-next'),
            __('SEO License', 'wp-seo-next'),
            Capabilities::CAP_LICENSE,
            self::PAGE_SLUG,
            [$this, 'renderPage']
        );
    }

    public function adminNotices(): void
    {
        if (empty($_GET['wpsn_notice'])) {
            return;
        }
        $notice = sanitize_key((string) wp_unslash($_GET['wpsn_notice']));

        $messages = [
            'license_saved' => __('License settings saved.', 'wp-seo-next'),
            'license_cleared' => __('License key cleared.', 'wp-seo-next'),
            'license_cache_cleared' => __('License cache cleared.', 'wp-seo-next'),
            'license_verified' => __('License verified.', 'wp-seo-next'),
            'billing_synced' => __('Billing data synced.', 'wp-seo-next'),
            'billing_cache_cleared' => __('Billing sync cache cleared.', 'wp-seo-next'),
            'self_serve_catalog_synced' => __('Self-serve plan catalog synced.', 'wp-seo-next'),
            'self_serve_catalog_cleared' => __('Self-serve plan cache cleared.', 'wp-seo-next'),
            'self_serve_checkout_started' => __('Self-serve checkout started.', 'wp-seo-next'),
            'self_serve_checkout_cleared' => __('Embedded checkout session cleared.', 'wp-seo-next'),
            'self_serve_action_done' => __('Self-serve billing action completed.', 'wp-seo-next'),
            'self_serve_manage_synced' => __('Subscription management state synced.', 'wp-seo-next'),
            'self_serve_manage_cleared' => __('Subscription management cache cleared.', 'wp-seo-next'),
            'self_serve_manage_updated' => __('Subscription management update completed.', 'wp-seo-next'),
            'settings_locked' => __('Settings are locked by Config Sync. Only license key was saved.', 'wp-seo-next'),
        ];

        $errors = [
            'permission' => __('You do not have permission to access this page.', 'wp-seo-next'),
            'invalid_request' => __('Invalid request.', 'wp-seo-next'),
            'verify_failed' => __('License verification failed.', 'wp-seo-next'),
            'billing_sync_failed' => __('Billing sync failed.', 'wp-seo-next'),
            'self_serve_failed' => __('Self-serve billing request failed.', 'wp-seo-next'),
            'self_serve_manage_failed' => __('Subscription management request failed.', 'wp-seo-next'),
        ];

        if (isset($messages[$notice])) {
            $msg = $messages[$notice];
            if (!empty($_GET['wpsn_message'])) {
                $msg .= ' ' . (string) sanitize_text_field((string) wp_unslash($_GET['wpsn_message']));
            }
            echo '<div class="notice notice-success is-dismissible"><p>' . esc_html($msg) . '</p></div>';
            return;
        }

        if (isset($errors[$notice])) {
            $msg = $errors[$notice];
            if (!empty($_GET['wpsn_error'])) {
                $msg .= ' ' . (string) sanitize_text_field((string) wp_unslash($_GET['wpsn_error']));
            }
            echo '<div class="notice notice-error is-dismissible"><p>' . esc_html($msg) . '</p></div>';
        }
    }

    public function renderPage(): void
    {
        if (!current_user_can(Capabilities::CAP_LICENSE)) {
            wp_die(esc_html__('You do not have permission to access this page.', 'wp-seo-next'));
        }

        $settings = $this->settingsRepo->getAll();
        $locked = !empty($settings['config_sync']['lock_settings']);

        $tab = isset($_GET['tab']) ? sanitize_key((string) wp_unslash($_GET['tab'])) : 'overview';
        if (!in_array($tab, ['overview', 'billing', 'settings'], true)) {
            $tab = 'overview';
        }

        $status = $this->service->status();
        $trusted = $this->service->isTrusted();
        $ents = $this->service->entitlements();
        $cache = $this->service->getCache();
        $billingCache = $this->billingSync->getCache();
        $billing = BillingCenterData::build($cache, $settings, $billingCache);

        $config = $this->licensingConfig();

        echo '<div class="wrap">';
        echo '<h1>' . esc_html__('SEO License', 'wp-seo-next') . '</h1>';
        echo '<p>' . esc_html__('Verify the license, view subscription details, and surface a Billing Center inside wp-admin when your SaaS payload or plugin settings provide portal data.', 'wp-seo-next') . '</p>';

        if ($locked) {
            echo '<div class="notice notice-info"><p>' . wp_kses_post(__('Settings are currently <strong>locked</strong> by Config Sync. You can still save the license key (secret), but server URL/enforcement toggles won’t be persisted.', 'wp-seo-next')) . '</p></div>';
        }

        echo '<h2 class="nav-tab-wrapper">';
        echo '<a href="' . esc_url($this->pageUrl(['tab' => 'overview'])) . '" class="nav-tab ' . ($tab === 'overview' ? 'nav-tab-active' : '') . '">' . esc_html__('Overview', 'wp-seo-next') . '</a>';
        echo '<a href="' . esc_url($this->pageUrl(['tab' => 'billing'])) . '" class="nav-tab ' . ($tab === 'billing' ? 'nav-tab-active' : '') . '">' . esc_html__('Billing Center', 'wp-seo-next') . '</a>';
        echo '<a href="' . esc_url($this->pageUrl(['tab' => 'settings'])) . '" class="nav-tab ' . ($tab === 'settings' ? 'nav-tab-active' : '') . '">' . esc_html__('Configuration', 'wp-seo-next') . '</a>';
        echo '</h2>';

        if ($tab === 'billing') {
            $this->renderBillingCenter($billing, $status, $trusted, $config);
        } elseif ($tab === 'settings') {
            $this->renderSettings($config, $locked);
        } else {
            $this->renderOverview($status, $trusted, $ents, $cache, $config, $billing);
        }

        echo '</div>';
    }

    private function pageUrl(array $args = []): string
    {
        $base = admin_url('tools.php?page=' . self::PAGE_SLUG);
        if (!empty($args)) {
            $base = add_query_arg($args, $base);
        }
        return $base;
    }

    /**
     * @return array<string,mixed>
     */
    private function licensingConfig(): array
    {
        return [
            'server_url' => (string) $this->container->get('settings')->get('licensing.server_url', ''),
            'verify_path' => (string) $this->container->get('settings')->get('licensing.verify_path', '/wp-seo-next/license/verify'),
            'update_server_url' => (string) $this->container->get('settings')->get('licensing.update_server_url', ''),
            'update_path' => (string) $this->container->get('settings')->get('licensing.update_path', '/wp-seo-next/plugin/update'),
            'enable_updates' => (bool) $this->container->get('settings')->get('licensing.enable_updates', false),
            'purchase_url' => (string) $this->container->get('settings')->get('licensing.purchase_url', ''),
            'billing_portal_url' => (string) $this->container->get('settings')->get('licensing.billing_portal_url', ''),
            'account_portal_url' => (string) $this->container->get('settings')->get('licensing.account_portal_url', ''),
            'invoices_url' => (string) $this->container->get('settings')->get('licensing.invoices_url', ''),
            'change_plan_url' => (string) $this->container->get('settings')->get('licensing.change_plan_url', ''),
            'cancel_url' => (string) $this->container->get('settings')->get('licensing.cancel_url', ''),
            'payment_method_url' => (string) $this->container->get('settings')->get('licensing.payment_method_url', ''),
            'billing_support_url' => (string) $this->container->get('settings')->get('licensing.billing_support_url', ''),
            'billing_embed' => (bool) $this->container->get('settings')->get('licensing.billing_embed', false),
            'billing_embed_height' => (int) $this->container->get('settings')->get('licensing.billing_embed_height', 980),
            'billing_embed_allowed_hosts' => $this->container->get('settings')->get('licensing.billing_embed_allowed_hosts', []),
            'billing_sync_enabled' => (bool) $this->container->get('settings')->get('licensing.billing_sync_enabled', false),
            'billing_sync_path' => (string) $this->container->get('settings')->get('licensing.billing_sync_path', '/wp-seo-next/billing/sync'),
            'billing_sync_timeout' => (int) $this->container->get('settings')->get('licensing.billing_sync_timeout', 15),
            'billing_sync_cache_ttl_hours' => (int) $this->container->get('settings')->get('licensing.billing_sync_cache_ttl_hours', 12),
            'billing_sync_scheduled' => (bool) $this->container->get('settings')->get('licensing.billing_sync_scheduled', true),
            'billing_sync_require_signed' => (bool) $this->container->get('settings')->get('licensing.billing_sync_require_signed', false),
            'self_serve_enabled' => (bool) $this->container->get('settings')->get('licensing.self_serve_enabled', false),
            'self_serve_catalog_path' => (string) $this->container->get('settings')->get('licensing.self_serve_catalog_path', '/wp-seo-next/billing/catalog'),
            'self_serve_checkout_path' => (string) $this->container->get('settings')->get('licensing.self_serve_checkout_path', '/wp-seo-next/billing/checkout'),
            'self_serve_action_path' => (string) $this->container->get('settings')->get('licensing.self_serve_action_path', '/wp-seo-next/billing/action'),
            'self_serve_manage_path' => (string) $this->container->get('settings')->get('licensing.self_serve_manage_path', '/wp-seo-next/billing/manage'),
            'self_serve_timeout' => (int) $this->container->get('settings')->get('licensing.self_serve_timeout', 20),
            'self_serve_cache_ttl_hours' => (int) $this->container->get('settings')->get('licensing.self_serve_cache_ttl_hours', 6),
            'self_serve_require_signed' => (bool) $this->container->get('settings')->get('licensing.self_serve_require_signed', false),
            'cache_ttl_hours' => (int) $this->container->get('settings')->get('licensing.cache_ttl_hours', 24),
            'grace_days' => (int) $this->container->get('settings')->get('licensing.grace_days', 14),
            'enforce_entitlements' => (bool) $this->container->get('settings')->get('licensing.enforce_entitlements', false),
            'require_signed' => (bool) $this->container->get('settings')->get('licensing.require_signed', false),
            'scheduled_verify' => (bool) $this->container->get('settings')->get('licensing.scheduled_verify', true),
        ];
    }

    /**
     * @param array<string,mixed>|null $cache
     * @param string[] $ents
     * @param array<string,mixed> $config
     * @param array<string,mixed> $billing
     */
    private function renderOverview(string $status, bool $trusted, array $ents, ?array $cache, array $config, array $billing): void
    {
        $checkedAt = $cache && isset($cache['checked_at']) ? (int) $cache['checked_at'] : 0;
        $checkedHuman = $checkedAt > 0 ? gmdate('Y-m-d H:i:s', $checkedAt) . ' UTC' : __('Never', 'wp-seo-next');

        $lastSuccess = $cache && isset($cache['last_success_at']) ? (int) $cache['last_success_at'] : 0;
        $lastSuccessHuman = $lastSuccess > 0 ? gmdate('Y-m-d H:i:s', $lastSuccess) . ' UTC' : __('Never', 'wp-seo-next');

        $nextCheck = $checkedAt > 0 ? ($checkedAt + (((int) $config['cache_ttl_hours']) * HOUR_IN_SECONDS)) : 0;
        $nextCheckHuman = $nextCheck > 0 ? gmdate('Y-m-d H:i:s', $nextCheck) . ' UTC' : __('n/a', 'wp-seo-next');

        $err = $cache && isset($cache['last_error']) ? (string) $cache['last_error'] : '';
        $badge = $this->renderStatusBadge($status, $trusted);
        $licensedPlan = PlanMatrix::detectCurrentPlan($cache, $this->billingSync->getCache(), $ents);
        $buildTier = PlanMatrix::buildTier();
        $billingPlanRaw = isset($billing['plan']) ? trim((string) $billing['plan']) : '';
        $secrets = new LicenseSecretsRepository();
        $contract = new ServerContract($this->container->get('settings'), $secrets);
        $binding = $secrets->getServerBinding();
        $sigVerifier = new LicenseSignatureVerifier($this->container->get('settings'));
        $serverMeta = isset($cache['server']['meta']) && is_array($cache['server']['meta']) ? $cache['server']['meta'] : [];
        $requestSigningActive = trim($secrets->getRequestSecret()) !== '';

        echo '<h2>' . esc_html__('Status', 'wp-seo-next') . '</h2>';
        echo '<p>' . wp_kses_post($badge) . '</p>';

        if ($err !== '') {
            echo '<p><strong>' . esc_html__('Last error:', 'wp-seo-next') . '</strong> ' . esc_html($err) . '</p>';
        }

        echo '<table class="widefat striped" style="max-width: 980px;">';
        echo '<tbody>';
        echo '<tr><th style="width:240px;">' . esc_html__('Last checked', 'wp-seo-next') . '</th><td><code>' . esc_html($checkedHuman) . '</code></td></tr>';
        echo '<tr><th>' . esc_html__('Last success', 'wp-seo-next') . '</th><td><code>' . esc_html($lastSuccessHuman) . '</code></td></tr>';
        echo '<tr><th>' . esc_html__('Next check (approx.)', 'wp-seo-next') . '</th><td><code>' . esc_html($nextCheckHuman) . '</code></td></tr>';
        echo '<tr><th>' . esc_html__('Plugin build', 'wp-seo-next') . '</th><td><strong>' . esc_html(PlanMatrix::tierLabel($buildTier)) . '</strong></td></tr>';
        echo '<tr><th>' . esc_html__('Licensed tier', 'wp-seo-next') . '</th><td><strong>' . esc_html(PlanMatrix::tierLabel($licensedPlan)) . '</strong></td></tr>';
        echo '<tr><th>' . esc_html__('Entitlement enforcement', 'wp-seo-next') . '</th><td>' . esc_html($this->service->enforceEntitlements() ? __('Enabled', 'wp-seo-next') : __('Disabled', 'wp-seo-next')) . '</td></tr>';
        echo '<tr><th>' . esc_html__('Contract version', 'wp-seo-next') . '</th><td><code>' . esc_html((string) (($binding['contract_version'] ?? '') !== '' ? $binding['contract_version'] : ServerContract::VERSION)) . '</code></td></tr>';
        echo '<tr><th>' . esc_html__('Install ID', 'wp-seo-next') . '</th><td><code>' . esc_html($contract->installId()) . '</code></td></tr>';
        echo '<tr><th>' . esc_html__('Site fingerprint', 'wp-seo-next') . '</th><td><code>' . esc_html($contract->siteFingerprint()) . '</code></td></tr>';
        echo '<tr><th>' . esc_html__('Bound server IDs', 'wp-seo-next') . '</th><td>';
        $boundParts = [];
        foreach (['site_id' => __('Site', 'wp-seo-next'), 'account_id' => __('Account', 'wp-seo-next'), 'client_id' => __('Client', 'wp-seo-next')] as $boundKey => $boundLabel) {
            if (!empty($binding[$boundKey])) {
                $boundParts[] = $boundLabel . ': ' . (string) $binding[$boundKey];
            }
        }
        echo !empty($boundParts) ? '<code>' . esc_html(implode(' · ', $boundParts)) . '</code>' : '—';
        echo '</td></tr>';
        echo '<tr><th>' . esc_html__('Request signing', 'wp-seo-next') . '</th><td>' . esc_html($requestSigningActive ? __('Active', 'wp-seo-next') : __('Not active yet', 'wp-seo-next'));
        if (!empty($binding['request_kid'])) {
            echo ' <code>' . esc_html((string) $binding['request_kid']) . '</code>';
        }
        echo '</td></tr>';
        echo '<tr><th>' . esc_html__('Response public key', 'wp-seo-next') . '</th><td>' . esc_html($sigVerifier->hasPublicKey() ? __('Configured', 'wp-seo-next') : __('Not configured', 'wp-seo-next')) . '</td></tr>';
        if (!empty($serverMeta['trace_id']) || !empty($serverMeta['request_id'])) {
            $serverMetaParts = [];
            if (!empty($serverMeta['trace_id'])) {
                $serverMetaParts[] = 'trace_id=' . (string) $serverMeta['trace_id'];
            }
            if (!empty($serverMeta['request_id'])) {
                $serverMetaParts[] = 'request_id=' . (string) $serverMeta['request_id'];
            }
            echo '<tr><th>' . esc_html__('Last server trace', 'wp-seo-next') . '</th><td><code>' . esc_html(implode(' · ', $serverMetaParts)) . '</code></td></tr>';
        }
        if ($billingPlanRaw !== '') {
            $planText = $billingPlanRaw;
            if (PlanMatrix::normalizePlan($billingPlanRaw) !== sanitize_key($billingPlanRaw)) {
                $planText .= ' → ' . PlanMatrix::tierLabel($licensedPlan);
            }
            echo '<tr><th>' . esc_html__('Billing plan (raw)', 'wp-seo-next') . '</th><td><code>' . esc_html($planText) . '</code></td></tr>';
        }
        if (!empty($billing['subscription_id'])) {
            echo '<tr><th>' . esc_html__('Subscription ID', 'wp-seo-next') . '</th><td><code>' . esc_html((string) $billing['subscription_id']) . '</code></td></tr>';
        }
        if (!empty($billing['expires_at'])) {
            echo '<tr><th>' . esc_html__('Renews / expires', 'wp-seo-next') . '</th><td><code>' . esc_html((string) $billing['expires_at']) . '</code></td></tr>';
        }
        echo '</tbody>';
        echo '</table>';

        echo '<p style="margin-top:12px;">';
        echo '<a class="button" href="' . esc_url(wp_nonce_url(admin_url('admin-post.php?action=wpsn_license_verify'), 'wpsn_license_verify')) . '">' . esc_html__('Verify now', 'wp-seo-next') . '</a> ';
        echo '<a class="button" href="' . esc_url(wp_nonce_url(admin_url('admin-post.php?action=wpsn_license_clear_cache'), 'wpsn_license_clear_cache')) . '">' . esc_html__('Clear cache', 'wp-seo-next') . '</a>';
        if (!empty($config['purchase_url'])) {
            echo ' <a class="button button-primary" href="' . esc_url((string) $config['purchase_url']) . '" target="_blank" rel="noopener noreferrer">' . esc_html__('Upgrade / Manage subscription', 'wp-seo-next') . '</a>';
        }
        if (!empty($billing['has_data'])) {
            echo ' <a class="button button-secondary" href="' . esc_url($this->pageUrl(['tab' => 'billing'])) . '">' . esc_html__('Open Billing Center', 'wp-seo-next') . '</a>';
        }
        if (!empty($config['billing_sync_enabled'])) {
            echo ' <a class="button" href="' . esc_url(wp_nonce_url(admin_url('admin-post.php?action=wpsn_billing_sync'), 'wpsn_billing_sync')) . '">' . esc_html__('Sync billing now', 'wp-seo-next') . '</a>';
        }
        if (!empty($config['self_serve_enabled'])) {
            echo ' <a class="button" href="' . esc_url(wp_nonce_url(admin_url('admin-post.php?action=wpsn_self_serve_catalog_refresh'), 'wpsn_self_serve_catalog_refresh')) . '">' . esc_html__('Refresh plans', 'wp-seo-next') . '</a>';
            $checkoutSession = $this->selfServe->getCheckoutSession();
            if ($checkoutSession && !empty($checkoutSession['url'])) {
                echo ' <a class="button button-primary" href="' . esc_url($this->pageUrl(['tab' => 'billing'])) . '">' . esc_html__('Continue checkout', 'wp-seo-next') . '</a>';
            }
        }
        echo '</p>';

        echo '<hr />';
        echo '<h2>' . esc_html__('Entitlements', 'wp-seo-next') . '</h2>';

        if (empty($ents)) {
            echo '<p class="description">' . esc_html__('No entitlements found (or license not verified yet).', 'wp-seo-next') . '</p>';
        } else {
            echo '<ul style="list-style:disc; padding-left: 20px;">';
            foreach ($ents as $e) {
                echo '<li><code>' . esc_html($e) . '</code></li>';
            }
            echo '</ul>';
        }

        $this->renderPlanMatrixSection($licensedPlan, $config);

        if (!empty($billing['seat_used']) || !empty($billing['seat_limit']) || !empty($billing['site_count']) || !empty($billing['site_limit'])) {
            echo '<hr />';
            echo '<h2>' . esc_html__('Subscription usage', 'wp-seo-next') . '</h2>';
            echo '<table class="widefat striped" style="max-width: 980px;">';
            echo '<tbody>';
            echo '<tr><th style="width:240px;">' . esc_html__('Seats', 'wp-seo-next') . '</th><td>' . esc_html($this->formatUsage($billing['seat_used'] ?? null, $billing['seat_limit'] ?? null)) . '</td></tr>';
            echo '<tr><th>' . esc_html__('Sites / domains', 'wp-seo-next') . '</th><td>' . esc_html($this->formatUsage($billing['site_count'] ?? null, $billing['site_limit'] ?? null)) . '</td></tr>';
            echo '</tbody>';
            echo '</table>';
        }

        if (!empty($config['billing_sync_enabled'])) {
            $syncCheckedAt = isset($billing['sync_checked_at']) ? (int) $billing['sync_checked_at'] : 0;
            $syncHuman = $syncCheckedAt > 0 ? gmdate('Y-m-d H:i:s', $syncCheckedAt) . ' UTC' : __('Never', 'wp-seo-next');
            echo '<hr />';
            echo '<h2>' . esc_html__('Billing sync', 'wp-seo-next') . '</h2>';
            echo '<table class="widefat striped" style="max-width: 980px;">';
            echo '<tbody>';
            echo '<tr><th style="width:240px;">' . esc_html__('Data source', 'wp-seo-next') . '</th><td>' . esc_html($this->formatBillingSource((string) ($billing['source'] ?? 'manual'))) . '</td></tr>';
            echo '<tr><th>' . esc_html__('Last billing sync', 'wp-seo-next') . '</th><td><code>' . esc_html($syncHuman) . '</code></td></tr>';
            echo '<tr><th>' . esc_html__('Sync status', 'wp-seo-next') . '</th><td>' . esc_html((string) (($billing['sync_status'] ?? '') !== '' ? $billing['sync_status'] : '—')) . '</td></tr>';
            echo '</tbody>';
            echo '</table>';
            if (!empty($billing['sync_last_error'])) {
                echo '<p><strong>' . esc_html__('Last billing sync error:', 'wp-seo-next') . '</strong> ' . esc_html((string) $billing['sync_last_error']) . '</p>';
            }
        }
    }

    /**
     * @param array<string,mixed> $config
     */
    private function renderPlanMatrixSection(string $licensedPlan, array $config): void
    {
        $buildTier = PlanMatrix::buildTier();
        $tiers = PlanMatrix::tiers();
        $currentRank = PlanMatrix::tierRank($licensedPlan);
        $moduleNames = $this->moduleLabelsById();

        echo '<hr />';
        echo '<h2>' . esc_html__('Free / Pro / Agency matrix', 'wp-seo-next') . '</h2>';
        echo '<p class="description">' . esc_html__('These are the plugin’s runtime tiers used for feature gating. Self-serve marketing labels like “Starter” are normalized into this matrix for module access.', 'wp-seo-next') . '</p>';

        if ($this->service->enforceEntitlements()) {
            echo '<p class="description">' . esc_html__('Entitlement enforcement is ON, so the badges below reflect real runtime gating.', 'wp-seo-next') . '</p>';
        } else {
            echo '<p class="description">' . esc_html__('Entitlement enforcement is currently OFF, so these badges are informative until you enable plan gating.', 'wp-seo-next') . '</p>';
        }

        echo '<div style="display:flex;flex-wrap:wrap;gap:16px;align-items:stretch;">';

        foreach ([PlanMatrix::TIER_FREE, PlanMatrix::TIER_PRO, PlanMatrix::TIER_AGENCY] as $tierId) {
            $tier = isset($tiers[$tierId]) && is_array($tiers[$tierId]) ? $tiers[$tierId] : [];
            $label = isset($tier['label']) ? (string) $tier['label'] : ucfirst($tierId);
            $description = isset($tier['description']) ? (string) $tier['description'] : '';
            $features = isset($tier['features']) && is_array($tier['features']) ? $tier['features'] : [];
            $moduleIds = PlanMatrix::modulesForTier($tierId);

            $badgeHtml = '';
            if ($licensedPlan === $tierId) {
                $badgeHtml = '<span class="button" style="cursor:default;pointer-events:none;">' . esc_html__('Current tier', 'wp-seo-next') . '</span>';
            } elseif ($currentRank > PlanMatrix::tierRank($tierId)) {
                $badgeHtml = '<span class="button" style="cursor:default;pointer-events:none;">' . esc_html__('Included below current plan', 'wp-seo-next') . '</span>';
            } elseif ($currentRank < PlanMatrix::tierRank($tierId) && !empty($config['purchase_url'])) {
                $badgeHtml = '<a class="button button-primary" href="' . esc_url((string) $config['purchase_url']) . '" target="_blank" rel="noopener noreferrer">' . esc_html__('Upgrade', 'wp-seo-next') . '</a>';
            }

            echo '<div class="card" style="flex:1 1 280px;min-width:280px;max-width:360px;margin:0;">';
            echo '<h3 style="margin-top:0;">' . esc_html($label) . '</h3>';
            if ($badgeHtml !== '') {
                echo '<p style="margin:6px 0 12px;">' . wp_kses_post($badgeHtml) . '</p>';
            }
            if ($description !== '') {
                echo '<p>' . esc_html($description) . '</p>';
            }
            if (!empty($features)) {
                echo '<ul style="list-style:disc;padding-left:20px;">';
                foreach ($features as $feature) {
                    if (!is_scalar($feature) || trim((string) $feature) === '') {
                        continue;
                    }
                    echo '<li>' . esc_html((string) $feature) . '</li>';
                }
                echo '</ul>';
            }

            if (!empty($moduleIds)) {
                echo '<p style="margin-top:12px;"><strong>' . esc_html__('Modules in this tier', 'wp-seo-next') . '</strong></p>';
                echo '<p style="margin:0;line-height:1.7;">';

                $parts = [];
                foreach ($moduleIds as $moduleId) {
                    $labelText = isset($moduleNames[$moduleId]) ? $moduleNames[$moduleId] : $moduleId;
                    $url = admin_url('options-general.php?page=wp-seo-next&wpsn_focus_module=' . rawurlencode($moduleId));
                    $parts[] = '<a href="' . esc_url($url) . '">' . esc_html($labelText) . '</a>';
                }

                echo implode(', ', $parts);
                echo '</p>';
            } elseif ($tierId === PlanMatrix::TIER_FREE) {
                echo '<p class="description" style="margin-top:12px;">' . esc_html__('All core modules not explicitly marked Pro or Agency fall into the Free tier.', 'wp-seo-next') . '</p>';
            }

            echo '</div>';
        }

        echo '</div>';

        if ($buildTier !== $licensedPlan) {
            echo '<p class="description" style="margin-top:12px;">' . esc_html(sprintf(
                /* translators: 1: build tier label, 2: licensed tier label. */
                __('This site is running the %1$s plugin build, while the current verified tier resolves to %2$s.', 'wp-seo-next'),
                PlanMatrix::tierLabel($buildTier),
                PlanMatrix::tierLabel($licensedPlan)
            )) . '</p>';
        }
    }

    /**
     * @return array<string,string>
     */
    private function moduleLabelsById(): array
    {
        $labels = [];
        $modules = $this->container->get('modules');

        if ($modules instanceof \WPSN\Core\ModuleRegistry) {
            foreach ($modules->all() as $moduleId => $module) {
                $labels[(string) $moduleId] = $module->name();
            }
        }

        return $labels;
    }

    /**
     * @param array<string,mixed> $billing
     */
    private function renderBillingCenter(array $billing, string $status, bool $trusted, array $config): void
    {
        $actions = isset($billing['actions']) && is_array($billing['actions']) ? $billing['actions'] : [];
        $embed = isset($billing['embed']) && is_array($billing['embed']) ? $billing['embed'] : [];

        $syncCheckedAt = isset($billing['sync_checked_at']) ? (int) $billing['sync_checked_at'] : 0;
        $syncHuman = $syncCheckedAt > 0 ? gmdate('Y-m-d H:i:s', $syncCheckedAt) . ' UTC' : __('Never', 'wp-seo-next');
        $syncLastError = isset($billing['sync_last_error']) ? (string) $billing['sync_last_error'] : '';
        $syncStatus = isset($billing['sync_status']) ? (string) $billing['sync_status'] : '';
        $syncTrusted = !empty($billing['sync_trusted']);
        $syncEnabled = $this->billingSync->isEnabled();

        echo '<h2>' . esc_html__('Billing Center', 'wp-seo-next') . '</h2>';
        echo '<p class="description">' . esc_html__('This view can pull billing/account data from a dedicated JSON sync endpoint, fall back to the verified license payload, and finally use manual URLs from Configuration.', 'wp-seo-next') . '</p>';

        echo '<p>' . wp_kses_post($this->renderStatusBadge($status, $trusted)) . '</p>';
        echo '<p style="margin: 12px 0;">';
        if ($syncEnabled) {
            echo '<a class="button" href="' . esc_url(wp_nonce_url(admin_url('admin-post.php?action=wpsn_billing_sync'), 'wpsn_billing_sync')) . '">' . esc_html__('Sync billing now', 'wp-seo-next') . '</a> ';
            echo '<a class="button" href="' . esc_url(wp_nonce_url(admin_url('admin-post.php?action=wpsn_billing_clear_cache'), 'wpsn_billing_clear_cache')) . '">' . esc_html__('Clear billing cache', 'wp-seo-next') . '</a> ';
        }
        echo '<a class="button button-secondary" href="' . esc_url($this->pageUrl(['tab' => 'settings'])) . '">' . esc_html__('Open configuration', 'wp-seo-next') . '</a>';
        echo '</p>';

        if (!empty($config['self_serve_enabled'])) {
            $managementCache = $this->management->getCache();
            $this->renderSelfServeManagementSection($managementCache, $config);

            $catalogCache = $this->selfServe->getCatalogCache();
            $checkoutSession = $this->selfServe->getCheckoutSession();
            $this->renderSelfServeUpgradeSection($catalogCache, $checkoutSession, $config);
        }

        if ($syncEnabled || !empty($billing['source']) || $syncLastError !== '') {
            echo '<table class="widefat striped" style="max-width: 1100px; margin-top: 16px;">';
            echo '<tbody>';
            echo '<tr><th style="width:240px;">' . esc_html__('Data source', 'wp-seo-next') . '</th><td>' . esc_html($this->formatBillingSource((string) ($billing['source'] ?? 'manual'))) . '</td></tr>';
            if ($syncEnabled) {
                echo '<tr><th>' . esc_html__('Last billing sync', 'wp-seo-next') . '</th><td><code>' . esc_html($syncHuman) . '</code></td></tr>';
                echo '<tr><th>' . esc_html__('Billing sync trust', 'wp-seo-next') . '</th><td>' . esc_html($syncTrusted ? __('Signed', 'wp-seo-next') : __('Unsigned / not verified', 'wp-seo-next')) . '</td></tr>';
                echo '<tr><th>' . esc_html__('Billing sync status', 'wp-seo-next') . '</th><td>' . esc_html($syncStatus !== '' ? $syncStatus : '—') . '</td></tr>';
            }
            echo '</tbody>';
            echo '</table>';
        }

        if ($syncLastError !== '') {
            echo '<div class="notice notice-warning inline"><p><strong>' . esc_html__('Last billing sync error:', 'wp-seo-next') . '</strong> ' . esc_html($syncLastError) . '</p></div>';
        }

        if (empty($billing['has_data'])) {
            echo '<div class="notice notice-info inline"><p>' . esc_html__('No billing portal, invoices, or subscription metadata were found yet. Add Billing Center URLs in Configuration, return billing data from your license verifier payload, or enable Billing JSON sync.', 'wp-seo-next') . '</p></div>';
            return;
        }

        $this->renderBillingActionButtons($actions);

        echo '<table class="widefat striped" style="max-width: 1100px; margin-top: 16px;">';
        echo '<tbody>';
        echo '<tr><th style="width:240px;">' . esc_html__('Plan', 'wp-seo-next') . '</th><td>' . esc_html((string) (($billing['plan'] ?? '') !== '' ? $billing['plan'] : '—')) . '</td></tr>';
        echo '<tr><th>' . esc_html__('Subscription ID', 'wp-seo-next') . '</th><td>' . esc_html((string) (($billing['subscription_id'] ?? '') !== '' ? $billing['subscription_id'] : '—')) . '</td></tr>';
        echo '<tr><th>' . esc_html__('Customer', 'wp-seo-next') . '</th><td>' . esc_html($this->formatCustomer($billing)) . '</td></tr>';
        echo '<tr><th>' . esc_html__('Renews / expires', 'wp-seo-next') . '</th><td>' . esc_html((string) (($billing['expires_at'] ?? '') !== '' ? $billing['expires_at'] : '—')) . '</td></tr>';
        echo '<tr><th>' . esc_html__('Seats', 'wp-seo-next') . '</th><td>' . esc_html($this->formatUsage($billing['seat_used'] ?? null, $billing['seat_limit'] ?? null)) . '</td></tr>';
        echo '<tr><th>' . esc_html__('Sites / domains', 'wp-seo-next') . '</th><td>' . esc_html($this->formatUsage($billing['site_count'] ?? null, $billing['site_limit'] ?? null)) . '</td></tr>';
        echo '</tbody>';
        echo '</table>';

        if (!empty($billing['domains']) && is_array($billing['domains'])) {
            echo '<h3 style="margin-top:18px;">' . esc_html__('Connected domains', 'wp-seo-next') . '</h3>';
            echo '<ul style="list-style:disc; padding-left: 20px;">';
            foreach ($billing['domains'] as $domain) {
                echo '<li><code>' . esc_html((string) $domain) . '</code></li>';
            }
            echo '</ul>';
        }

        if (!empty($billing['invoices']) && is_array($billing['invoices'])) {
            echo '<h3 style="margin-top:18px;">' . esc_html__('Invoices', 'wp-seo-next') . '</h3>';
            echo '<table class="widefat striped" style="max-width: 1100px;">';
            echo '<thead><tr>';
            echo '<th>' . esc_html__('Invoice', 'wp-seo-next') . '</th>';
            echo '<th style="width:180px;">' . esc_html__('Date', 'wp-seo-next') . '</th>';
            echo '<th style="width:140px;">' . esc_html__('Status', 'wp-seo-next') . '</th>';
            echo '<th style="width:160px;">' . esc_html__('Amount', 'wp-seo-next') . '</th>';
            echo '<th style="width:180px;">' . esc_html__('Download', 'wp-seo-next') . '</th>';
            echo '</tr></thead><tbody>';
            foreach ($billing['invoices'] as $invoice) {
                if (!is_array($invoice)) {
                    continue;
                }
                $number = (string) ($invoice['number'] ?? $invoice['id'] ?? '');
                echo '<tr>';
                echo '<td><code>' . esc_html($number !== '' ? $number : '—') . '</code></td>';
                echo '<td>' . esc_html((string) (($invoice['date'] ?? '') !== '' ? $invoice['date'] : '—')) . '</td>';
                echo '<td>' . esc_html((string) (($invoice['status'] ?? '') !== '' ? $invoice['status'] : '—')) . '</td>';
                echo '<td>' . esc_html((string) (($invoice['amount'] ?? '') !== '' ? $invoice['amount'] : '—')) . '</td>';
                echo '<td>';
                if (!empty($invoice['download_url'])) {
                    echo '<a class="button button-small" href="' . esc_url((string) $invoice['download_url']) . '" target="_blank" rel="noopener noreferrer">' . esc_html__('Download', 'wp-seo-next') . '</a>';
                } else {
                    echo '—';
                }
                echo '</td>';
                echo '</tr>';
            }
            echo '</tbody></table>';
        }

        if (!empty($embed['enabled'])) {
            echo '<h3 style="margin-top:18px;">' . esc_html__('Embedded portal', 'wp-seo-next') . '</h3>';
            if (!empty($embed['can_embed']) && !empty($embed['url'])) {
                echo '<iframe src="' . esc_url((string) $embed['url']) . '" style="width:100%; min-height:' . esc_attr((string) $embed['height']) . 'px; border:1px solid #dcdcde; border-radius: 6px; background:#fff;" loading="lazy" sandbox="allow-scripts allow-forms allow-same-origin allow-popups allow-downloads"></iframe>';
            } else {
                $reason = (string) ($embed['reason'] ?? '');
                $message = __('The embedded portal could not be shown.', 'wp-seo-next');
                if ($reason === 'disabled') {
                    $message = __('Portal embedding is disabled in Configuration.', 'wp-seo-next');
                } elseif ($reason === 'missing_url') {
                    $message = __('No portal URL is available for embedding yet.', 'wp-seo-next');
                } elseif ($reason === 'invalid_url') {
                    $message = __('The portal URL is invalid.', 'wp-seo-next');
                } elseif ($reason === 'host_not_allowlisted') {
                    $message = __('The portal host is not on the embed allowlist. Add it in Configuration or use the external action buttons instead.', 'wp-seo-next');
                }
                echo '<div class="notice notice-warning inline"><p>' . esc_html($message) . '</p></div>';
            }
        }
    }

    /**
     * @param array<string,mixed> $config
     */
    private function renderSettings(array $config, bool $locked): void
    {
        $allowHosts = $config['billing_embed_allowed_hosts'] ?? [];
        if (!is_array($allowHosts)) {
            $allowHosts = [];
        }
        $allowHostsText = implode("\n", array_map('strval', $allowHosts));

        echo '<h2>' . esc_html__('Configuration', 'wp-seo-next') . '</h2>';
        echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
        echo '<input type="hidden" name="action" value="wpsn_license_save" />';
        echo '<input type="hidden" name="return_tab" value="settings" />';
        wp_nonce_field('wpsn_license_save');

        echo '<table class="form-table" role="presentation" style="max-width: 980px;">';

        echo '<tr><th scope="row">' . esc_html__('License key', 'wp-seo-next') . '</th><td>';
        echo '<input type="password" class="regular-text" name="license_key" value="' . esc_attr($this->service->getLicenseKey()) . '" autocomplete="off" />';
        echo '<p class="description">' . esc_html__('Stored as a secret (not exported by Config Sync).', 'wp-seo-next') . '</p>';
        echo '</td></tr>';

        echo '<tr><th scope="row">' . esc_html__('License server URL', 'wp-seo-next') . '</th><td>';
        echo '<input type="url" class="regular-text" name="server_url" value="' . esc_attr((string) $config['server_url']) . '" ' . disabled($locked, true, false) . ' />';
        echo '<p class="description">' . esc_html__('Example: https://license.example.com', 'wp-seo-next') . '</p>';
        echo '</td></tr>';

        echo '<tr><th scope="row">' . esc_html__('Verify endpoint path', 'wp-seo-next') . '</th><td>';
        echo '<input type="text" class="regular-text" name="verify_path" value="' . esc_attr((string) $config['verify_path']) . '" ' . disabled($locked, true, false) . ' />';
        echo '<p class="description">' . esc_html__('Appended to the server URL. Default: /wp-seo-next/license/verify', 'wp-seo-next') . '</p>';
        echo '</td></tr>';

        echo '<tr><th scope="row">' . esc_html__('Response public key (PEM)', 'wp-seo-next') . '</th><td>';
        echo '<textarea class="large-text code" rows="8" name="public_key_pem" ' . disabled($locked, true, false) . '>' . esc_textarea((string) ($config['public_key_pem'] ?? '')) . '</textarea>';
        echo '<p class="description">' . esc_html__('Used to verify signed license, billing, self-serve and update payloads. You can paste a single PEM block or a JSON object keyed by kid.', 'wp-seo-next') . '</p>';
        echo '</td></tr>';

        echo '<tr><th scope="row">' . esc_html__('Update server URL', 'wp-seo-next') . '</th><td>';
        echo '<input type="url" class="regular-text" name="update_server_url" value="' . esc_attr((string) $config['update_server_url']) . '" ' . disabled($locked, true, false) . ' />';
        echo '<p class="description">' . esc_html__('Optional update host for paid builds.', 'wp-seo-next') . '</p>';
        echo '</td></tr>';

        echo '<tr><th scope="row">' . esc_html__('Update endpoint path', 'wp-seo-next') . '</th><td>';
        echo '<input type="text" class="regular-text" name="update_path" value="' . esc_attr((string) $config['update_path']) . '" ' . disabled($locked, true, false) . ' />';
        echo '<p class="description">' . esc_html__('Appended to the update server URL. Default: /wp-seo-next/plugin/update', 'wp-seo-next') . '</p>';
        echo '</td></tr>';

        echo '<tr><th scope="row">' . esc_html__('Plugin updates', 'wp-seo-next') . '</th><td>';
        echo '<label><input type="checkbox" name="enable_updates" value="1" ' . checked(!empty($config['enable_updates']), true, false) . ' ' . disabled($locked, true, false) . ' /> ' . esc_html__('Enable paid-build update checks', 'wp-seo-next') . '</label>';
        echo '</td></tr>';

        echo '<tr><th scope="row">' . esc_html__('Purchase / upgrade URL', 'wp-seo-next') . '</th><td>';
        echo '<input type="url" class="regular-text" name="purchase_url" value="' . esc_attr((string) $config['purchase_url']) . '" ' . disabled($locked, true, false) . ' />';
        echo '<p class="description">' . esc_html__('Shown as the main “Upgrade / Manage subscription” button. Optional.', 'wp-seo-next') . '</p>';
        echo '</td></tr>';

        echo '<tr><th scope="row" colspan="2"><h3 style="margin:0;">' . esc_html__('Billing Center fallbacks', 'wp-seo-next') . '</h3></th></tr>';

        echo '<tr><th scope="row">' . esc_html__('Billing portal URL', 'wp-seo-next') . '</th><td>';
        echo '<input type="url" class="regular-text" name="billing_portal_url" value="' . esc_attr((string) $config['billing_portal_url']) . '" ' . disabled($locked, true, false) . ' />';
        echo '<p class="description">' . esc_html__('Optional portal to embed or open from wp-admin. Manual settings are used when the license payload does not provide a portal URL.', 'wp-seo-next') . '</p>';
        echo '</td></tr>';

        echo '<tr><th scope="row">' . esc_html__('Account portal URL', 'wp-seo-next') . '</th><td>';
        echo '<input type="url" class="regular-text" name="account_portal_url" value="' . esc_attr((string) $config['account_portal_url']) . '" ' . disabled($locked, true, false) . ' />';
        echo '</td></tr>';

        echo '<tr><th scope="row">' . esc_html__('Invoices URL', 'wp-seo-next') . '</th><td>';
        echo '<input type="url" class="regular-text" name="invoices_url" value="' . esc_attr((string) $config['invoices_url']) . '" ' . disabled($locked, true, false) . ' />';
        echo '</td></tr>';

        echo '<tr><th scope="row">' . esc_html__('Change plan URL', 'wp-seo-next') . '</th><td>';
        echo '<input type="url" class="regular-text" name="change_plan_url" value="' . esc_attr((string) $config['change_plan_url']) . '" ' . disabled($locked, true, false) . ' />';
        echo '</td></tr>';

        echo '<tr><th scope="row">' . esc_html__('Cancel subscription URL', 'wp-seo-next') . '</th><td>';
        echo '<input type="url" class="regular-text" name="cancel_url" value="' . esc_attr((string) $config['cancel_url']) . '" ' . disabled($locked, true, false) . ' />';
        echo '</td></tr>';

        echo '<tr><th scope="row">' . esc_html__('Payment method URL', 'wp-seo-next') . '</th><td>';
        echo '<input type="url" class="regular-text" name="payment_method_url" value="' . esc_attr((string) $config['payment_method_url']) . '" ' . disabled($locked, true, false) . ' />';
        echo '</td></tr>';

        echo '<tr><th scope="row">' . esc_html__('Billing support URL', 'wp-seo-next') . '</th><td>';
        echo '<input type="url" class="regular-text" name="billing_support_url" value="' . esc_attr((string) $config['billing_support_url']) . '" ' . disabled($locked, true, false) . ' />';
        echo '</td></tr>';

        echo '<tr><th scope="row">' . esc_html__('Embed portal inside wp-admin', 'wp-seo-next') . '</th><td>';
        echo '<label><input type="checkbox" name="billing_embed" value="1" ' . checked(!empty($config['billing_embed']), true, false) . ' ' . disabled($locked, true, false) . ' /> ' . esc_html__('Render the billing or account portal in an iframe inside the Billing Center tab when allowed.', 'wp-seo-next') . '</label>';
        echo '</td></tr>';

        echo '<tr><th scope="row">' . esc_html__('Embedded portal height', 'wp-seo-next') . '</th><td>';
        echo '<input type="number" min="480" max="2000" name="billing_embed_height" value="' . esc_attr((string) $config['billing_embed_height']) . '" ' . disabled($locked, true, false) . ' />';
        echo '</td></tr>';

        echo '<tr><th scope="row">' . esc_html__('Embedded portal host allowlist', 'wp-seo-next') . '</th><td>';
        echo '<textarea name="billing_embed_allowed_hosts" rows="5" cols="40" class="large-text code" ' . disabled($locked, true, false) . '>' . esc_textarea($allowHostsText) . '</textarea>';
        echo '<p class="description">' . esc_html__('One host per line (or comma-separated). Manual portal URLs are always allowed; payload-driven portals must match this allowlist or the license server host before they can be embedded.', 'wp-seo-next') . '</p>';
        echo '</td></tr>';

        echo '<tr><th scope="row" colspan="2"><h3 style="margin:0;">' . esc_html__('Billing JSON sync', 'wp-seo-next') . '</h3></th></tr>';

        echo '<tr><th scope="row">' . esc_html__('Enable billing sync', 'wp-seo-next') . '</th><td>';
        echo '<label><input type="checkbox" name="billing_sync_enabled" value="1" ' . checked(!empty($config['billing_sync_enabled']), true, false) . ' ' . disabled($locked, true, false) . ' /> ' . esc_html__('Fetch billing metadata from a dedicated JSON endpoint.', 'wp-seo-next') . '</label>';
        echo '<p class="description">' . esc_html__('Useful when billing/account data lives outside the main license verify payload.', 'wp-seo-next') . '</p>';
        echo '</td></tr>';

        echo '<tr><th scope="row">' . esc_html__('Billing sync endpoint path', 'wp-seo-next') . '</th><td>';
        echo '<input type="text" class="regular-text" name="billing_sync_path" value="' . esc_attr((string) $config['billing_sync_path']) . '" ' . disabled($locked, true, false) . ' />';
        echo '<p class="description">' . esc_html__('Appended to the license server URL. Default: /wp-seo-next/billing/sync', 'wp-seo-next') . '</p>';
        echo '</td></tr>';

        echo '<tr><th scope="row">' . esc_html__('Billing sync timeout (seconds)', 'wp-seo-next') . '</th><td>';
        echo '<input type="number" min="3" max="60" name="billing_sync_timeout" value="' . esc_attr((string) $config['billing_sync_timeout']) . '" ' . disabled($locked, true, false) . ' />';
        echo '</td></tr>';

        echo '<tr><th scope="row">' . esc_html__('Billing sync cache TTL (hours)', 'wp-seo-next') . '</th><td>';
        echo '<input type="number" min="1" max="168" name="billing_sync_cache_ttl_hours" value="' . esc_attr((string) $config['billing_sync_cache_ttl_hours']) . '" ' . disabled($locked, true, false) . ' />';
        echo '<p class="description">' . esc_html__('How long to reuse a successful billing sync response before syncing again.', 'wp-seo-next') . '</p>';
        echo '</td></tr>';

        echo '<tr><th scope="row">' . esc_html__('Scheduled billing sync', 'wp-seo-next') . '</th><td>';
        echo '<label><input type="checkbox" name="billing_sync_scheduled" value="1" ' . checked(!empty($config['billing_sync_scheduled']), true, false) . ' ' . disabled($locked, true, false) . ' /> ' . esc_html__('Sync billing data via WP-Cron (twice daily)', 'wp-seo-next') . '</label>';
        echo '</td></tr>';

        echo '<tr><th scope="row">' . esc_html__('Require signed billing payload', 'wp-seo-next') . '</th><td>';
        echo '<label><input type="checkbox" name="billing_sync_require_signed" value="1" ' . checked(!empty($config['billing_sync_require_signed']), true, false) . ' ' . disabled($locked, true, false) . ' /> ' . esc_html__('Only accept signed billing sync responses', 'wp-seo-next') . '</label>';
        echo '</td></tr>';

        echo '<tr><th scope="row" colspan="2"><h3 style="margin:0;">' . esc_html__('Self-serve billing flow', 'wp-seo-next') . '</h3></th></tr>';

        echo '<tr><th scope="row">' . esc_html__('Enable self-serve flow', 'wp-seo-next') . '</th><td>';
        echo '<label><input type="checkbox" name="self_serve_enabled" value="1" ' . checked(!empty($config['self_serve_enabled']), true, false) . ' ' . disabled($locked, true, false) . ' /> ' . esc_html__('Allow plan selection and checkout directly inside wp-admin.', 'wp-seo-next') . '</label>';
        echo '<p class="description">' . esc_html__('This uses dedicated JSON endpoints to fetch the current plan catalog, start a hosted checkout session, and perform supported billing actions without relying only on an external portal.', 'wp-seo-next') . '</p>';
        echo '</td></tr>';

        echo '<tr><th scope="row">' . esc_html__('Catalog endpoint path', 'wp-seo-next') . '</th><td>';
        echo '<input type="text" class="regular-text" name="self_serve_catalog_path" value="' . esc_attr((string) $config['self_serve_catalog_path']) . '" ' . disabled($locked, true, false) . ' />';
        echo '<p class="description">' . esc_html__('Appended to the license server URL. Default: /wp-seo-next/billing/catalog', 'wp-seo-next') . '</p>';
        echo '</td></tr>';

        echo '<tr><th scope="row">' . esc_html__('Checkout endpoint path', 'wp-seo-next') . '</th><td>';
        echo '<input type="text" class="regular-text" name="self_serve_checkout_path" value="' . esc_attr((string) $config['self_serve_checkout_path']) . '" ' . disabled($locked, true, false) . ' />';
        echo '<p class="description">' . esc_html__('Appended to the license server URL. Default: /wp-seo-next/billing/checkout', 'wp-seo-next') . '</p>';
        echo '</td></tr>';

        echo '<tr><th scope="row">' . esc_html__('Billing action endpoint path', 'wp-seo-next') . '</th><td>';
        echo '<input type="text" class="regular-text" name="self_serve_action_path" value="' . esc_attr((string) $config['self_serve_action_path']) . '" ' . disabled($locked, true, false) . ' />';
        echo '<p class="description">' . esc_html__('Used for actions like cancel, resume, or change-plan when your backend supports them. Default: /wp-seo-next/billing/action', 'wp-seo-next') . '</p>';
        echo '</td></tr>';

        echo '<tr><th scope="row">' . esc_html__('Management state endpoint path', 'wp-seo-next') . '</th><td>';
        echo '<input type="text" class="regular-text" name="self_serve_manage_path" value="' . esc_attr((string) $config['self_serve_manage_path']) . '" ' . disabled($locked, true, false) . ' />';
        echo '<p class="description">' . esc_html__('Appended to the license server URL. Default: /wp-seo-next/billing/manage', 'wp-seo-next') . '</p>';
        echo '</td></tr>';

        echo '<tr><th scope="row">' . esc_html__('Self-serve timeout (seconds)', 'wp-seo-next') . '</th><td>';
        echo '<input type="number" min="3" max="60" name="self_serve_timeout" value="' . esc_attr((string) $config['self_serve_timeout']) . '" ' . disabled($locked, true, false) . ' />';
        echo '</td></tr>';

        echo '<tr><th scope="row">' . esc_html__('Self-serve cache TTL (hours)', 'wp-seo-next') . '</th><td>';
        echo '<input type="number" min="1" max="168" name="self_serve_cache_ttl_hours" value="' . esc_attr((string) $config['self_serve_cache_ttl_hours']) . '" ' . disabled($locked, true, false) . ' />';
        echo '<p class="description">' . esc_html__('How long to reuse the last fetched plan catalog before refreshing it again.', 'wp-seo-next') . '</p>';
        echo '</td></tr>';

        echo '<tr><th scope="row">' . esc_html__('Require signed self-serve payloads', 'wp-seo-next') . '</th><td>';
        echo '<label><input type="checkbox" name="self_serve_require_signed" value="1" ' . checked(!empty($config['self_serve_require_signed']), true, false) . ' ' . disabled($locked, true, false) . ' /> ' . esc_html__('Only accept signed catalog / checkout / action payloads', 'wp-seo-next') . '</label>';
        echo '</td></tr>';

        echo '<tr><th scope="row" colspan="2"><h3 style="margin:0;">' . esc_html__('Verification behavior', 'wp-seo-next') . '</h3></th></tr>';

        echo '<tr><th scope="row">' . esc_html__('Cache TTL (hours)', 'wp-seo-next') . '</th><td>';
        echo '<input type="number" min="1" max="168" name="cache_ttl_hours" value="' . esc_attr((string) $config['cache_ttl_hours']) . '" ' . disabled($locked, true, false) . ' />';
        echo '<p class="description">' . esc_html__('How long to reuse a successful verification result.', 'wp-seo-next') . '</p>';
        echo '</td></tr>';

        echo '<tr><th scope="row">' . esc_html__('Grace period (days)', 'wp-seo-next') . '</th><td>';
        echo '<input type="number" min="0" max="60" name="grace_days" value="' . esc_attr((string) $config['grace_days']) . '" ' . disabled($locked, true, false) . ' />';
        echo '<p class="description">' . esc_html__('If the server can’t be reached, keep previously active entitlements for this many days.', 'wp-seo-next') . '</p>';
        echo '</td></tr>';

        echo '<tr><th scope="row">' . esc_html__('Scheduled verification', 'wp-seo-next') . '</th><td>';
        echo '<label><input type="checkbox" name="scheduled_verify" value="1" ' . checked(!empty($config['scheduled_verify']), true, false) . ' ' . disabled($locked, true, false) . ' /> ' . esc_html__('Verify via WP-Cron (twice daily)', 'wp-seo-next') . '</label>';
        echo '</td></tr>';

        echo '<tr><th scope="row">' . esc_html__('Module locking', 'wp-seo-next') . '</th><td>';
        echo '<label><input type="checkbox" name="enforce_entitlements" value="1" ' . checked(!empty($config['enforce_entitlements']), true, false) . ' ' . disabled($locked, true, false) . ' /> ' . esc_html__('Enforce entitlements (lock premium modules)', 'wp-seo-next') . '</label>';
        echo '<p class="description">' . esc_html__('If enabled, modules mapped as premium will be disabled unless the license is active and entitled.', 'wp-seo-next') . '</p>';
        echo '</td></tr>';

        echo '<tr><th scope="row">' . esc_html__('Require signed payload', 'wp-seo-next') . '</th><td>';
        echo '<label><input type="checkbox" name="require_signed" value="1" ' . checked(!empty($config['require_signed']), true, false) . ' ' . disabled($locked, true, false) . ' /> ' . esc_html__('Only accept signed license payloads', 'wp-seo-next') . '</label>';
        echo '<p class="description">' . esc_html__('Recommended for production. In development you can disable it and accept unsigned responses via filters.', 'wp-seo-next') . '</p>';
        echo '</td></tr>';

        echo '</table>';

        submit_button(__('Save license settings', 'wp-seo-next'));
        echo '</form>';

        echo '<p>';
        echo '<a class="button button-secondary" href="' . esc_url(wp_nonce_url(admin_url('admin-post.php?action=wpsn_license_clear'), 'wpsn_license_clear')) . '">' . esc_html__('Clear license key', 'wp-seo-next') . '</a>';
        echo '</p>';
    }

    /**
     * @param array<string,mixed>|null $catalogCache
     * @param array<string,mixed>|null $checkoutSession
     * @param array<string,mixed> $config
     */
    private function renderSelfServeUpgradeSection(?array $catalogCache, ?array $checkoutSession, array $config): void
    {
        $catalog = $catalogCache && isset($catalogCache['catalog']) && is_array($catalogCache['catalog']) ? $catalogCache['catalog'] : [];
        $plans = isset($catalog['plans']) && is_array($catalog['plans']) ? $catalog['plans'] : [];
        $status = $catalogCache && isset($catalogCache['status']) ? (string) $catalogCache['status'] : '';
        $checkedAt = $catalogCache && isset($catalogCache['checked_at']) ? (int) $catalogCache['checked_at'] : 0;
        $checkedHuman = $checkedAt > 0 ? gmdate('Y-m-d H:i:s', $checkedAt) . ' UTC' : __('Never', 'wp-seo-next');
        $lastError = $catalogCache && isset($catalogCache['last_error']) ? (string) $catalogCache['last_error'] : '';
        $trusted = !empty($catalogCache['trusted']);
        $currentPlan = isset($catalog['current_plan']) ? sanitize_key((string) $catalog['current_plan']) : '';
        $actions = $this->extractSelfServeActions($catalog);

        echo '<hr />';
        echo '<h3>' . esc_html__('Self-serve upgrades inside WP', 'wp-seo-next') . '</h3>';
        echo '<p class="description">' . esc_html__('Fetch the current plan catalog, start a hosted checkout session, and keep the customer inside wp-admin instead of relying only on an external billing portal.', 'wp-seo-next') . '</p>';

        echo '<p style="margin:12px 0;">';
        echo '<a class="button button-primary" href="' . esc_url(wp_nonce_url(admin_url('admin-post.php?action=wpsn_self_serve_catalog_refresh'), 'wpsn_self_serve_catalog_refresh')) . '">' . esc_html__('Refresh plans', 'wp-seo-next') . '</a> ';
        echo '<a class="button" href="' . esc_url(wp_nonce_url(admin_url('admin-post.php?action=wpsn_self_serve_catalog_clear'), 'wpsn_self_serve_catalog_clear')) . '">' . esc_html__('Clear plan cache', 'wp-seo-next') . '</a> ';
        if ($checkoutSession && !empty($checkoutSession['url'])) {
            echo '<a class="button" href="' . esc_url(wp_nonce_url(admin_url('admin-post.php?action=wpsn_self_serve_checkout_clear'), 'wpsn_self_serve_checkout_clear')) . '">' . esc_html__('Clear checkout session', 'wp-seo-next') . '</a>';
        }
        echo '</p>';

        if (!$this->selfServe->hasLicenseKey()) {
            echo '<div class="notice notice-warning inline"><p>' . esc_html__('Add and save a license key first. The self-serve flow uses it to fetch plan options and create a checkout session.', 'wp-seo-next') . '</p></div>';
            return;
        }

        echo '<table class="widefat striped" style="max-width: 1100px; margin-top: 8px;">';
        echo '<tbody>';
        echo '<tr><th style="width:240px;">' . esc_html__('Catalog status', 'wp-seo-next') . '</th><td>' . esc_html($status !== '' ? $status : __('Not fetched yet', 'wp-seo-next')) . '</td></tr>';
        echo '<tr><th>' . esc_html__('Catalog trust', 'wp-seo-next') . '</th><td>' . esc_html($trusted ? __('Signed', 'wp-seo-next') : __('Unsigned / not verified', 'wp-seo-next')) . '</td></tr>';
        echo '<tr><th>' . esc_html__('Last catalog refresh', 'wp-seo-next') . '</th><td><code>' . esc_html($checkedHuman) . '</code></td></tr>';
        echo '<tr><th>' . esc_html__('Current plan from catalog', 'wp-seo-next') . '</th><td>' . esc_html($currentPlan !== '' ? $currentPlan : '—') . '</td></tr>';
        echo '<tr><th>' . esc_html__('Catalog cache TTL', 'wp-seo-next') . '</th><td>' . esc_html(sprintf(__('%d hours', 'wp-seo-next'), (int) ($config['self_serve_cache_ttl_hours'] ?? 6))) . '</td></tr>';
        echo '</tbody>';
        echo '</table>';

        if ($lastError !== '') {
            echo '<div class="notice notice-warning inline"><p><strong>' . esc_html__('Last self-serve error:', 'wp-seo-next') . '</strong> ' . esc_html($lastError) . '</p></div>';
        }

        if (!empty($actions)) {
            echo '<h4 style="margin-top:18px;">' . esc_html__('Subscription actions', 'wp-seo-next') . '</h4>';
            echo '<div style="margin:10px 0;">';
            foreach ($actions as $action) {
                echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:inline-block; margin:0 8px 8px 0;">';
                echo '<input type="hidden" name="action" value="wpsn_self_serve_action" />';
                echo '<input type="hidden" name="self_serve_action_name" value="' . esc_attr($action) . '" />';
                echo '<input type="hidden" name="return_tab" value="billing" />';
                wp_nonce_field('wpsn_self_serve_action');
                submit_button($this->selfServeActionLabel($action), 'secondary small', '', false);
                echo '</form>';
            }
            echo '</div>';
        }

        echo '<h4 style="margin-top:18px;">' . esc_html__('Plan catalog', 'wp-seo-next') . '</h4>';
        if (empty($plans)) {
            echo '<div class="notice notice-info inline"><p>' . esc_html__('No plan catalog is cached yet. Click “Refresh plans” to fetch self-serve upgrade options from your licensing backend.', 'wp-seo-next') . '</p></div>';
        } else {
            echo '<div class="wpsn-self-serve-plans" style="display:flex; flex-wrap:wrap; gap:16px; align-items:stretch;">';
            foreach ($plans as $plan) {
                if (!is_array($plan)) {
                    continue;
                }
                $this->renderSelfServePlanCard($plan, $currentPlan);
            }
            echo '</div>';
        }

        if ($checkoutSession && !empty($checkoutSession['url'])) {
            $this->renderSelfServeCheckoutSession($checkoutSession);
        }
    }

    /**
     * @param array<string,mixed> $plan
     */
    private function renderSelfServePlanCard(array $plan, string $currentPlan): void
    {
        $planId = isset($plan['id']) ? sanitize_key((string) $plan['id']) : '';
        if ($planId === '') {
            return;
        }

        $label = isset($plan['label']) && trim((string) $plan['label']) !== '' ? (string) $plan['label'] : ucfirst($planId);
        $description = isset($plan['description']) ? trim((string) $plan['description']) : '';
        $price = isset($plan['price']) ? trim((string) $plan['price']) : '';
        $currency = isset($plan['currency']) ? strtoupper((string) $plan['currency']) : '';
        $interval = isset($plan['interval']) ? sanitize_key((string) $plan['interval']) : '';
        $recommended = !empty($plan['recommended']);
        $isCurrent = !empty($plan['current']) || ($currentPlan !== '' && $currentPlan === $planId);
        $features = isset($plan['features']) && is_array($plan['features']) ? $plan['features'] : [];

        $priceLabel = trim($price . ($currency !== '' ? ' ' . $currency : '') . ($interval !== '' ? ' / ' . $interval : ''));

        echo '<div class="card" style="flex:1 1 280px; max-width:360px; min-width:280px; margin:0;">';
        echo '<h4 style="margin-top:0;">' . esc_html($label) . '</h4>';
        echo '<p style="margin:6px 0 10px;">';
        if ($recommended) {
            echo '<span class="button button-primary" style="cursor:default; pointer-events:none; margin-right:6px;">' . esc_html__('Recommended', 'wp-seo-next') . '</span>';
        }
        if ($isCurrent) {
            echo '<span class="button" style="cursor:default; pointer-events:none;">' . esc_html__('Current plan', 'wp-seo-next') . '</span>';
        }
        echo '</p>';
        if ($priceLabel !== '') {
            echo '<p><strong>' . esc_html($priceLabel) . '</strong></p>';
        }
        if ($description !== '') {
            echo '<p>' . esc_html($description) . '</p>';
        }
        if (!empty($features)) {
            echo '<ul style="list-style:disc; padding-left:20px; margin:10px 0;">';
            foreach ($features as $feature) {
                if (!is_scalar($feature) || trim((string) $feature) === '') {
                    continue;
                }
                echo '<li>' . esc_html((string) $feature) . '</li>';
            }
            echo '</ul>';
        }

        if ($isCurrent) {
            echo '<p><button type="button" class="button" disabled="disabled">' . esc_html__('Current plan', 'wp-seo-next') . '</button></p>';
        } else {
            echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
            echo '<input type="hidden" name="action" value="wpsn_self_serve_checkout" />';
            echo '<input type="hidden" name="plan_id" value="' . esc_attr($planId) . '" />';
            echo '<input type="hidden" name="interval" value="' . esc_attr($interval) . '" />';
            echo '<input type="hidden" name="return_tab" value="billing" />';
            wp_nonce_field('wpsn_self_serve_checkout');
            echo '<p><label for="wpsn_coupon_' . esc_attr($planId) . '"><strong>' . esc_html__('Coupon', 'wp-seo-next') . '</strong></label><br />';
            echo '<input type="text" id="wpsn_coupon_' . esc_attr($planId) . '" name="coupon" class="regular-text" placeholder="WELCOME10" /></p>';
            submit_button($this->selfServeCheckoutLabel((string) ($plan['action'] ?? ''), $label), 'primary', '', false);
            echo '</form>';
        }

        echo '</div>';
    }

    /**
     * @param array<string,mixed> $checkoutSession
     */
    private function renderSelfServeCheckoutSession(array $checkoutSession): void
    {
        $url = isset($checkoutSession['url']) ? trim((string) $checkoutSession['url']) : '';
        if ($url === '') {
            return;
        }

        $sessionId = isset($checkoutSession['session_id']) ? trim((string) $checkoutSession['session_id']) : '';
        $planId = isset($checkoutSession['plan_id']) ? sanitize_key((string) $checkoutSession['plan_id']) : '';
        $trusted = !empty($checkoutSession['trusted']);
        $createdAt = isset($checkoutSession['created_at']) ? (int) $checkoutSession['created_at'] : 0;
        $createdHuman = $createdAt > 0 ? gmdate('Y-m-d H:i:s', $createdAt) . ' UTC' : __('Unknown', 'wp-seo-next');
        $message = isset($checkoutSession['message']) ? trim((string) $checkoutSession['message']) : '';
        $canEmbed = !empty($checkoutSession['can_embed']);
        $height = isset($checkoutSession['embed_height']) ? max(480, min(2000, (int) $checkoutSession['embed_height'])) : 980;
        $reason = isset($checkoutSession['embed_reason']) ? (string) $checkoutSession['embed_reason'] : '';

        echo '<h4 style="margin-top:24px;">' . esc_html__('Current checkout session', 'wp-seo-next') . '</h4>';
        echo '<table class="widefat striped" style="max-width: 1100px; margin-top: 8px;">';
        echo '<tbody>';
        echo '<tr><th style="width:240px;">' . esc_html__('Plan', 'wp-seo-next') . '</th><td>' . esc_html($planId !== '' ? $planId : '—') . '</td></tr>';
        echo '<tr><th>' . esc_html__('Session ID', 'wp-seo-next') . '</th><td>' . esc_html($sessionId !== '' ? $sessionId : '—') . '</td></tr>';
        echo '<tr><th>' . esc_html__('Created', 'wp-seo-next') . '</th><td><code>' . esc_html($createdHuman) . '</code></td></tr>';
        echo '<tr><th>' . esc_html__('Trust', 'wp-seo-next') . '</th><td>' . esc_html($trusted ? __('Signed', 'wp-seo-next') : __('Unsigned / not verified', 'wp-seo-next')) . '</td></tr>';
        echo '</tbody>';
        echo '</table>';

        if ($message !== '') {
            echo '<p class="description">' . esc_html($message) . '</p>';
        }

        echo '<p style="margin:12px 0;">';
        echo '<a class="button button-primary" href="' . esc_url($url) . '" target="_blank" rel="noopener noreferrer">' . esc_html__('Open checkout in a new tab', 'wp-seo-next') . '</a>';
        echo '</p>';

        if ($canEmbed) {
            echo '<iframe src="' . esc_url($url) . '" style="width:100%; min-height:' . esc_attr((string) $height) . 'px; border:1px solid #dcdcde; border-radius: 6px; background:#fff;" loading="lazy" sandbox="allow-scripts allow-forms allow-same-origin allow-popups allow-downloads"></iframe>';
            return;
        }

        $embedMessage = __('The hosted checkout could not be embedded here.', 'wp-seo-next');
        if ($reason === 'disabled') {
            $embedMessage = __('Checkout embedding is disabled in Configuration.', 'wp-seo-next');
        } elseif ($reason === 'missing_url') {
            $embedMessage = __('The checkout session did not include a usable URL.', 'wp-seo-next');
        } elseif ($reason === 'invalid_url') {
            $embedMessage = __('The checkout URL is invalid.', 'wp-seo-next');
        } elseif ($reason === 'host_not_allowlisted') {
            $embedMessage = __('The checkout host is not in the allowed embed hosts. Add it to the portal allowlist or let the backend return checkout_embed_allowed_hosts in the catalog payload.', 'wp-seo-next');
        }

        echo '<div class="notice notice-warning inline"><p>' . esc_html($embedMessage) . '</p></div>';
    }

    /**
     * @param array<string,mixed> $catalog
     * @return string[]
     */
    private function extractSelfServeActions(array $catalog): array
    {
        $raw = [];
        foreach (['available_actions', 'actions', 'subscription_actions'] as $key) {
            if (!isset($catalog[$key])) {
                continue;
            }
            $raw = $catalog[$key];
            break;
        }

        if (is_string($raw)) {
            $raw = preg_split('/[
,]+/', $raw) ?: [];
        }
        if (!is_array($raw)) {
            return [];
        }

        $allowed = ['cancel', 'resume', 'change_plan', 'upgrade', 'downgrade'];
        $out = [];
        foreach ($raw as $key => $value) {
            $candidate = '';
            if (is_string($key) && !is_numeric($key)) {
                $candidate = sanitize_key($key);
            } elseif (is_scalar($value)) {
                $candidate = sanitize_key((string) $value);
            }
            if ($candidate !== '' && in_array($candidate, $allowed, true)) {
                $out[] = $candidate;
            }
        }

        return array_values(array_unique($out));
    }

    private function selfServeActionLabel(string $action): string
    {
        switch ($action) {
            case 'cancel':
                return __('Cancel subscription', 'wp-seo-next');
            case 'resume':
                return __('Resume subscription', 'wp-seo-next');
            case 'change_plan':
                return __('Change plan', 'wp-seo-next');
            case 'upgrade':
                return __('Upgrade now', 'wp-seo-next');
            case 'downgrade':
                return __('Downgrade plan', 'wp-seo-next');
            default:
                return __('Run billing action', 'wp-seo-next');
        }
    }

    private function selfServeCheckoutLabel(string $action, string $label): string
    {
        $action = sanitize_key($action);
        if ($action === 'downgrade') {
            return sprintf(__('Switch to %s', 'wp-seo-next'), $label);
        }
        if (in_array($action, ['change_plan', 'upgrade'], true)) {
            return sprintf(__('Choose %s', 'wp-seo-next'), $label);
        }
        return sprintf(__('Start checkout for %s', 'wp-seo-next'), $label);
    }

    /**
     * @param array<string,mixed>|null $managementCache
     * @param array<string,mixed> $config
     */
    private function renderSelfServeManagementSection(?array $managementCache, array $config): void
    {
        $management = $managementCache && isset($managementCache['management']) && is_array($managementCache['management']) ? $managementCache['management'] : [];
        $customer = isset($management['customer']) && is_array($management['customer']) ? $management['customer'] : [];
        $subscription = isset($management['subscription']) && is_array($management['subscription']) ? $management['subscription'] : [];
        $paymentMethods = isset($management['payment_methods']) && is_array($management['payment_methods']) ? $management['payment_methods'] : [];
        $notes = isset($management['notes']) && is_array($management['notes']) ? $management['notes'] : [];
        $allowed = isset($management['actions']) && is_array($management['actions']) ? array_values(array_unique(array_filter(array_map('sanitize_key', $management['actions'])))) : [];

        $checkedAt = $managementCache && isset($managementCache['checked_at']) ? (int) $managementCache['checked_at'] : 0;
        $checkedHuman = $checkedAt > 0 ? gmdate('Y-m-d H:i:s', $checkedAt) . ' UTC' : __('Never', 'wp-seo-next');
        $lastError = $managementCache && isset($managementCache['last_error']) ? (string) $managementCache['last_error'] : '';
        $status = $managementCache && isset($managementCache['status']) ? (string) $managementCache['status'] : '';
        $trusted = !empty($managementCache['trusted']);
        $cacheTtlHours = (int) ($config['self_serve_cache_ttl_hours'] ?? 6);

        $plans = isset($management['plans']) && is_array($management['plans']) ? $management['plans'] : [];
        if (empty($plans)) {
            $catalogCache = $this->selfServe->getCatalogCache();
            if ($catalogCache && isset($catalogCache['catalog']['plans']) && is_array($catalogCache['catalog']['plans'])) {
                $plans = $catalogCache['catalog']['plans'];
            }
        }

        echo '<hr />';
        echo '<h3>' . esc_html__('In-admin subscription management API', 'wp-seo-next') . '</h3>';
        echo '<p class="description">' . esc_html__('Use dedicated JSON endpoints to refresh subscription state, update customer details, switch plans, adjust seat or site limits, cancel or resume a subscription, and manage saved payment methods directly inside wp-admin without relying on a hosted portal or iframe.', 'wp-seo-next') . '</p>';

        echo '<p style="margin:12px 0;">';
        echo '<a class="button button-primary" href="' . esc_url(wp_nonce_url(admin_url('admin-post.php?action=wpsn_self_serve_manage_refresh'), 'wpsn_self_serve_manage_refresh')) . '">' . esc_html__('Refresh subscription state', 'wp-seo-next') . '</a> ';
        echo '<a class="button" href="' . esc_url(wp_nonce_url(admin_url('admin-post.php?action=wpsn_self_serve_manage_clear'), 'wpsn_self_serve_manage_clear')) . '">' . esc_html__('Clear management cache', 'wp-seo-next') . '</a>';
        echo '</p>';

        if (!$this->management->hasLicenseKey()) {
            echo '<div class="notice notice-info inline"><p>' . esc_html__('Add and save a license key first to use the in-admin subscription management API.', 'wp-seo-next') . '</p></div>';
            return;
        }

        echo '<table class="widefat striped" style="max-width: 1100px; margin-top: 8px;">';
        echo '<tbody>';
        echo '<tr><th style="width:240px;">' . esc_html__('Management status', 'wp-seo-next') . '</th><td>' . esc_html($status !== '' ? $status : '—') . '</td></tr>';
        echo '<tr><th>' . esc_html__('Last refresh', 'wp-seo-next') . '</th><td><code>' . esc_html($checkedHuman) . '</code></td></tr>';
        echo '<tr><th>' . esc_html__('Trust', 'wp-seo-next') . '</th><td>' . esc_html($trusted ? __('Signed', 'wp-seo-next') : __('Unsigned / not verified', 'wp-seo-next')) . '</td></tr>';
        echo '<tr><th>' . esc_html__('Management cache TTL', 'wp-seo-next') . '</th><td>' . esc_html(sprintf(__('%d hours', 'wp-seo-next'), max(1, $cacheTtlHours))) . '</td></tr>';
        if (!empty($subscription['id'])) {
            echo '<tr><th>' . esc_html__('Subscription ID', 'wp-seo-next') . '</th><td><code>' . esc_html((string) $subscription['id']) . '</code></td></tr>';
        }
        if (!empty($subscription['plan_label']) || !empty($subscription['plan_id'])) {
            $planLabel = !empty($subscription['plan_label']) ? (string) $subscription['plan_label'] : (string) $subscription['plan_id'];
            echo '<tr><th>' . esc_html__('Current plan', 'wp-seo-next') . '</th><td>' . esc_html($planLabel) . '</td></tr>';
        }
        if (!empty($subscription['status'])) {
            echo '<tr><th>' . esc_html__('Subscription state', 'wp-seo-next') . '</th><td>' . esc_html((string) $subscription['status']) . '</td></tr>';
        }
        if (!empty($subscription['renews_at'])) {
            echo '<tr><th>' . esc_html__('Renews / changes on', 'wp-seo-next') . '</th><td><code>' . esc_html((string) $subscription['renews_at']) . '</code></td></tr>';
        }
        if (isset($subscription['seat_limit']) || isset($subscription['site_limit'])) {
            echo '<tr><th>' . esc_html__('Managed limits', 'wp-seo-next') . '</th><td>' . esc_html(sprintf(__('Seats: %1$s · Sites: %2$s', 'wp-seo-next'), $this->formatLimitValue($subscription['seat_limit'] ?? null), $this->formatLimitValue($subscription['site_limit'] ?? null))) . '</td></tr>';
        }
        echo '</tbody>';
        echo '</table>';

        if ($lastError !== '') {
            echo '<div class="notice notice-warning inline"><p><strong>' . esc_html__('Last subscription management error:', 'wp-seo-next') . '</strong> ' . esc_html($lastError) . '</p></div>';
        }

        if (empty($management)) {
            echo '<div class="notice notice-info inline"><p>' . esc_html__('No subscription management payload has been cached yet. Refresh the subscription state to load editable customer, plan, usage, and payment method data from your licensing backend.', 'wp-seo-next') . '</p></div>';
            return;
        }

        echo '<div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 16px; margin-top:16px;">';

        echo '<div class="postbox" style="padding:16px; margin:0;">';
        echo '<h4 style="margin-top:0;">' . esc_html__('Customer details', 'wp-seo-next') . '</h4>';
        echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
        echo '<input type="hidden" name="action" value="wpsn_self_serve_manage_update_customer" />';
        wp_nonce_field('wpsn_self_serve_manage_update_customer');
        echo '<p><label>' . esc_html__('Customer name', 'wp-seo-next') . '<br /><input type="text" class="regular-text" name="customer_name" value="' . esc_attr((string) ($customer['name'] ?? '')) . '" /></label></p>';
        echo '<p><label>' . esc_html__('Customer email', 'wp-seo-next') . '<br /><input type="email" class="regular-text" name="customer_email" value="' . esc_attr((string) ($customer['email'] ?? '')) . '" /></label></p>';
        echo '<p><label>' . esc_html__('Company', 'wp-seo-next') . '<br /><input type="text" class="regular-text" name="company" value="' . esc_attr((string) ($customer['company'] ?? '')) . '" /></label></p>';
        echo '<p><label>' . esc_html__('VAT / tax ID', 'wp-seo-next') . '<br /><input type="text" class="regular-text" name="vat_id" value="' . esc_attr((string) ($customer['vat_id'] ?? '')) . '" /></label></p>';
        echo '<p><button class="button button-primary" type="submit"' . disabled(!$this->managementActionAllowed($allowed, 'update_customer'), true, false) . '>' . esc_html__('Save customer details', 'wp-seo-next') . '</button></p>';
        echo '</form>';
        if (!$this->managementActionAllowed($allowed, 'update_customer')) {
            echo '<p class="description">' . esc_html__('The current backend payload did not explicitly allow customer updates.', 'wp-seo-next') . '</p>';
        }
        echo '</div>';

        echo '<div class="postbox" style="padding:16px; margin:0;">';
        echo '<h4 style="margin-top:0;">' . esc_html__('Change plan', 'wp-seo-next') . '</h4>';
        echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
        echo '<input type="hidden" name="action" value="wpsn_self_serve_manage_change_plan" />';
        wp_nonce_field('wpsn_self_serve_manage_change_plan');
        echo '<p><label>' . esc_html__('Plan', 'wp-seo-next') . '<br /><select name="plan_id" class="regular-text">';
        echo '<option value="">' . esc_html__('Select a plan', 'wp-seo-next') . '</option>';
        foreach ($plans as $plan) {
            if (!is_array($plan)) {
                continue;
            }
            $planId = isset($plan['id']) ? sanitize_key((string) $plan['id']) : '';
            if ($planId === '') {
                continue;
            }
            $label = isset($plan['label']) && (string) $plan['label'] !== '' ? (string) $plan['label'] : strtoupper($planId);
            $interval = isset($plan['interval']) ? sanitize_key((string) $plan['interval']) : '';
            $priceLabel = isset($plan['price_label']) ? trim((string) $plan['price_label']) : '';
            $suffix = [];
            if ($interval !== '') {
                $suffix[] = $interval;
            }
            if ($priceLabel !== '') {
                $suffix[] = $priceLabel;
            }
            if (!empty($plan['current']) || ((string) ($subscription['plan_id'] ?? '') !== '' && (string) ($subscription['plan_id'] ?? '') === $planId)) {
                $suffix[] = __('current', 'wp-seo-next');
            }
            $optionLabel = $label;
            if (!empty($suffix)) {
                $optionLabel .= ' — ' . implode(' · ', array_map('strval', $suffix));
            }
            echo '<option value="' . esc_attr($planId) . '" ' . selected((string) ($subscription['plan_id'] ?? ''), $planId, false) . '>' . esc_html($optionLabel) . '</option>';
        }
        echo '</select></label></p>';
        echo '<p><label>' . esc_html__('Interval', 'wp-seo-next') . '<br /><select name="interval" class="regular-text">';
        $currentInterval = isset($subscription['interval']) ? sanitize_key((string) $subscription['interval']) : '';
        $intervalOptions = array_values(array_unique(array_filter([$currentInterval, 'month', 'year', 'lifetime'])));
        echo '<option value="">' . esc_html__('Backend default', 'wp-seo-next') . '</option>';
        foreach ($intervalOptions as $intervalValue) {
            echo '<option value="' . esc_attr($intervalValue) . '" ' . selected($currentInterval, $intervalValue, false) . '>' . esc_html($intervalValue) . '</option>';
        }
        echo '</select></label></p>';
        echo '<p><label>' . esc_html__('Coupon', 'wp-seo-next') . '<br /><input type="text" class="regular-text" name="coupon" value="" /></label></p>';
        echo '<p><button class="button button-primary" type="submit"' . disabled(!$this->managementActionAllowed($allowed, 'change_plan'), true, false) . '>' . esc_html__('Apply plan change', 'wp-seo-next') . '</button></p>';
        echo '</form>';
        if (!$this->managementActionAllowed($allowed, 'change_plan')) {
            echo '<p class="description">' . esc_html__('The current backend payload did not explicitly allow plan changes.', 'wp-seo-next') . '</p>';
        }
        echo '</div>';

        echo '<div class="postbox" style="padding:16px; margin:0;">';
        echo '<h4 style="margin-top:0;">' . esc_html__('Usage limits', 'wp-seo-next') . '</h4>';
        echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
        echo '<input type="hidden" name="action" value="wpsn_self_serve_manage_usage" />';
        wp_nonce_field('wpsn_self_serve_manage_usage');
        echo '<p><label>' . esc_html__('Seat limit', 'wp-seo-next') . '<br /><input type="number" min="0" step="1" class="small-text" name="seat_limit" value="' . esc_attr(isset($subscription['seat_limit']) ? (string) $subscription['seat_limit'] : '') . '" /></label></p>';
        echo '<p><label>' . esc_html__('Site limit', 'wp-seo-next') . '<br /><input type="number" min="0" step="1" class="small-text" name="site_limit" value="' . esc_attr(isset($subscription['site_limit']) ? (string) $subscription['site_limit'] : '') . '" /></label></p>';
        echo '<p><button class="button button-primary" type="submit"' . disabled(!$this->managementActionAllowed($allowed, 'update_usage'), true, false) . '>' . esc_html__('Update limits', 'wp-seo-next') . '</button></p>';
        echo '</form>';
        if (!$this->managementActionAllowed($allowed, 'update_usage')) {
            echo '<p class="description">' . esc_html__('The current backend payload did not explicitly allow usage-limit updates.', 'wp-seo-next') . '</p>';
        }
        echo '</div>';

        echo '<div class="postbox" style="padding:16px; margin:0;">';
        echo '<h4 style="margin-top:0;">' . esc_html__('Subscription actions', 'wp-seo-next') . '</h4>';
        echo '<p class="description">' . esc_html__('Use these operations when your backend supports immediate cancellation, cancellation at period end, or resuming a scheduled cancellation.', 'wp-seo-next') . '</p>';
        echo '<div style="display:flex; gap:8px; flex-wrap:wrap;">';
        $subscriptionOps = [
            'cancel_now' => __('Cancel now', 'wp-seo-next'),
            'cancel_at_period_end' => __('Cancel at period end', 'wp-seo-next'),
            'resume' => __('Resume subscription', 'wp-seo-next'),
        ];
        foreach ($subscriptionOps as $operation => $label) {
            echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin:0;">';
            echo '<input type="hidden" name="action" value="wpsn_self_serve_manage_subscription" />';
            echo '<input type="hidden" name="subscription_operation" value="' . esc_attr($operation) . '" />';
            wp_nonce_field('wpsn_self_serve_manage_subscription');
            echo '<button class="button" type="submit"' . disabled(!$this->managementActionAllowed($allowed, $operation), true, false) . '>' . esc_html($label) . '</button>';
            echo '</form>';
        }
        echo '</div>';
        echo '</div>';

        echo '</div>';

        $this->renderUsageAndSiteAssignmentsSection($management, $allowed);

        if (!empty($paymentMethods)) {
            echo '<h4 style="margin-top:20px;">' . esc_html__('Saved payment methods', 'wp-seo-next') . '</h4>';
            echo '<table class="widefat striped" style="max-width:1100px; margin-top:8px;">';
            echo '<thead><tr><th>' . esc_html__('Payment method', 'wp-seo-next') . '</th><th>' . esc_html__('Status', 'wp-seo-next') . '</th><th>' . esc_html__('Actions', 'wp-seo-next') . '</th></tr></thead>';
            echo '<tbody>';
            foreach ($paymentMethods as $method) {
                if (!is_array($method)) {
                    continue;
                }
                $methodId = isset($method['id']) ? (string) $method['id'] : '';
                if ($methodId === '') {
                    continue;
                }
                $parts = [];
                if (!empty($method['brand'])) {
                    $parts[] = (string) $method['brand'];
                }
                if (!empty($method['last4'])) {
                    $parts[] = '•••• ' . (string) $method['last4'];
                }
                if (!empty($method['exp_month']) && !empty($method['exp_year'])) {
                    $parts[] = sprintf('%s/%s', (string) $method['exp_month'], (string) $method['exp_year']);
                }
                if (empty($parts) && !empty($method['label'])) {
                    $parts[] = (string) $method['label'];
                }
                $label = !empty($parts) ? implode(' · ', $parts) : $methodId;
                $statusBits = [];
                if (!empty($method['default'])) {
                    $statusBits[] = __('Default', 'wp-seo-next');
                }
                if (!empty($method['status'])) {
                    $statusBits[] = (string) $method['status'];
                }
                echo '<tr>';
                echo '<td><code>' . esc_html($methodId) . '</code><br />' . esc_html($label) . '</td>';
                echo '<td>' . esc_html(!empty($statusBits) ? implode(' · ', $statusBits) : '—') . '</td>';
                echo '<td>';
                echo '<div style="display:flex; gap:8px; flex-wrap:wrap;">';
                echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin:0;">';
                echo '<input type="hidden" name="action" value="wpsn_self_serve_manage_payment_method" />';
                echo '<input type="hidden" name="payment_method_operation" value="set_default_payment_method" />';
                echo '<input type="hidden" name="payment_method_id" value="' . esc_attr($methodId) . '" />';
                wp_nonce_field('wpsn_self_serve_manage_payment_method');
                echo '<button class="button" type="submit"' . disabled(!$this->managementActionAllowed($allowed, 'set_default_payment_method'), true, false) . '>' . esc_html__('Set default', 'wp-seo-next') . '</button>';
                echo '</form>';

                echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin:0;">';
                echo '<input type="hidden" name="action" value="wpsn_self_serve_manage_payment_method" />';
                echo '<input type="hidden" name="payment_method_operation" value="remove_payment_method" />';
                echo '<input type="hidden" name="payment_method_id" value="' . esc_attr($methodId) . '" />';
                wp_nonce_field('wpsn_self_serve_manage_payment_method');
                echo '<button class="button" type="submit"' . disabled(!$this->managementActionAllowed($allowed, 'remove_payment_method'), true, false) . '>' . esc_html__('Remove', 'wp-seo-next') . '</button>';
                echo '</form>';
                echo '</div>';
                echo '</td>';
                echo '</tr>';
            }
            echo '</tbody>';
            echo '</table>';
        }

        if (!empty($notes)) {
            echo '<h4 style="margin-top:20px;">' . esc_html__('Backend notes', 'wp-seo-next') . '</h4>';
            echo '<ul style="list-style:disc; padding-left:20px;">';
            foreach ($notes as $note) {
                if (!is_scalar($note) || trim((string) $note) === '') {
                    continue;
                }
                echo '<li>' . esc_html((string) $note) . '</li>';
            }
            echo '</ul>';
        }
    }

    /**
     * @param array<string,mixed> $management
     * @param string[] $allowed
     */
    private function renderUsageAndSiteAssignmentsSection(array $management, array $allowed): void
    {
        $usage = isset($management['usage']) && is_array($management['usage']) ? $management['usage'] : [];
        $siteAssignments = isset($management['site_assignments']) && is_array($management['site_assignments']) ? $management['site_assignments'] : [];
        $seatAssignments = isset($management['seat_assignments']) && is_array($management['seat_assignments']) ? $management['seat_assignments'] : [];
        $usageEvents = isset($management['usage_events']) && is_array($management['usage_events']) ? $management['usage_events'] : [];

        $currentSiteUrl = home_url('/');
        $currentSiteHost = wp_parse_url($currentSiteUrl, PHP_URL_HOST);
        $currentSiteHost = is_string($currentSiteHost) ? strtolower($currentSiteHost) : '';
        $currentAssignment = null;
        foreach ($siteAssignments as $assignment) {
            if (is_array($assignment) && !empty($assignment['is_current_site'])) {
                $currentAssignment = $assignment;
                break;
            }
        }
        $currentAssignmentId = is_array($currentAssignment) && !empty($currentAssignment['id']) ? (string) $currentAssignment['id'] : '';

        $currentAdminEmail = (string) get_bloginfo('admin_email');
        $currentAdminName = '';
        if (function_exists('wp_get_current_user')) {
            $user = wp_get_current_user();
            if (is_object($user)) {
                if (isset($user->display_name) && is_string($user->display_name)) {
                    $currentAdminName = trim((string) $user->display_name);
                }
                if (isset($user->user_email) && is_string($user->user_email) && trim((string) $user->user_email) !== '') {
                    $currentAdminEmail = trim((string) $user->user_email);
                }
            }
        }

        $siteLabelMap = [];
        foreach ($siteAssignments as $assignment) {
            if (!is_array($assignment) || empty($assignment['id'])) {
                continue;
            }
            $siteLabelMap[(string) $assignment['id']] = $this->siteAssignmentChoiceLabel($assignment);
        }

        echo '<hr />';
        echo '<h4 style="margin-top:20px;">' . esc_html__('Usage, site slots, and seat assignment manager', 'wp-seo-next') . '</h4>';
        echo '<p class="description" style="margin:0 0 12px;">' . esc_html__('Manage which domains/sites consume subscription slots directly inside wp-admin. You can assign the current site, register additional domains, bind individual teammate seats to a site slot, edit seat metadata, release unused assignments, and review the usage history returned by the licensing backend.', 'wp-seo-next') . '</p>';

        if (!empty($usage)) {
            $seatUsed = isset($usage['seats']['used']) ? $usage['seats']['used'] : null;
            $seatLimit = isset($usage['seats']['limit']) ? $usage['seats']['limit'] : null;
            $seatAvail = isset($usage['seats']['available']) ? $usage['seats']['available'] : null;
            $siteUsed = isset($usage['sites']['used']) ? $usage['sites']['used'] : null;
            $siteLimit = isset($usage['sites']['limit']) ? $usage['sites']['limit'] : null;
            $siteAvail = isset($usage['sites']['available']) ? $usage['sites']['available'] : null;

            echo '<table class="widefat striped" style="max-width:1100px; margin-top:8px;">';
            echo '<thead><tr><th>' . esc_html__('Resource', 'wp-seo-next') . '</th><th>' . esc_html__('Used', 'wp-seo-next') . '</th><th>' . esc_html__('Limit', 'wp-seo-next') . '</th><th>' . esc_html__('Available', 'wp-seo-next') . '</th></tr></thead>';
            echo '<tbody>';
            echo '<tr><td>' . esc_html__('Seats', 'wp-seo-next') . '</td><td>' . esc_html($seatUsed !== null ? (string) $seatUsed : '—') . '</td><td>' . esc_html($seatLimit !== null ? (string) $seatLimit : '—') . '</td><td>' . esc_html($seatAvail !== null ? (string) $seatAvail : '—') . '</td></tr>';
            echo '<tr><td>' . esc_html__('Sites / domains', 'wp-seo-next') . '</td><td>' . esc_html($siteUsed !== null ? (string) $siteUsed : '—') . '</td><td>' . esc_html($siteLimit !== null ? (string) $siteLimit : '—') . '</td><td>' . esc_html($siteAvail !== null ? (string) $siteAvail : '—') . '</td></tr>';
            echo '</tbody></table>';
        }

        echo '<div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap:16px; margin-top:16px;">';

        echo '<div class="postbox" style="padding:16px; margin:0;">';
        echo '<h4 style="margin-top:0;">' . esc_html__('Assign current site', 'wp-seo-next') . '</h4>';
        if (is_array($currentAssignment)) {
            echo '<p class="description">' . esc_html__('This WordPress site already appears in the current assignment list. Use the row actions below to update or release it if needed.', 'wp-seo-next') . '</p>';
        } else {
            echo '<p class="description">' . esc_html__('Bind the current WordPress site to this subscription slot. Useful after migrations, staging-to-production moves, or first-time activation.', 'wp-seo-next') . '</p>';
        }
        echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
        echo '<input type="hidden" name="action" value="wpsn_self_serve_manage_site_assignment" />';
        echo '<input type="hidden" name="site_assignment_operation" value="assign_site" />';
        wp_nonce_field('wpsn_self_serve_manage_site_assignment');
        echo '<p><label>' . esc_html__('Current site URL', 'wp-seo-next') . '<br /><input type="text" class="regular-text" name="site_url" value="' . esc_attr($currentSiteUrl) . '" readonly="readonly" /></label></p>';
        echo '<p><label>' . esc_html__('Label', 'wp-seo-next') . '<br /><input type="text" class="regular-text" name="label" value="' . esc_attr(get_bloginfo('name')) . '" /></label></p>';
        echo '<p><label>' . esc_html__('Internal notes', 'wp-seo-next') . '<br /><textarea name="notes" rows="3" class="large-text" placeholder="' . esc_attr__('Production site / agency seat', 'wp-seo-next') . '"></textarea></label></p>';
        echo '<p><button class="button button-primary" type="submit"' . disabled(!$this->managementActionAllowed($allowed, 'assign_site'), true, false) . '>' . esc_html__('Assign current site', 'wp-seo-next') . '</button></p>';
        echo '</form>';
        if (!$this->managementActionAllowed($allowed, 'assign_site')) {
            echo '<p class="description">' . esc_html__('The current backend payload did not explicitly allow assigning sites.', 'wp-seo-next') . '</p>';
        }
        echo '</div>';

        echo '<div class="postbox" style="padding:16px; margin:0;">';
        echo '<h4 style="margin-top:0;">' . esc_html__('Assign another site / domain', 'wp-seo-next') . '</h4>';
        echo '<p class="description">' . esc_html__('Register another production domain, client site, or separate environment against this subscription. You can paste a full URL or just a hostname.', 'wp-seo-next') . '</p>';
        echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
        echo '<input type="hidden" name="action" value="wpsn_self_serve_manage_site_assignment" />';
        echo '<input type="hidden" name="site_assignment_operation" value="assign_site" />';
        wp_nonce_field('wpsn_self_serve_manage_site_assignment');
        echo '<p><label>' . esc_html__('Site URL or domain', 'wp-seo-next') . '<br /><input type="text" class="regular-text" name="site_url" value="" placeholder="https://client.example.com/" /></label></p>';
        echo '<p><label>' . esc_html__('Label', 'wp-seo-next') . '<br /><input type="text" class="regular-text" name="label" value="" placeholder="' . esc_attr__('Client site', 'wp-seo-next') . '" /></label></p>';
        echo '<p><label>' . esc_html__('Internal notes', 'wp-seo-next') . '<br /><textarea name="notes" rows="3" class="large-text" placeholder="' . esc_attr__('Optional notes for billing/support teams', 'wp-seo-next') . '"></textarea></label></p>';
        echo '<p><button class="button button-primary" type="submit"' . disabled(!$this->managementActionAllowed($allowed, 'assign_site'), true, false) . '>' . esc_html__('Assign site slot', 'wp-seo-next') . '</button></p>';
        echo '</form>';
        echo '</div>';

        echo '<div class="postbox" style="padding:16px; margin:0;">';
        echo '<h4 style="margin-top:0;">' . esc_html__('Assign current admin user', 'wp-seo-next') . '</h4>';
        echo '<p class="description">' . esc_html__('Reserve one seat for the current site owner/admin. This is useful when a subscription tracks team members separately from domains.', 'wp-seo-next') . '</p>';
        echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
        echo '<input type="hidden" name="action" value="wpsn_self_serve_manage_seat_assignment" />';
        echo '<input type="hidden" name="seat_assignment_operation" value="assign_seat" />';
        wp_nonce_field('wpsn_self_serve_manage_seat_assignment');
        echo '<p><label>' . esc_html__('Email', 'wp-seo-next') . '<br /><input type="email" class="regular-text" name="email" value="' . esc_attr($currentAdminEmail) . '" /></label></p>';
        echo '<p><label>' . esc_html__('Display name', 'wp-seo-next') . '<br /><input type="text" class="regular-text" name="name" value="' . esc_attr($currentAdminName) . '" placeholder="' . esc_attr__('Site owner', 'wp-seo-next') . '" /></label></p>';
        echo '<p><label>' . esc_html__('Seats consumed', 'wp-seo-next') . '<br /><input type="number" min="1" step="1" class="small-text" name="seat_count" value="1" /></label></p>';
        echo '<p><label>' . esc_html__('Link to site slot', 'wp-seo-next') . '<br />';
        $this->renderSiteAssignmentSelect('site_assignment_id', $currentAssignmentId, $siteAssignments, __('Not linked to a site slot', 'wp-seo-next'));
        echo '</label></p>';
        echo '<p><label>' . esc_html__('Role / label', 'wp-seo-next') . '<br /><input type="text" class="regular-text" name="role" value="" placeholder="' . esc_attr__('Owner / Admin', 'wp-seo-next') . '" /></label></p>';
        echo '<p><label>' . esc_html__('Notes', 'wp-seo-next') . '<br /><input type="text" class="regular-text" name="notes" value="" placeholder="' . esc_attr__('Primary contact for this license', 'wp-seo-next') . '" /></label></p>';
        echo '<p><button class="button button-primary" type="submit"' . disabled(!$this->managementActionAllowed($allowed, 'assign_seat'), true, false) . '>' . esc_html__('Assign seat', 'wp-seo-next') . '</button></p>';
        echo '</form>';
        if (!$this->managementActionAllowed($allowed, 'assign_seat')) {
            echo '<p class="description">' . esc_html__('The current backend payload did not explicitly allow assigning seats.', 'wp-seo-next') . '</p>';
        }
        echo '</div>';

        echo '<div class="postbox" style="padding:16px; margin:0;">';
        echo '<h4 style="margin-top:0;">' . esc_html__('Assign another teammate / seat', 'wp-seo-next') . '</h4>';
        echo '<p class="description">' . esc_html__('Create or invite another seat assignee and optionally attach them to a specific site slot returned by the backend.', 'wp-seo-next') . '</p>';
        echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
        echo '<input type="hidden" name="action" value="wpsn_self_serve_manage_seat_assignment" />';
        echo '<input type="hidden" name="seat_assignment_operation" value="assign_seat" />';
        wp_nonce_field('wpsn_self_serve_manage_seat_assignment');
        echo '<p><label>' . esc_html__('Email', 'wp-seo-next') . '<br /><input type="email" class="regular-text" name="email" value="" placeholder="teammate@example.com" /></label></p>';
        echo '<p><label>' . esc_html__('Display name', 'wp-seo-next') . '<br /><input type="text" class="regular-text" name="name" value="" placeholder="' . esc_attr__('SEO manager', 'wp-seo-next') . '" /></label></p>';
        echo '<p><label>' . esc_html__('Seats consumed', 'wp-seo-next') . '<br /><input type="number" min="1" step="1" class="small-text" name="seat_count" value="1" /></label></p>';
        echo '<p><label>' . esc_html__('Link to site slot', 'wp-seo-next') . '<br />';
        $this->renderSiteAssignmentSelect('site_assignment_id', '', $siteAssignments, __('Not linked to a site slot', 'wp-seo-next'));
        echo '</label></p>';
        echo '<p><label>' . esc_html__('Role / label', 'wp-seo-next') . '<br /><input type="text" class="regular-text" name="role" value="" placeholder="' . esc_attr__('Account manager', 'wp-seo-next') . '" /></label></p>';
        echo '<p><label>' . esc_html__('Notes', 'wp-seo-next') . '<br /><input type="text" class="regular-text" name="notes" value="" placeholder="' . esc_attr__('Optional internal notes', 'wp-seo-next') . '" /></label></p>';
        echo '<p><button class="button button-primary" type="submit"' . disabled(!$this->managementActionAllowed($allowed, 'assign_seat'), true, false) . '>' . esc_html__('Assign teammate seat', 'wp-seo-next') . '</button></p>';
        echo '</form>';
        echo '</div>';

        echo '</div>';

        if (!empty($siteAssignments)) {
            echo '<h4 style="margin-top:20px;">' . esc_html__('Assigned sites / domains', 'wp-seo-next') . '</h4>';
            echo '<table class="widefat striped" style="max-width:1100px; margin-top:8px;">';
            echo '<thead><tr>';
            echo '<th>' . esc_html__('Site', 'wp-seo-next') . '</th>';
            echo '<th>' . esc_html__('Status', 'wp-seo-next') . '</th>';
            echo '<th>' . esc_html__('Assigned', 'wp-seo-next') . '</th>';
            echo '<th>' . esc_html__('Last seen', 'wp-seo-next') . '</th>';
            echo '<th>' . esc_html__('Actions', 'wp-seo-next') . '</th>';
            echo '</tr></thead><tbody>';
            foreach ($siteAssignments as $assignment) {
                if (!is_array($assignment)) {
                    continue;
                }
                $assignmentId = isset($assignment['id']) ? (string) $assignment['id'] : '';
                $siteUrl = isset($assignment['site_url']) ? (string) $assignment['site_url'] : '';
                $domain = isset($assignment['domain']) ? (string) $assignment['domain'] : '';
                $label = isset($assignment['label']) ? (string) $assignment['label'] : '';
                $notes = isset($assignment['notes']) ? (string) $assignment['notes'] : '';
                $statusBits = [];
                if (!empty($assignment['is_current_site'])) {
                    $statusBits[] = __('Current site', 'wp-seo-next');
                }
                if (!empty($assignment['is_primary'])) {
                    $statusBits[] = __('Primary', 'wp-seo-next');
                }
                if (!empty($assignment['status'])) {
                    $statusBits[] = (string) $assignment['status'];
                }
                if (!empty($assignment['seat_count'])) {
                    $statusBits[] = sprintf(__('Seats: %d', 'wp-seo-next'), (int) $assignment['seat_count']);
                }

                echo '<tr' . (!empty($assignment['is_current_site']) ? ' style="background:#f6ffed;"' : '') . '>';
                echo '<td>';
                if ($domain !== '') {
                    echo '<code>' . esc_html($domain) . '</code>';
                } else {
                    echo '—';
                }
                if ($siteUrl !== '') {
                    echo '<br /><span class="description">' . esc_html($siteUrl) . '</span>';
                }
                if ($label !== '') {
                    echo '<br /><strong>' . esc_html($label) . '</strong>';
                }
                if ($notes !== '') {
                    echo '<br /><span class="description">' . esc_html($notes) . '</span>';
                }
                echo '</td>';
                echo '<td>' . esc_html(!empty($statusBits) ? implode(' · ', $statusBits) : '—') . '</td>';
                echo '<td>' . esc_html(!empty($assignment['assigned_at']) ? (string) $assignment['assigned_at'] : '—') . '</td>';
                echo '<td>' . esc_html(!empty($assignment['last_seen_at']) ? (string) $assignment['last_seen_at'] : '—') . '</td>';
                echo '<td>';
                echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin:0 0 8px;">';
                echo '<input type="hidden" name="action" value="wpsn_self_serve_manage_site_assignment" />';
                echo '<input type="hidden" name="site_assignment_operation" value="update_site_assignment" />';
                echo '<input type="hidden" name="assignment_id" value="' . esc_attr($assignmentId) . '" />';
                wp_nonce_field('wpsn_self_serve_manage_site_assignment');
                echo '<input type="text" class="regular-text" name="site_url" value="' . esc_attr($siteUrl !== '' ? $siteUrl : $domain) . '" placeholder="https://client.example.com/" style="margin-bottom:6px; width:100%;" />';
                echo '<input type="text" class="regular-text" name="label" value="' . esc_attr($label) . '" placeholder="' . esc_attr__('Label', 'wp-seo-next') . '" style="margin-bottom:6px; width:100%;" />';
                echo '<input type="text" class="regular-text" name="notes" value="' . esc_attr($notes) . '" placeholder="' . esc_attr__('Notes', 'wp-seo-next') . '" style="margin-bottom:6px; width:100%;" />';
                echo '<button class="button button-small" type="submit"' . disabled(!$this->managementActionAllowed($allowed, 'update_site_assignment'), true, false) . '>' . esc_html__('Save assignment', 'wp-seo-next') . '</button>';
                echo '</form>';

                echo '<div style="display:flex; gap:8px; flex-wrap:wrap;">';
                echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin:0;">';
                echo '<input type="hidden" name="action" value="wpsn_self_serve_manage_site_assignment" />';
                echo '<input type="hidden" name="site_assignment_operation" value="set_primary_site" />';
                echo '<input type="hidden" name="assignment_id" value="' . esc_attr($assignmentId) . '" />';
                wp_nonce_field('wpsn_self_serve_manage_site_assignment');
                echo '<button class="button button-small" type="submit"' . disabled(!$this->managementActionAllowed($allowed, 'set_primary_site'), true, false) . '>' . esc_html__('Set primary', 'wp-seo-next') . '</button>';
                echo '</form>';

                echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin:0;">';
                echo '<input type="hidden" name="action" value="wpsn_self_serve_manage_site_assignment" />';
                echo '<input type="hidden" name="site_assignment_operation" value="release_site" />';
                echo '<input type="hidden" name="assignment_id" value="' . esc_attr($assignmentId) . '" />';
                wp_nonce_field('wpsn_self_serve_manage_site_assignment');
                echo '<button class="button button-small" type="submit"' . disabled(!$this->managementActionAllowed($allowed, 'release_site'), true, false) . '>' . esc_html__('Release slot', 'wp-seo-next') . '</button>';
                echo '</form>';
                echo '</div>';
                echo '</td>';
                echo '</tr>';
            }
            echo '</tbody></table>';
        } else {
            echo '<div class="notice notice-info inline"><p>' . esc_html__('No site assignments were returned by the backend yet. Assign the current site or add another domain to start consuming site slots.', 'wp-seo-next') . '</p></div>';
        }

        echo '<h4 style="margin-top:20px;">' . esc_html__('Seat assignment editor', 'wp-seo-next') . '</h4>';
        echo '<p class="description" style="margin:0 0 8px;">' . esc_html__('Seats can be attached to a teammate and optionally linked to one of the site slots above. This lets agencies see who is actively consuming a seat and which client/site that seat belongs to.', 'wp-seo-next') . '</p>';
        if (!empty($seatAssignments)) {
            echo '<table class="widefat striped" style="max-width:1200px; margin-top:8px;">';
            echo '<thead><tr><th>' . esc_html__('User', 'wp-seo-next') . '</th><th>' . esc_html__('Status', 'wp-seo-next') . '</th><th>' . esc_html__('Seats', 'wp-seo-next') . '</th><th>' . esc_html__('Linked site', 'wp-seo-next') . '</th><th>' . esc_html__('Assigned', 'wp-seo-next') . '</th><th>' . esc_html__('Last active', 'wp-seo-next') . '</th><th>' . esc_html__('Actions', 'wp-seo-next') . '</th></tr></thead><tbody>';
            foreach ($seatAssignments as $seat) {
                if (!is_array($seat)) {
                    continue;
                }
                $seatId = isset($seat['id']) ? (string) $seat['id'] : '';
                $name = isset($seat['name']) ? (string) $seat['name'] : '';
                $email = isset($seat['email']) ? (string) $seat['email'] : '';
                $role = isset($seat['role']) ? (string) $seat['role'] : '';
                $notes = isset($seat['notes']) ? (string) $seat['notes'] : '';
                $statusBits = [];
                if (!empty($seat['is_current_user'])) {
                    $statusBits[] = __('Current admin', 'wp-seo-next');
                }
                if (!empty($seat['status'])) {
                    $statusBits[] = (string) $seat['status'];
                }
                if ($role !== '') {
                    $statusBits[] = $role;
                }
                $linkedSiteId = isset($seat['site_assignment_id']) ? (string) $seat['site_assignment_id'] : '';
                $linkedSiteLabel = $linkedSiteId !== '' && isset($siteLabelMap[$linkedSiteId]) ? $siteLabelMap[$linkedSiteId] : $linkedSiteId;

                echo '<tr' . (!empty($seat['is_current_user']) ? ' style="background:#f6ffed;"' : '') . '>';
                echo '<td>';
                echo esc_html(trim($name . ($name !== '' && $email !== '' ? ' · ' : '') . $email) !== '' ? trim($name . ($name !== '' && $email !== '' ? ' · ' : '') . $email) : '—');
                if ($notes !== '') {
                    echo '<br /><span class="description">' . esc_html($notes) . '</span>';
                }
                echo '</td>';
                echo '<td>' . esc_html(!empty($statusBits) ? implode(' · ', $statusBits) : '—') . '</td>';
                echo '<td>' . esc_html(isset($seat['seat_count']) && $seat['seat_count'] !== null ? (string) $seat['seat_count'] : '—') . '</td>';
                echo '<td>' . ($linkedSiteLabel !== '' ? esc_html($linkedSiteLabel) : '—') . '</td>';
                echo '<td>' . esc_html(!empty($seat['assigned_at']) ? (string) $seat['assigned_at'] : '—') . '</td>';
                echo '<td>' . esc_html(!empty($seat['last_seen_at']) ? (string) $seat['last_seen_at'] : '—') . '</td>';
                echo '<td>';
                echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin:0 0 8px;">';
                echo '<input type="hidden" name="action" value="wpsn_self_serve_manage_seat_assignment" />';
                echo '<input type="hidden" name="seat_assignment_operation" value="update_seat_assignment" />';
                echo '<input type="hidden" name="assignment_id" value="' . esc_attr($seatId) . '" />';
                wp_nonce_field('wpsn_self_serve_manage_seat_assignment');
                echo '<input type="email" class="regular-text" name="email" value="' . esc_attr($email) . '" placeholder="teammate@example.com" style="margin-bottom:6px; width:100%;" />';
                echo '<input type="text" class="regular-text" name="name" value="' . esc_attr($name) . '" placeholder="' . esc_attr__('Display name', 'wp-seo-next') . '" style="margin-bottom:6px; width:100%;" />';
                echo '<input type="text" class="regular-text" name="role" value="' . esc_attr($role) . '" placeholder="' . esc_attr__('Role / label', 'wp-seo-next') . '" style="margin-bottom:6px; width:100%;" />';
                echo '<input type="text" class="regular-text" name="status" value="' . esc_attr(isset($seat['status']) ? (string) $seat['status'] : '') . '" placeholder="' . esc_attr__('Status', 'wp-seo-next') . '" style="margin-bottom:6px; width:100%;" />';
                echo '<input type="number" min="1" step="1" class="small-text" name="seat_count" value="' . esc_attr(isset($seat['seat_count']) && $seat['seat_count'] !== null ? (string) $seat['seat_count'] : '1') . '" style="margin-bottom:6px; width:100%;" />';
                echo '<div style="margin-bottom:6px;">';
                $this->renderSiteAssignmentSelect('site_assignment_id', $linkedSiteId, $siteAssignments, __('Not linked to a site slot', 'wp-seo-next'));
                echo '</div>';
                echo '<input type="text" class="regular-text" name="notes" value="' . esc_attr($notes) . '" placeholder="' . esc_attr__('Notes', 'wp-seo-next') . '" style="margin-bottom:6px; width:100%;" />';
                echo '<button class="button button-small" type="submit"' . disabled(!$this->managementActionAllowed($allowed, 'update_seat_assignment'), true, false) . '>' . esc_html__('Save seat', 'wp-seo-next') . '</button>';
                echo '</form>';

                echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin:0;">';
                echo '<input type="hidden" name="action" value="wpsn_self_serve_manage_seat_assignment" />';
                echo '<input type="hidden" name="seat_assignment_operation" value="release_seat" />';
                echo '<input type="hidden" name="assignment_id" value="' . esc_attr($seatId) . '" />';
                wp_nonce_field('wpsn_self_serve_manage_seat_assignment');
                echo '<button class="button button-small" type="submit"' . disabled(!$this->managementActionAllowed($allowed, 'release_seat'), true, false) . '>' . esc_html__('Release seat', 'wp-seo-next') . '</button>';
                echo '</form>';
                echo '</td>';
                echo '</tr>';
            }
            echo '</tbody></table>';
        } else {
            echo '<div class="notice notice-info inline"><p>' . esc_html__('No seat assignments were returned yet. Use the forms above to attach the current admin or another teammate to a subscription seat.', 'wp-seo-next') . '</p></div>';
        }

        if (!empty($usageEvents)) {
            echo '<h4 style="margin-top:20px;">' . esc_html__('Usage history', 'wp-seo-next') . '</h4>';
            echo '<table class="widefat striped" style="max-width:1100px; margin-top:8px;">';
            echo '<thead><tr><th style="width:180px;">' . esc_html__('Date', 'wp-seo-next') . '</th><th style="width:180px;">' . esc_html__('Event', 'wp-seo-next') . '</th><th>' . esc_html__('Summary', 'wp-seo-next') . '</th><th style="width:240px;">' . esc_html__('Value', 'wp-seo-next') . '</th></tr></thead><tbody>';
            foreach ($usageEvents as $event) {
                if (!is_array($event)) {
                    continue;
                }
                echo '<tr>';
                echo '<td>' . esc_html(!empty($event['date']) ? (string) $event['date'] : '—') . '</td>';
                echo '<td>' . esc_html(!empty($event['kind']) ? (string) $event['kind'] : '—') . '</td>';
                echo '<td>' . esc_html(!empty($event['summary']) ? (string) $event['summary'] : '—') . '</td>';
                echo '<td>' . esc_html(!empty($event['value']) ? (string) $event['value'] : '—') . '</td>';
                echo '</tr>';
            }
            echo '</tbody></table>';
        }

        if ($currentSiteHost !== '' && !is_array($currentAssignment)) {
            echo '<p class="description" style="margin-top:8px;">' . esc_html(sprintf(__('Current site host: %s', 'wp-seo-next'), $currentSiteHost)) . '</p>';
        }
    }

    /**
     * @param array<string,mixed> $assignment
     */
    private function siteAssignmentChoiceLabel(array $assignment): string
    {
        $domain = isset($assignment['domain']) ? trim((string) $assignment['domain']) : '';
        $siteUrl = isset($assignment['site_url']) ? trim((string) $assignment['site_url']) : '';
        $label = isset($assignment['label']) ? trim((string) $assignment['label']) : '';
        $status = isset($assignment['status']) ? trim((string) $assignment['status']) : '';

        $bits = [];
        if ($label !== '') {
            $bits[] = $label;
        }
        if ($domain !== '') {
            $bits[] = $domain;
        } elseif ($siteUrl !== '') {
            $bits[] = $siteUrl;
        }
        if (!empty($assignment['is_primary'])) {
            $bits[] = __('Primary', 'wp-seo-next');
        }
        if ($status !== '') {
            $bits[] = $status;
        }

        return !empty($bits) ? implode(' · ', $bits) : __('Site slot', 'wp-seo-next');
    }

    /**
     * @param array<int,array<string,mixed>> $siteAssignments
     */
    private function renderSiteAssignmentSelect(string $fieldName, string $selectedId, array $siteAssignments, string $placeholder = ''): void
    {
        $placeholder = trim($placeholder) !== '' ? $placeholder : __('Not linked to a site slot', 'wp-seo-next');
        echo '<select name="' . esc_attr($fieldName) . '" class="regular-text">';
        echo '<option value="">' . esc_html($placeholder) . '</option>';
        foreach ($siteAssignments as $assignment) {
            if (!is_array($assignment)) {
                continue;
            }
            $assignmentId = isset($assignment['id']) ? (string) $assignment['id'] : '';
            if ($assignmentId === '') {
                continue;
            }
            $selected = $assignmentId === $selectedId ? ' selected="selected"' : '';
            echo '<option value="' . esc_attr($assignmentId) . '"' . $selected . '>' . esc_html($this->siteAssignmentChoiceLabel($assignment)) . '</option>';
        }
        echo '</select>';
    }

    /**
     * @param string[] $allowed
     */
    private function managementActionAllowed(array $allowed, string $action): bool
    {
        if (empty($allowed)) {
            return true;
        }
        return in_array(sanitize_key($action), $allowed, true);
    }

    /**
     * @param array<string,mixed> $actions
     */
    private function renderBillingActionButtons(array $actions): void
    {
        $labels = [
            'portal_url' => __('Open billing portal', 'wp-seo-next'),
            'account_url' => __('Open account portal', 'wp-seo-next'),
            'change_plan_url' => __('Change plan', 'wp-seo-next'),
            'payment_method_url' => __('Update payment method', 'wp-seo-next'),
            'invoices_url' => __('All invoices', 'wp-seo-next'),
            'cancel_url' => __('Cancel subscription', 'wp-seo-next'),
            'support_url' => __('Billing support', 'wp-seo-next'),
            'purchase_url' => __('Upgrade', 'wp-seo-next'),
        ];

        $seen = [];
        $html = [];
        foreach ($labels as $key => $label) {
            $url = isset($actions[$key]) ? trim((string) $actions[$key]) : '';
            if ($url === '' || isset($seen[$url])) {
                continue;
            }
            $seen[$url] = true;
            $class = in_array($key, ['portal_url', 'change_plan_url', 'purchase_url'], true) ? 'button button-primary' : 'button';
            $html[] = '<a class="' . esc_attr($class) . '" href="' . esc_url($url) . '" target="_blank" rel="noopener noreferrer">' . esc_html($label) . '</a>';
        }

        if (!empty($html)) {
            echo '<p style="margin: 12px 0;">' . implode(' ', $html) . '</p>';
        }
    }

    private function formatBillingSource(string $source): string
    {
        switch ($source) {
            case 'billing_sync':
                return __('Billing JSON sync endpoint', 'wp-seo-next');
            case 'license_payload':
                return __('License verify payload', 'wp-seo-next');
            case 'manual':
            default:
                return __('Manual configuration only', 'wp-seo-next');
        }
    }

    /**
     * @param array<string,mixed> $billing
     */
    private function formatCustomer(array $billing): string
    {
        $parts = [];
        if (!empty($billing['customer_name'])) {
            $parts[] = (string) $billing['customer_name'];
        }
        if (!empty($billing['customer_email'])) {
            $parts[] = (string) $billing['customer_email'];
        }
        if (empty($parts)) {
            return '—';
        }
        return implode(' · ', $parts);
    }

    /**
     * @param mixed $used
     * @param mixed $limit
     */
    private function formatUsage($used, $limit): string
    {
        $usedNum = is_numeric($used) ? (int) $used : null;
        $limitNum = is_numeric($limit) ? (int) $limit : null;

        if ($usedNum === null && $limitNum === null) {
            return '—';
        }
        if ($usedNum !== null && $limitNum !== null) {
            return (string) $usedNum . ' / ' . $this->formatLimitValue($limitNum);
        }
        if ($usedNum !== null) {
            return (string) $usedNum;
        }
        return sprintf(__('Limit: %s', 'wp-seo-next'), $this->formatLimitValue($limitNum));
    }

    /**
     * @param mixed $limit
     */
    private function formatLimitValue($limit): string
    {
        if (!is_numeric($limit)) {
            return '—';
        }

        $limitNum = (int) $limit;
        if ($limitNum < 0) {
            return __('Unlimited', 'wp-seo-next');
        }

        return (string) $limitNum;
    }

    private function renderStatusBadge(string $status, bool $trusted): string
    {
        $label = $status;
        $class = 'notice-info';

        switch ($status) {
            case LicenseService::STATUS_NONE:
                if (PlanMatrix::buildTier() === PlanMatrix::TIER_FREE) {
                    $label = __('Free', 'wp-seo-next');
                    $class = 'notice-success';
                    $trusted = true;
                } else {
                    $label = __('No key', 'wp-seo-next');
                    $class = 'notice-info';
                }
                break;
            case LicenseService::STATUS_ACTIVE:
                $label = __('Active', 'wp-seo-next');
                $class = 'notice-success';
                break;
            case LicenseService::STATUS_GRACE:
                $label = __('Grace mode', 'wp-seo-next');
                $class = 'notice-warning';
                break;
            case LicenseService::STATUS_EXPIRED:
                $label = __('Expired', 'wp-seo-next');
                $class = 'notice-error';
                break;
            case LicenseService::STATUS_INVALID:
                $label = __('Invalid', 'wp-seo-next');
                $class = 'notice-error';
                break;
            case LicenseService::STATUS_UNKNOWN:
            default:
                $label = __('Unknown', 'wp-seo-next');
                $class = 'notice-info';
                break;
        }

        $trustText = $trusted ? (PlanMatrix::buildTier() === PlanMatrix::TIER_FREE && $status === LicenseService::STATUS_NONE ? __('Local build', 'wp-seo-next') : __('Signed', 'wp-seo-next')) : __('Unsigned/Untrusted', 'wp-seo-next');

        return '<span class="notice ' . esc_attr($class) . '" style="display:inline-block; padding: 6px 10px; margin:0;">'
            . '<strong>' . esc_html($label) . '</strong>'
            . ' <span style="opacity:.8">(' . esc_html($trustText) . ')</span>'
            . '</span>';
    }

    public function handleSelfServeCatalogRefresh(): void
    {
        if (!current_user_can(Capabilities::CAP_LICENSE)) {
            wp_die(esc_html__('You do not have permission to perform this action.', 'wp-seo-next'));
        }

        check_admin_referer('wpsn_self_serve_catalog_refresh');

        $res = $this->selfServe->fetchCatalogIfNeeded(true);
        if (empty($res['ok'])) {
            $err = isset($res['error']) ? (string) $res['error'] : '';
            wp_safe_redirect(add_query_arg([
                'page' => self::PAGE_SLUG,
                'tab' => 'billing',
                'wpsn_notice' => 'self_serve_failed',
                'wpsn_error' => $err,
            ], admin_url('tools.php')));
            exit;
        }

        wp_safe_redirect(add_query_arg(['page' => self::PAGE_SLUG, 'tab' => 'billing', 'wpsn_notice' => 'self_serve_catalog_synced'], admin_url('tools.php')));
        exit;
    }

    public function handleSelfServeCatalogClear(): void
    {
        if (!current_user_can(Capabilities::CAP_LICENSE)) {
            wp_die(esc_html__('You do not have permission to perform this action.', 'wp-seo-next'));
        }

        check_admin_referer('wpsn_self_serve_catalog_clear');

        $this->selfServe->clearCatalogCache();

        wp_safe_redirect(add_query_arg(['page' => self::PAGE_SLUG, 'tab' => 'billing', 'wpsn_notice' => 'self_serve_catalog_cleared'], admin_url('tools.php')));
        exit;
    }

    public function handleSelfServeCheckout(): void
    {
        if (!current_user_can(Capabilities::CAP_LICENSE)) {
            wp_die(esc_html__('You do not have permission to perform this action.', 'wp-seo-next'));
        }

        check_admin_referer('wpsn_self_serve_checkout');

        $returnTab = isset($_POST['return_tab']) ? sanitize_key((string) wp_unslash($_POST['return_tab'])) : 'billing';
        if (!in_array($returnTab, ['overview', 'billing', 'settings'], true)) {
            $returnTab = 'billing';
        }

        $planId = isset($_POST['plan_id']) ? sanitize_key((string) wp_unslash($_POST['plan_id'])) : '';
        $interval = isset($_POST['interval']) ? sanitize_key((string) wp_unslash($_POST['interval'])) : '';
        $coupon = isset($_POST['coupon']) ? sanitize_text_field((string) wp_unslash($_POST['coupon'])) : '';

        $res = $this->selfServe->startCheckout($planId, $interval, $coupon);
        if (empty($res['ok'])) {
            $err = isset($res['error']) ? (string) $res['error'] : '';
            wp_safe_redirect(add_query_arg([
                'page' => self::PAGE_SLUG,
                'tab' => $returnTab,
                'wpsn_notice' => 'self_serve_failed',
                'wpsn_error' => $err,
            ], admin_url('tools.php')));
            exit;
        }

        $message = isset($res['message']) ? sanitize_text_field((string) $res['message']) : '';
        wp_safe_redirect(add_query_arg([
            'page' => self::PAGE_SLUG,
            'tab' => 'billing',
            'wpsn_notice' => 'self_serve_checkout_started',
            'wpsn_message' => $message,
        ], admin_url('tools.php')));
        exit;
    }

    public function handleSelfServeCheckoutClear(): void
    {
        if (!current_user_can(Capabilities::CAP_LICENSE)) {
            wp_die(esc_html__('You do not have permission to perform this action.', 'wp-seo-next'));
        }

        check_admin_referer('wpsn_self_serve_checkout_clear');

        $this->selfServe->clearCheckoutSession();

        wp_safe_redirect(add_query_arg(['page' => self::PAGE_SLUG, 'tab' => 'billing', 'wpsn_notice' => 'self_serve_checkout_cleared'], admin_url('tools.php')));
        exit;
    }

    public function handleSelfServeAction(): void
    {
        if (!current_user_can(Capabilities::CAP_LICENSE)) {
            wp_die(esc_html__('You do not have permission to perform this action.', 'wp-seo-next'));
        }

        check_admin_referer('wpsn_self_serve_action');

        $returnTab = isset($_POST['return_tab']) ? sanitize_key((string) wp_unslash($_POST['return_tab'])) : 'billing';
        if (!in_array($returnTab, ['overview', 'billing', 'settings'], true)) {
            $returnTab = 'billing';
        }

        $action = isset($_POST['self_serve_action_name']) ? sanitize_key((string) wp_unslash($_POST['self_serve_action_name'])) : '';
        $planId = isset($_POST['plan_id']) ? sanitize_key((string) wp_unslash($_POST['plan_id'])) : '';
        $interval = isset($_POST['interval']) ? sanitize_key((string) wp_unslash($_POST['interval'])) : '';

        $res = $this->selfServe->performAction($action, $planId, $interval);
        if (empty($res['ok'])) {
            $err = isset($res['error']) ? (string) $res['error'] : '';
            wp_safe_redirect(add_query_arg([
                'page' => self::PAGE_SLUG,
                'tab' => $returnTab,
                'wpsn_notice' => 'self_serve_failed',
                'wpsn_error' => $err,
            ], admin_url('tools.php')));
            exit;
        }

        $message = isset($res['message']) ? sanitize_text_field((string) $res['message']) : '';
        wp_safe_redirect(add_query_arg([
            'page' => self::PAGE_SLUG,
            'tab' => 'billing',
            'wpsn_notice' => 'self_serve_action_done',
            'wpsn_message' => $message,
        ], admin_url('tools.php')));
        exit;
    }

    public function handleSelfServeManagementRefresh(): void
    {
        if (!current_user_can(Capabilities::CAP_LICENSE)) {
            wp_die(esc_html__('You do not have permission to perform this action.', 'wp-seo-next'));
        }

        check_admin_referer('wpsn_self_serve_manage_refresh');

        $res = $this->management->fetchStateIfNeeded(true);
        if (empty($res['ok'])) {
            $err = isset($res['error']) ? (string) $res['error'] : '';
            wp_safe_redirect($this->pageUrl([
                'tab' => 'billing',
                'wpsn_notice' => 'self_serve_manage_failed',
                'wpsn_error' => $err,
            ]));
            exit;
        }

        wp_safe_redirect($this->pageUrl([
            'tab' => 'billing',
            'wpsn_notice' => 'self_serve_manage_synced',
            'wpsn_message' => isset($res['message']) ? sanitize_text_field((string) $res['message']) : '',
        ]));
        exit;
    }

    public function handleSelfServeManagementClear(): void
    {
        if (!current_user_can(Capabilities::CAP_LICENSE)) {
            wp_die(esc_html__('You do not have permission to perform this action.', 'wp-seo-next'));
        }

        check_admin_referer('wpsn_self_serve_manage_clear');

        $this->management->clearCache();

        wp_safe_redirect($this->pageUrl([
            'tab' => 'billing',
            'wpsn_notice' => 'self_serve_manage_cleared',
        ]));
        exit;
    }

    public function handleSelfServeManagementUpdateCustomer(): void
    {
        if (!current_user_can(Capabilities::CAP_LICENSE)) {
            wp_die(esc_html__('You do not have permission to perform this action.', 'wp-seo-next'));
        }

        check_admin_referer('wpsn_self_serve_manage_update_customer');

        $params = [
            'customer_name' => isset($_POST['customer_name']) ? sanitize_text_field((string) wp_unslash($_POST['customer_name'])) : '',
            'customer_email' => isset($_POST['customer_email']) ? sanitize_email((string) wp_unslash($_POST['customer_email'])) : '',
            'company' => isset($_POST['company']) ? sanitize_text_field((string) wp_unslash($_POST['company'])) : '',
            'vat_id' => isset($_POST['vat_id']) ? sanitize_text_field((string) wp_unslash($_POST['vat_id'])) : '',
        ];

        $res = $this->management->performOperation('update_customer', $params);
        if (empty($res['ok'])) {
            $err = isset($res['error']) ? (string) $res['error'] : '';
            wp_safe_redirect($this->pageUrl([
                'tab' => 'billing',
                'wpsn_notice' => 'self_serve_manage_failed',
                'wpsn_error' => $err,
            ]));
            exit;
        }

        wp_safe_redirect($this->pageUrl([
            'tab' => 'billing',
            'wpsn_notice' => 'self_serve_manage_updated',
            'wpsn_message' => isset($res['message']) ? sanitize_text_field((string) $res['message']) : '',
        ]));
        exit;
    }

    public function handleSelfServeManagementChangePlan(): void
    {
        if (!current_user_can(Capabilities::CAP_LICENSE)) {
            wp_die(esc_html__('You do not have permission to perform this action.', 'wp-seo-next'));
        }

        check_admin_referer('wpsn_self_serve_manage_change_plan');

        $params = [
            'plan_id' => isset($_POST['plan_id']) ? sanitize_key((string) wp_unslash($_POST['plan_id'])) : '',
            'interval' => isset($_POST['interval']) ? sanitize_key((string) wp_unslash($_POST['interval'])) : '',
            'coupon' => isset($_POST['coupon']) ? sanitize_text_field((string) wp_unslash($_POST['coupon'])) : '',
        ];

        $res = $this->management->performOperation('change_plan', $params);
        if (empty($res['ok'])) {
            $err = isset($res['error']) ? (string) $res['error'] : '';
            wp_safe_redirect($this->pageUrl([
                'tab' => 'billing',
                'wpsn_notice' => 'self_serve_manage_failed',
                'wpsn_error' => $err,
            ]));
            exit;
        }

        wp_safe_redirect($this->pageUrl([
            'tab' => 'billing',
            'wpsn_notice' => 'self_serve_manage_updated',
            'wpsn_message' => isset($res['message']) ? sanitize_text_field((string) $res['message']) : '',
        ]));
        exit;
    }

    public function handleSelfServeManagementUsage(): void
    {
        if (!current_user_can(Capabilities::CAP_LICENSE)) {
            wp_die(esc_html__('You do not have permission to perform this action.', 'wp-seo-next'));
        }

        check_admin_referer('wpsn_self_serve_manage_usage');

        $params = [
            'seat_limit' => isset($_POST['seat_limit']) && $_POST['seat_limit'] !== '' ? max(0, (int) $_POST['seat_limit']) : '',
            'site_limit' => isset($_POST['site_limit']) && $_POST['site_limit'] !== '' ? max(0, (int) $_POST['site_limit']) : '',
        ];

        $res = $this->management->performOperation('update_usage', $params);
        if (empty($res['ok'])) {
            $err = isset($res['error']) ? (string) $res['error'] : '';
            wp_safe_redirect($this->pageUrl([
                'tab' => 'billing',
                'wpsn_notice' => 'self_serve_manage_failed',
                'wpsn_error' => $err,
            ]));
            exit;
        }

        wp_safe_redirect($this->pageUrl([
            'tab' => 'billing',
            'wpsn_notice' => 'self_serve_manage_updated',
            'wpsn_message' => isset($res['message']) ? sanitize_text_field((string) $res['message']) : '',
        ]));
        exit;
    }

    public function handleSelfServeManagementSubscription(): void
    {
        if (!current_user_can(Capabilities::CAP_LICENSE)) {
            wp_die(esc_html__('You do not have permission to perform this action.', 'wp-seo-next'));
        }

        check_admin_referer('wpsn_self_serve_manage_subscription');

        $operation = isset($_POST['subscription_operation']) ? sanitize_key((string) wp_unslash($_POST['subscription_operation'])) : '';
        if (!in_array($operation, ['cancel_now', 'cancel_at_period_end', 'resume'], true)) {
            wp_safe_redirect($this->pageUrl([
                'tab' => 'billing',
                'wpsn_notice' => 'self_serve_manage_failed',
                'wpsn_error' => __('Unsupported subscription operation.', 'wp-seo-next'),
            ]));
            exit;
        }

        $res = $this->management->performOperation($operation, []);
        if (empty($res['ok'])) {
            $err = isset($res['error']) ? (string) $res['error'] : '';
            wp_safe_redirect($this->pageUrl([
                'tab' => 'billing',
                'wpsn_notice' => 'self_serve_manage_failed',
                'wpsn_error' => $err,
            ]));
            exit;
        }

        wp_safe_redirect($this->pageUrl([
            'tab' => 'billing',
            'wpsn_notice' => 'self_serve_manage_updated',
            'wpsn_message' => isset($res['message']) ? sanitize_text_field((string) $res['message']) : '',
        ]));
        exit;
    }

    public function handleSelfServeManagementPaymentMethod(): void
    {
        if (!current_user_can(Capabilities::CAP_LICENSE)) {
            wp_die(esc_html__('You do not have permission to perform this action.', 'wp-seo-next'));
        }

        check_admin_referer('wpsn_self_serve_manage_payment_method');

        $operation = isset($_POST['payment_method_operation']) ? sanitize_key((string) wp_unslash($_POST['payment_method_operation'])) : '';
        if (!in_array($operation, ['set_default_payment_method', 'remove_payment_method'], true)) {
            wp_safe_redirect($this->pageUrl([
                'tab' => 'billing',
                'wpsn_notice' => 'self_serve_manage_failed',
                'wpsn_error' => __('Unsupported payment-method operation.', 'wp-seo-next'),
            ]));
            exit;
        }

        $paymentMethodId = isset($_POST['payment_method_id']) ? sanitize_text_field((string) wp_unslash($_POST['payment_method_id'])) : '';
        if ($paymentMethodId === '') {
            wp_safe_redirect($this->pageUrl([
                'tab' => 'billing',
                'wpsn_notice' => 'self_serve_manage_failed',
                'wpsn_error' => __('Payment method ID is required.', 'wp-seo-next'),
            ]));
            exit;
        }

        $res = $this->management->performOperation($operation, [
            'payment_method_id' => $paymentMethodId,
        ]);
        if (empty($res['ok'])) {
            $err = isset($res['error']) ? (string) $res['error'] : '';
            wp_safe_redirect($this->pageUrl([
                'tab' => 'billing',
                'wpsn_notice' => 'self_serve_manage_failed',
                'wpsn_error' => $err,
            ]));
            exit;
        }

        wp_safe_redirect($this->pageUrl([
            'tab' => 'billing',
            'wpsn_notice' => 'self_serve_manage_updated',
            'wpsn_message' => isset($res['message']) ? sanitize_text_field((string) $res['message']) : '',
        ]));
        exit;
    }

    public function handleSelfServeManagementSiteAssignment(): void
    {
        if (!current_user_can(Capabilities::CAP_LICENSE)) {
            wp_die(esc_html__('You do not have permission to perform this action.', 'wp-seo-next'));
        }

        check_admin_referer('wpsn_self_serve_manage_site_assignment');

        $operation = isset($_POST['site_assignment_operation']) ? sanitize_key((string) wp_unslash($_POST['site_assignment_operation'])) : '';
        if (!in_array($operation, ['assign_site', 'update_site_assignment', 'release_site', 'set_primary_site'], true)) {
            wp_safe_redirect($this->pageUrl([
                'tab' => 'billing',
                'wpsn_notice' => 'self_serve_manage_failed',
                'wpsn_error' => __('Unsupported site-assignment operation.', 'wp-seo-next'),
            ]));
            exit;
        }

        $assignmentId = isset($_POST['assignment_id']) ? sanitize_text_field((string) wp_unslash($_POST['assignment_id'])) : '';
        $siteUrlRaw = isset($_POST['site_url']) ? trim((string) wp_unslash($_POST['site_url'])) : '';
        $siteUrl = $siteUrlRaw;
        if ($siteUrl !== '' && preg_match('#^[a-z][a-z0-9+.-]*://#i', $siteUrl)) {
            $siteUrl = esc_url_raw($siteUrl);
        } else {
            $siteUrl = sanitize_text_field($siteUrl);
        }
        $label = isset($_POST['label']) ? sanitize_text_field((string) wp_unslash($_POST['label'])) : '';
        $notes = isset($_POST['notes']) ? sanitize_text_field((string) wp_unslash($_POST['notes'])) : '';

        if ($operation === 'assign_site' && $siteUrl === '') {
            $siteUrl = home_url('/');
        }
        if (in_array($operation, ['update_site_assignment', 'release_site', 'set_primary_site'], true) && $assignmentId === '') {
            wp_safe_redirect($this->pageUrl([
                'tab' => 'billing',
                'wpsn_notice' => 'self_serve_manage_failed',
                'wpsn_error' => __('Assignment ID is required.', 'wp-seo-next'),
            ]));
            exit;
        }

        $params = [];
        if ($assignmentId !== '') {
            $params['assignment_id'] = $assignmentId;
        }
        if ($siteUrl !== '') {
            $params['site_url'] = $siteUrl;
        }
        if ($label !== '') {
            $params['label'] = $label;
        }
        if ($notes !== '') {
            $params['notes'] = $notes;
        }

        $res = $this->management->performOperation($operation, $params);
        if (empty($res['ok'])) {
            $err = isset($res['error']) ? (string) $res['error'] : '';
            wp_safe_redirect($this->pageUrl([
                'tab' => 'billing',
                'wpsn_notice' => 'self_serve_manage_failed',
                'wpsn_error' => $err,
            ]));
            exit;
        }

        wp_safe_redirect($this->pageUrl([
            'tab' => 'billing',
            'wpsn_notice' => 'self_serve_manage_updated',
            'wpsn_message' => isset($res['message']) ? sanitize_text_field((string) $res['message']) : '',
        ]));
        exit;
    }


    public function handleSelfServeManagementSeatAssignment(): void
    {
        if (!current_user_can(Capabilities::CAP_LICENSE)) {
            wp_die(esc_html__('You do not have permission to perform this action.', 'wp-seo-next'));
        }

        check_admin_referer('wpsn_self_serve_manage_seat_assignment');

        $operation = isset($_POST['seat_assignment_operation']) ? sanitize_key((string) wp_unslash($_POST['seat_assignment_operation'])) : '';
        if (!in_array($operation, ['assign_seat', 'update_seat_assignment', 'release_seat'], true)) {
            wp_safe_redirect($this->pageUrl([
                'tab' => 'billing',
                'wpsn_notice' => 'self_serve_manage_failed',
                'wpsn_error' => __('Unsupported seat-assignment operation.', 'wp-seo-next'),
            ]));
            exit;
        }

        $assignmentId = isset($_POST['assignment_id']) ? sanitize_text_field((string) wp_unslash($_POST['assignment_id'])) : '';
        $emailRaw = isset($_POST['email']) ? trim((string) wp_unslash($_POST['email'])) : '';
        $email = function_exists('sanitize_email') ? sanitize_email($emailRaw) : sanitize_text_field($emailRaw);
        $name = isset($_POST['name']) ? sanitize_text_field((string) wp_unslash($_POST['name'])) : '';
        $role = isset($_POST['role']) ? sanitize_text_field((string) wp_unslash($_POST['role'])) : '';
        $status = isset($_POST['status']) ? sanitize_text_field((string) wp_unslash($_POST['status'])) : '';
        $notes = isset($_POST['notes']) ? sanitize_text_field((string) wp_unslash($_POST['notes'])) : '';
        $siteAssignmentId = isset($_POST['site_assignment_id']) ? sanitize_text_field((string) wp_unslash($_POST['site_assignment_id'])) : '';
        $seatCount = isset($_POST['seat_count']) ? max(1, (int) wp_unslash($_POST['seat_count'])) : 0;

        if ($operation === 'assign_seat' && $email === '' && $name === '') {
            wp_safe_redirect($this->pageUrl([
                'tab' => 'billing',
                'wpsn_notice' => 'self_serve_manage_failed',
                'wpsn_error' => __('Seat email or display name is required.', 'wp-seo-next'),
            ]));
            exit;
        }
        if (in_array($operation, ['update_seat_assignment', 'release_seat'], true) && $assignmentId === '') {
            wp_safe_redirect($this->pageUrl([
                'tab' => 'billing',
                'wpsn_notice' => 'self_serve_manage_failed',
                'wpsn_error' => __('Seat assignment ID is required.', 'wp-seo-next'),
            ]));
            exit;
        }

        $params = [];
        if ($assignmentId !== '') {
            $params['assignment_id'] = $assignmentId;
            $params['seat_assignment_id'] = $assignmentId;
        }
        if ($email !== '') {
            $params['email'] = $email;
        }
        if ($name !== '') {
            $params['name'] = $name;
        }
        if ($role !== '') {
            $params['role'] = $role;
        }
        if ($status !== '') {
            $params['status'] = $status;
        }
        if ($notes !== '') {
            $params['notes'] = $notes;
        }
        if ($siteAssignmentId !== '') {
            $params['site_assignment_id'] = $siteAssignmentId;
        }
        if ($seatCount > 0) {
            $params['seat_count'] = $seatCount;
        }

        $res = $this->management->performOperation($operation, $params);
        if (empty($res['ok'])) {
            $err = isset($res['error']) ? (string) $res['error'] : '';
            wp_safe_redirect($this->pageUrl([
                'tab' => 'billing',
                'wpsn_notice' => 'self_serve_manage_failed',
                'wpsn_error' => $err,
            ]));
            exit;
        }

        wp_safe_redirect($this->pageUrl([
            'tab' => 'billing',
            'wpsn_notice' => 'self_serve_manage_updated',
            'wpsn_message' => isset($res['message']) ? sanitize_text_field((string) $res['message']) : '',
        ]));
        exit;
    }

    public function handleSave(): void
    {
        if (!current_user_can(Capabilities::CAP_LICENSE)) {
            wp_die(esc_html__('You do not have permission to perform this action.', 'wp-seo-next'));
        }

        check_admin_referer('wpsn_license_save');

        $returnTab = isset($_POST['return_tab']) ? sanitize_key((string) wp_unslash($_POST['return_tab'])) : 'settings';
        if (!in_array($returnTab, ['overview', 'billing', 'settings'], true)) {
            $returnTab = 'settings';
        }

        $settings = $this->settingsRepo->getAll();
        $locked = !empty($settings['config_sync']['lock_settings']);

        $key = isset($_POST['license_key']) ? (string) wp_unslash($_POST['license_key']) : '';
        $this->service->setLicenseKey($key);

        if ($locked) {
            wp_safe_redirect(add_query_arg(['page' => self::PAGE_SLUG, 'tab' => $returnTab, 'wpsn_notice' => 'settings_locked'], admin_url('tools.php')));
            exit;
        }

        $serverUrl = isset($_POST['server_url']) ? esc_url_raw((string) wp_unslash($_POST['server_url'])) : '';
        $verifyPath = isset($_POST['verify_path']) ? sanitize_text_field((string) wp_unslash($_POST['verify_path'])) : '/wp-seo-next/license/verify';
        $publicKeyPem = isset($_POST['public_key_pem']) ? sanitize_textarea_field((string) wp_unslash($_POST['public_key_pem'])) : '';
        $updateServerUrl = isset($_POST['update_server_url']) ? esc_url_raw((string) wp_unslash($_POST['update_server_url'])) : '';
        $updatePath = isset($_POST['update_path']) ? sanitize_text_field((string) wp_unslash($_POST['update_path'])) : '/wp-seo-next/plugin/update';
        $enableUpdates = !empty($_POST['enable_updates']);
        $purchaseUrl = isset($_POST['purchase_url']) ? (string) wp_unslash($_POST['purchase_url']) : '';

        $billingPortalUrl = isset($_POST['billing_portal_url']) ? (string) wp_unslash($_POST['billing_portal_url']) : '';
        $accountPortalUrl = isset($_POST['account_portal_url']) ? (string) wp_unslash($_POST['account_portal_url']) : '';
        $invoicesUrl = isset($_POST['invoices_url']) ? (string) wp_unslash($_POST['invoices_url']) : '';
        $changePlanUrl = isset($_POST['change_plan_url']) ? (string) wp_unslash($_POST['change_plan_url']) : '';
        $cancelUrl = isset($_POST['cancel_url']) ? (string) wp_unslash($_POST['cancel_url']) : '';
        $paymentMethodUrl = isset($_POST['payment_method_url']) ? (string) wp_unslash($_POST['payment_method_url']) : '';
        $billingSupportUrl = isset($_POST['billing_support_url']) ? (string) wp_unslash($_POST['billing_support_url']) : '';
        $billingEmbed = !empty($_POST['billing_embed']);

        $billingEmbedHeight = isset($_POST['billing_embed_height']) ? (int) $_POST['billing_embed_height'] : 980;
        $billingEmbedHeight = max(480, min(2000, $billingEmbedHeight));

        $billingSyncEnabled = !empty($_POST['billing_sync_enabled']);
        $billingSyncPath = isset($_POST['billing_sync_path']) ? sanitize_text_field((string) wp_unslash($_POST['billing_sync_path'])) : '/wp-seo-next/billing/sync';
        $billingSyncTimeout = isset($_POST['billing_sync_timeout']) ? (int) $_POST['billing_sync_timeout'] : 15;
        $billingSyncTimeout = max(3, min(60, $billingSyncTimeout));
        $billingSyncCacheTtlHours = isset($_POST['billing_sync_cache_ttl_hours']) ? (int) $_POST['billing_sync_cache_ttl_hours'] : 12;
        $billingSyncCacheTtlHours = max(1, min(168, $billingSyncCacheTtlHours));
        $billingSyncScheduled = !empty($_POST['billing_sync_scheduled']);
        $billingSyncRequireSigned = !empty($_POST['billing_sync_require_signed']);

        $selfServeEnabled = !empty($_POST['self_serve_enabled']);
        $selfServeCatalogPath = isset($_POST['self_serve_catalog_path']) ? sanitize_text_field((string) wp_unslash($_POST['self_serve_catalog_path'])) : '/wp-seo-next/billing/catalog';
        $selfServeCheckoutPath = isset($_POST['self_serve_checkout_path']) ? sanitize_text_field((string) wp_unslash($_POST['self_serve_checkout_path'])) : '/wp-seo-next/billing/checkout';
        $selfServeActionPath = isset($_POST['self_serve_action_path']) ? sanitize_text_field((string) wp_unslash($_POST['self_serve_action_path'])) : '/wp-seo-next/billing/action';
        $selfServeManagePath = isset($_POST['self_serve_manage_path']) ? sanitize_text_field((string) wp_unslash($_POST['self_serve_manage_path'])) : '/wp-seo-next/billing/manage';
        $selfServeTimeout = isset($_POST['self_serve_timeout']) ? (int) $_POST['self_serve_timeout'] : 20;
        $selfServeTimeout = max(3, min(60, $selfServeTimeout));
        $selfServeCacheTtlHours = isset($_POST['self_serve_cache_ttl_hours']) ? (int) $_POST['self_serve_cache_ttl_hours'] : 6;
        $selfServeCacheTtlHours = max(1, min(168, $selfServeCacheTtlHours));
        $selfServeRequireSigned = !empty($_POST['self_serve_require_signed']);

        $allowHostsRaw = isset($_POST['billing_embed_allowed_hosts']) ? (string) wp_unslash($_POST['billing_embed_allowed_hosts']) : '';
        $allowHosts = preg_split('/[\r\n,]+/', $allowHostsRaw) ?: [];
        $allowHosts = array_values(array_filter(array_map(static function ($host): string {
            $host = strtolower(trim(sanitize_text_field($host)));
            if ($host === '') {
                return '';
            }
            $parsed = wp_parse_url($host, PHP_URL_HOST);
            if (is_string($parsed) && $parsed !== '') {
                $host = strtolower($parsed);
            }
            return preg_match('/^[a-z0-9.-]+$/', $host) ? $host : '';
        }, $allowHosts)));

        $cacheTtlHours = isset($_POST['cache_ttl_hours']) ? (int) $_POST['cache_ttl_hours'] : 24;
        $cacheTtlHours = max(1, min(168, $cacheTtlHours));

        $graceDays = isset($_POST['grace_days']) ? (int) $_POST['grace_days'] : 14;
        $graceDays = max(0, min(60, $graceDays));

        $enforce = !empty($_POST['enforce_entitlements']);
        $requireSigned = !empty($_POST['require_signed']);
        $scheduled = !empty($_POST['scheduled_verify']);

        $settings['licensing'] = isset($settings['licensing']) && is_array($settings['licensing']) ? $settings['licensing'] : [];
        $settings['licensing']['server_url'] = $serverUrl;
        $settings['licensing']['verify_path'] = $verifyPath;
        $settings['licensing']['public_key_pem'] = $publicKeyPem;
        $settings['licensing']['update_server_url'] = $updateServerUrl;
        $settings['licensing']['update_path'] = $updatePath;
        $settings['licensing']['enable_updates'] = $enableUpdates;
        $settings['licensing']['purchase_url'] = $purchaseUrl;
        $settings['licensing']['billing_portal_url'] = $billingPortalUrl;
        $settings['licensing']['account_portal_url'] = $accountPortalUrl;
        $settings['licensing']['invoices_url'] = $invoicesUrl;
        $settings['licensing']['change_plan_url'] = $changePlanUrl;
        $settings['licensing']['cancel_url'] = $cancelUrl;
        $settings['licensing']['payment_method_url'] = $paymentMethodUrl;
        $settings['licensing']['billing_support_url'] = $billingSupportUrl;
        $settings['licensing']['billing_embed'] = $billingEmbed;
        $settings['licensing']['billing_embed_height'] = $billingEmbedHeight;
        $settings['licensing']['billing_embed_allowed_hosts'] = array_values(array_unique($allowHosts));
        $settings['licensing']['billing_sync_enabled'] = $billingSyncEnabled;
        $settings['licensing']['billing_sync_path'] = $billingSyncPath;
        $settings['licensing']['billing_sync_timeout'] = $billingSyncTimeout;
        $settings['licensing']['billing_sync_cache_ttl_hours'] = $billingSyncCacheTtlHours;
        $settings['licensing']['billing_sync_scheduled'] = $billingSyncScheduled;
        $settings['licensing']['billing_sync_require_signed'] = $billingSyncRequireSigned;
        $settings['licensing']['self_serve_enabled'] = $selfServeEnabled;
        $settings['licensing']['self_serve_catalog_path'] = $selfServeCatalogPath;
        $settings['licensing']['self_serve_checkout_path'] = $selfServeCheckoutPath;
        $settings['licensing']['self_serve_action_path'] = $selfServeActionPath;
        $settings['licensing']['self_serve_manage_path'] = $selfServeManagePath;
        $settings['licensing']['self_serve_timeout'] = $selfServeTimeout;
        $settings['licensing']['self_serve_cache_ttl_hours'] = $selfServeCacheTtlHours;
        $settings['licensing']['self_serve_require_signed'] = $selfServeRequireSigned;
        $settings['licensing']['cache_ttl_hours'] = $cacheTtlHours;
        $settings['licensing']['grace_days'] = $graceDays;
        $settings['licensing']['enforce_entitlements'] = $enforce;
        $settings['licensing']['require_signed'] = $requireSigned;
        $settings['licensing']['scheduled_verify'] = $scheduled;

        $this->settingsRepo->update($settings);

        do_action('wpsn/license/settings_saved', (int) get_current_user_id(), [
            'enforce' => $enforce,
            'server_url' => $serverUrl,
        ]);

        wp_safe_redirect(add_query_arg(['page' => self::PAGE_SLUG, 'tab' => $returnTab, 'wpsn_notice' => 'license_saved'], admin_url('tools.php')));
        exit;
    }

    public function handleClear(): void
    {
        if (!current_user_can(Capabilities::CAP_LICENSE)) {
            wp_die(esc_html__('You do not have permission to perform this action.', 'wp-seo-next'));
        }

        check_admin_referer('wpsn_license_clear');

        $this->service->clearLicenseKey();
        $this->billingSync->clearCache();
        $this->selfServe->clearCatalogCache();
        $this->selfServe->clearCheckoutSession();
        $this->management->clearCache();

        do_action('wpsn/license/cleared', (int) get_current_user_id());

        wp_safe_redirect(add_query_arg(['page' => self::PAGE_SLUG, 'tab' => 'overview', 'wpsn_notice' => 'license_cleared'], admin_url('tools.php')));
        exit;
    }

    public function handleClearCache(): void
    {
        if (!current_user_can(Capabilities::CAP_LICENSE)) {
            wp_die(esc_html__('You do not have permission to perform this action.', 'wp-seo-next'));
        }

        check_admin_referer('wpsn_license_clear_cache');

        $this->service->clearCache();

        wp_safe_redirect(add_query_arg(['page' => self::PAGE_SLUG, 'tab' => 'overview', 'wpsn_notice' => 'license_cache_cleared'], admin_url('tools.php')));
        exit;
    }

    public function handleClearBillingCache(): void
    {
        if (!current_user_can(Capabilities::CAP_LICENSE)) {
            wp_die(esc_html__('You do not have permission to perform this action.', 'wp-seo-next'));
        }

        check_admin_referer('wpsn_billing_clear_cache');

        $this->billingSync->clearCache();

        wp_safe_redirect(add_query_arg(['page' => self::PAGE_SLUG, 'tab' => 'billing', 'wpsn_notice' => 'billing_cache_cleared'], admin_url('tools.php')));
        exit;
    }

    public function handleBillingSync(): void
    {
        if (!current_user_can(Capabilities::CAP_LICENSE)) {
            wp_die(esc_html__('You do not have permission to perform this action.', 'wp-seo-next'));
        }

        check_admin_referer('wpsn_billing_sync');

        $res = $this->billingSync->syncIfNeeded(true);
        if (empty($res['ok'])) {
            $err = isset($res['error']) ? (string) $res['error'] : '';
            wp_safe_redirect(add_query_arg([
                'page' => self::PAGE_SLUG,
                'tab' => 'billing',
                'wpsn_notice' => 'billing_sync_failed',
                'wpsn_error' => $err,
            ], admin_url('tools.php')));
            exit;
        }

        wp_safe_redirect(add_query_arg(['page' => self::PAGE_SLUG, 'tab' => 'billing', 'wpsn_notice' => 'billing_synced'], admin_url('tools.php')));
        exit;
    }

    public function handleVerify(): void
    {
        if (!current_user_can(Capabilities::CAP_LICENSE)) {
            wp_die(esc_html__('You do not have permission to perform this action.', 'wp-seo-next'));
        }

        check_admin_referer('wpsn_license_verify');

        $res = $this->service->verifyIfNeeded(true);
        if (empty($res['ok'])) {
            $err = isset($res['error']) ? (string) $res['error'] : '';
            wp_safe_redirect(add_query_arg([
                'page' => self::PAGE_SLUG,
                'tab' => 'overview',
                'wpsn_notice' => 'verify_failed',
                'wpsn_error' => $err,
            ], admin_url('tools.php')));
            exit;
        }

        wp_safe_redirect(add_query_arg(['page' => self::PAGE_SLUG, 'tab' => 'overview', 'wpsn_notice' => 'license_verified'], admin_url('tools.php')));
        exit;
    }
}
