<?php
/**
 * Plugin Name: TechDex Client Post Preview
 * Description: Preview unpublished WordPress content as if it were live, then share that same view with clients using expiring links.
 * Version: 0.1.10
 * Author: Dexter Nelson | TechDex Development and Solutions
 * Author URI: https://techdex.net
 * License: GPL-2.0-or-later
 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
 * Text Domain: techdex-client-post-preview
 * Domain Path: /languages
 */

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

final class TechDex_Client_Post_Preview_Plugin {
    private const QUERY_FLAG = 'cpp_preview';
    private const QUERY_POST = 'cpp_post';
    private const QUERY_TOKEN = 'cpp_token';
    private const QUERY_LINK = 'cpp_link';

    private const META_TOKEN = '_cpp_token';
    private const META_TOKEN_HASH = '_cpp_token_hash';
    private const META_EXPIRES_AT = '_cpp_expires_at';
    private const META_CREATED_AT = '_cpp_created_at';
    private const META_CREATED_BY = '_cpp_created_by';
    private const META_LINKS = '_cpp_links';
    private const ENCRYPTED_TOKEN_PREFIX = 'cpp_enc:';
    private const ENCRYPTION_CIPHER = 'aes-256-cbc';

    private const NOTICE_QUERY_ARG = 'cpp_notice';
    private const DASHBOARD_PAGE_SLUG = 'techdex-client-post-preview';
    private const PREVIEW_LINKS_PAGE_SLUG = 'techdex-client-post-preview-preview-links';
    private const GETTING_STARTED_PAGE_SLUG = 'techdex-client-post-preview-getting-started';
    private const README_PAGE_SLUG = 'techdex-client-post-preview-readme';
    private const LICENSE_PAGE_SLUG = 'techdex-client-post-preview-license';
    private const DETAILS_PAGE_SLUG = 'techdex-client-post-preview-details';
    private const LINK_DETAILS_PAGE_SLUG = 'techdex-client-post-preview-link-details';
    private const CHANGELOG_PAGE_SLUG = 'techdex-client-post-preview-changelog';
    private const SUPPORT_PAGE_SLUG = 'techdex-client-post-preview-support';
    private const OPTION_DEFAULT_EXPIRATION_DAYS = 'cpp_default_expiration_days';
    private const OPTION_LOG_RETENTION_DAYS = 'cpp_log_retention_days';
    private const OPTION_LICENSE_KEY = 'cpp_license_key';
    private const OPTION_LICENSE_STATE = 'cpp_license_state';
    private const FREE_ACTIVE_POST_LIMIT = 3;
    private const PRO_ACTIVE_POST_LIMIT = 12;
    private const FREE_LINK_LIMIT = 1;
    private const PRO_LINK_LIMIT = 2;
    private const AGENCY_LINK_LIMIT = 3;
    private const PRO_PURCHASE_URL = 'https://buy.stripe.com/3cI9AT50IdWj16fcWTdEs04';
    private const AGENCY_PURCHASE_URL = 'https://buy.stripe.com/fZu8wP2SA2dB2aje0XdEs05';
    private const UPDATE_SERVICE_URL = 'https://techdex.net/software/techdex-client-post-preview-plugin/release-pulse.php';
    private const UPDATE_CACHE_KEY = 'cpp_update_metadata';
    private const DEFAULT_EXPIRATION = WEEK_IN_SECONDS;
    private const DEFAULT_EXPIRATION_DAYS = 7;
    private const DEFAULT_LOG_RETENTION_DAYS = 30;
    private const ACCESS_LOG_LIMIT = 20;
    private const SUPPORT_EMAIL = 'liveminder@gmail.com';
    private const PRODUCT_KEY = 'techdex-client-post-preview';
    private const VERSION = '0.1.10';
    private const CLEANUP_NOTICE_TRANSIENT = 'cpp_cleanup_notice';

    /**
     * @var WP_Post|null
     */
    private static $active_post = null;

    /**
     * @var array<string,mixed>|null
     */
    private static $active_link = null;

    /**
     * @var bool|null
     */
    private static $request_is_valid = null;

    /**
     * @var bool
     */
    private static $preview_view_logged = false;

    public static function init(): void {
        add_action('plugins_loaded', [__CLASS__, 'maybe_mark_nocache'], 1);

        add_filter('query_vars', [__CLASS__, 'register_query_vars']);
        add_filter('request', [__CLASS__, 'filter_request']);
        add_action('pre_get_posts', [__CLASS__, 'prepare_main_query']);
        add_filter('the_posts', [__CLASS__, 'inject_preview_post'], 10, 2);
        add_action('wp', [__CLASS__, 'normalize_main_query']);
        add_filter('pre_handle_404', [__CLASS__, 'bypass_404_for_preview'], 10, 2);
        add_filter('redirect_canonical', [__CLASS__, 'disable_canonical_redirect'], 10, 2);
        add_action('send_headers', [__CLASS__, 'send_preview_headers']);
        add_action('template_redirect', [__CLASS__, 'redirect_published_preview_request']);
        add_action('template_redirect', [__CLASS__, 'enforce_preview_passcode'], 15);
        add_action('template_redirect', [__CLASS__, 'track_preview_view'], 20);
        add_action('wp_footer', [__CLASS__, 'render_preview_banner']);

        add_action('admin_init', [__CLASS__, 'cleanup_legacy_packaging_artifacts'], 1);
        add_action('admin_init', [__CLASS__, 'register_settings']);
        add_action('admin_menu', [__CLASS__, 'register_management_page']);
        add_action('current_screen', [__CLASS__, 'suppress_third_party_admin_notices']);
        add_action('admin_footer-plugins.php', [__CLASS__, 'set_plugin_author_link_target']);
        add_filter('plugin_action_links_' . plugin_basename(__FILE__), [__CLASS__, 'register_plugin_action_links']);
        add_filter('plugin_row_meta', [__CLASS__, 'register_plugin_row_meta'], 10, 2);
        add_filter('pre_set_site_transient_update_plugins', [__CLASS__, 'filter_plugin_updates']);
        add_filter('plugins_api', [__CLASS__, 'filter_plugin_information'], 10, 3);
        add_action('add_meta_boxes', [__CLASS__, 'add_meta_box']);
        add_action('admin_post_cpp_generate_link', [__CLASS__, 'handle_generate_link']);
        add_action('admin_post_cpp_revoke_link', [__CLASS__, 'handle_revoke_link']);
        add_action('admin_post_cpp_update_link_expiration', [__CLASS__, 'handle_update_link_expiration']);
        add_action('admin_post_cpp_update_link_label', [__CLASS__, 'handle_update_link_label']);
        add_action('admin_post_cpp_update_link_passcode', [__CLASS__, 'handle_update_link_passcode']);
        add_action('admin_post_cpp_clear_link_passcode', [__CLASS__, 'handle_clear_link_passcode']);
        add_action('admin_post_cpp_clear_link_log', [__CLASS__, 'handle_clear_link_log']);
        add_action('admin_post_cpp_manage_link_workflow', [__CLASS__, 'handle_manage_link_workflow']);
        add_action('admin_post_cpp_renew_link', [__CLASS__, 'handle_renew_link']);
        add_action('admin_post_cpp_regenerate_link', [__CLASS__, 'handle_regenerate_link']);
        add_action('admin_post_cpp_toggle_auto_revoke_link', [__CLASS__, 'handle_toggle_auto_revoke_link']);
        add_action('admin_post_cpp_never_expire_link', [__CLASS__, 'handle_never_expire_link']);
        add_action('admin_post_cpp_activate_license', [__CLASS__, 'handle_activate_license']);
        add_action('admin_post_cpp_refresh_license', [__CLASS__, 'handle_refresh_license']);
        add_action('admin_post_cpp_deactivate_license', [__CLASS__, 'handle_deactivate_license']);
        add_action('admin_post_cpp_send_support', [__CLASS__, 'handle_support_request']);
        add_action('admin_notices', [__CLASS__, 'render_admin_notice']);
    }

    public static function maybe_mark_nocache(): void {
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only query check used only to disable page caching for preview requests.
        if (isset($_GET[self::QUERY_FLAG], $_GET[self::QUERY_POST], $_GET[self::QUERY_TOKEN])) {
            // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- Core-recognized constant used to disable caching for preview responses.
            if (!defined('DONOTCACHEPAGE')) {
                // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- Core-recognized constant used to disable caching for preview responses.
                define('DONOTCACHEPAGE', true);
            }
        }
    }

    public static function register_query_vars(array $vars): array {
        $vars[] = self::QUERY_FLAG;
        $vars[] = self::QUERY_POST;
        $vars[] = self::QUERY_TOKEN;
        $vars[] = self::QUERY_LINK;

        return $vars;
    }

    public static function filter_request(array $vars): array {
        $post = self::get_valid_preview_post();

        if (!$post) {
            return $vars;
        }

        $vars['p'] = (int) $post->ID;
        $vars['post_type'] = $post->post_type;
        $vars['name'] = $post->post_name;
        $vars['preview'] = 'true';
        $vars[self::QUERY_FLAG] = '1';
        $vars[self::QUERY_POST] = (string) $post->ID;

        return $vars;
    }

    public static function prepare_main_query(WP_Query $query): void {
        if (!self::is_active_preview_request() || !$query->is_main_query()) {
            return;
        }

        $post = self::get_valid_preview_post();

        if (!$post) {
            return;
        }

        $query->set('p', (int) $post->ID);
        $query->set('post_type', $post->post_type);
        $query->set('post_status', [$post->post_status]);
        $query->set(self::QUERY_FLAG, '1');
        $query->set(self::QUERY_POST, (string) $post->ID);
        $query->set('ignore_sticky_posts', true);
        $query->is_home = false;
        $query->is_singular = true;
        $query->is_single = !is_post_type_hierarchical($post->post_type);
        $query->is_page = is_post_type_hierarchical($post->post_type);
    }

    public static function inject_preview_post(array $posts, WP_Query $query): array {
        if (!self::is_active_preview_request() || !$query->is_main_query()) {
            return $posts;
        }

        $post = self::get_valid_preview_post();

        if (!$post) {
            return $posts;
        }

        return [$post];
    }

    public static function normalize_main_query(): void {
        if (!self::is_active_preview_request()) {
            return;
        }

        $post = self::get_valid_preview_post();

        if (!$post) {
            return;
        }

        global $wp_query;

        $wp_query->posts = [$post];
        $wp_query->post = $post;
        $wp_query->post_count = 1;
        $wp_query->found_posts = 1;
        $wp_query->max_num_pages = 1;
        $wp_query->is_404 = false;
        $wp_query->is_home = false;
        $wp_query->is_archive = false;
        $wp_query->is_singular = true;
        $wp_query->is_single = !is_post_type_hierarchical($post->post_type);
        $wp_query->is_page = is_post_type_hierarchical($post->post_type);
        $wp_query->queried_object = $post;
        $wp_query->queried_object_id = (int) $post->ID;
    }

    public static function bypass_404_for_preview($preempt, WP_Query $query) {
        if (!self::is_active_preview_request() || !$query->is_main_query()) {
            return $preempt;
        }

        return true;
    }

    public static function disable_canonical_redirect($redirect_url, $requested_url) {
        if (self::is_active_preview_request()) {
            return false;
        }

        return $redirect_url;
    }

    public static function send_preview_headers(): void {
        if (!self::is_active_preview_request()) {
            return;
        }

        nocache_headers();
        header('X-Robots-Tag: noindex, nofollow, noarchive', true);
    }

    public static function redirect_published_preview_request(): void {
        if (!self::is_preview_request()) {
            return;
        }

        $post = self::get_requested_preview_post();

        if (!$post || 'publish' !== $post->post_status) {
            return;
        }

        $permalink = get_permalink($post);

        if (!$permalink) {
            return;
        }

        wp_safe_redirect($permalink, 302);
        exit;
    }

    public static function enforce_preview_passcode(): void {
        if (!self::is_active_preview_request()) {
            return;
        }

        $post = self::get_valid_preview_post();

        if (!$post || !is_array(self::$active_link)) {
            return;
        }

        $passcode_hash = (string) (self::$active_link['passcode_hash'] ?? '');

        if ('' === $passcode_hash) {
            return;
        }

        if (self::has_valid_preview_passcode_cookie((string) self::$active_link['id'], $passcode_hash)) {
            return;
        }

        $error = '';

        $request_method = isset($_SERVER['REQUEST_METHOD']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_METHOD'])) : '';

        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce is verified below for the submitted passcode form.
        if ('POST' === strtoupper($request_method) && isset($_POST['cpp_preview_passcode_submit'])) {
            $nonce = isset($_POST['_wpnonce']) ? sanitize_text_field(wp_unslash($_POST['_wpnonce'])) : '';

            if (!wp_verify_nonce($nonce, 'cpp_preview_passcode_' . self::$active_link['id'])) {
                $error = __('That passcode submission could not be verified. Please try again.', 'techdex-client-post-preview');
            } else {
                // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Passcodes must remain raw so punctuation and spacing survive password verification.
                $submitted_passcode = isset($_POST['cpp_preview_passcode']) ? trim((string) wp_unslash($_POST['cpp_preview_passcode'])) : '';

                if ('' !== $submitted_passcode && wp_check_password($submitted_passcode, $passcode_hash)) {
                    self::set_preview_passcode_cookie((string) self::$active_link['id'], $passcode_hash, (int) (self::$active_link['expires_at'] ?? 0));
                    wp_safe_redirect(self::get_current_request_url());
                    exit;
                }

                $error = __('Incorrect passcode. Please try again.', 'techdex-client-post-preview');
            }
        }

        self::render_preview_passcode_gate($post, $error);
        exit;
    }

    public static function track_preview_view(): void {
        if (self::$preview_view_logged || !self::is_active_preview_request()) {
            return;
        }

        $post = self::get_valid_preview_post();

        if (!$post || !is_array(self::$active_link) || empty(self::$active_link['id'])) {
            return;
        }

        $link_id = (string) self::$active_link['id'];
        $links = self::get_preview_links((int) $post->ID);
        $updated = false;

        foreach ($links as &$link) {
            if ((string) $link['id'] !== $link_id) {
                continue;
            }

            $link['view_count'] = (int) ($link['view_count'] ?? 0) + 1;
            $link['last_viewed_at'] = time();
            $log = is_array($link['access_log'] ?? null) ? $link['access_log'] : [];
            array_unshift($log, [
                'timestamp' => $link['last_viewed_at'],
                'ip' => self::get_anonymized_request_ip(),
                'user_agent' => self::get_request_user_agent(),
            ]);
            $link['access_log'] = array_slice($log, 0, self::ACCESS_LOG_LIMIT);
            self::$active_link = $link;
            $updated = true;
            break;
        }
        unset($link);

        if ($updated) {
            self::save_preview_links((int) $post->ID, $links);
            self::$preview_view_logged = true;
        }
    }

    public static function render_preview_banner(): void {
        if (!self::is_active_preview_request()) {
            return;
        }

        $post = self::get_valid_preview_post();

        if (!$post) {
            return;
        }

        $expires_at = is_array(self::$active_link) ? (int) (self::$active_link['expires_at'] ?? 0) : 0;
        $expires_text = $expires_at > 0
            ? sprintf('Link expires %s.', esc_html(wp_date(get_option('date_format') . ' ' . get_option('time_format'), $expires_at)))
            : 'This preview link does not expire.';

        echo '<div style="position:fixed;right:16px;bottom:16px;z-index:99999;padding:12px 16px;border-radius:12px;background:#111827;color:#f9fafb;font:600 14px/1.4 sans-serif;box-shadow:0 14px 38px rgba(0,0,0,.25);max-width:320px;">';
        echo '<div style="margin-bottom:4px;">Client Preview Mode</div>';
        echo '<div style="font-weight:400;opacity:.9;">This page is showing the latest saved version of "' . esc_html(get_the_title($post)) . '". ' . esc_html($expires_text) . '</div>';
        echo '</div>';
    }

    public static function register_management_page(): void {
        add_menu_page(
            __('TechDex Client Post Preview', 'techdex-client-post-preview'),
            __('TechDex Client Post Preview', 'techdex-client-post-preview'),
            'manage_options',
            self::DASHBOARD_PAGE_SLUG,
            [__CLASS__, 'render_dashboard_page'],
            'dashicons-visibility',
            58
        );

        add_submenu_page(
            self::DASHBOARD_PAGE_SLUG,
            __('Dashboard', 'techdex-client-post-preview'),
            __('Dashboard', 'techdex-client-post-preview'),
            'manage_options',
            self::DASHBOARD_PAGE_SLUG,
            [__CLASS__, 'render_dashboard_page']
        );

        add_submenu_page(
            self::DASHBOARD_PAGE_SLUG,
            __('Preview Links', 'techdex-client-post-preview'),
            __('Preview Links', 'techdex-client-post-preview'),
            'manage_options',
            self::PREVIEW_LINKS_PAGE_SLUG,
            [__CLASS__, 'render_preview_links_page']
        );

        add_submenu_page(
            self::DASHBOARD_PAGE_SLUG,
            __('Getting Started', 'techdex-client-post-preview'),
            __('Getting Started', 'techdex-client-post-preview'),
            'manage_options',
            self::GETTING_STARTED_PAGE_SLUG,
            [__CLASS__, 'render_getting_started_page']
        );

        add_submenu_page(
            self::DASHBOARD_PAGE_SLUG,
            __('Readme', 'techdex-client-post-preview'),
            __('Readme', 'techdex-client-post-preview'),
            'manage_options',
            self::README_PAGE_SLUG,
            [__CLASS__, 'render_readme_page']
        );

        add_submenu_page(
            self::DASHBOARD_PAGE_SLUG,
            __('License', 'techdex-client-post-preview'),
            __('License', 'techdex-client-post-preview'),
            'manage_options',
            self::LICENSE_PAGE_SLUG,
            [__CLASS__, 'render_license_page']
        );

        if (self::is_agency_active()) {
            add_submenu_page(
                self::DASHBOARD_PAGE_SLUG,
                __('Details', 'techdex-client-post-preview'),
                __('Details', 'techdex-client-post-preview'),
                'manage_options',
                self::DETAILS_PAGE_SLUG,
                [__CLASS__, 'render_details_summary_page']
            );
        }

        add_submenu_page(
            null,
            __('Link Details', 'techdex-client-post-preview'),
            __('Link Details', 'techdex-client-post-preview'),
            'manage_options',
            self::LINK_DETAILS_PAGE_SLUG,
            [__CLASS__, 'render_link_details_page']
        );

        add_submenu_page(
            null,
            __('Changelog', 'techdex-client-post-preview'),
            __('Changelog', 'techdex-client-post-preview'),
            'manage_options',
            self::CHANGELOG_PAGE_SLUG,
            [__CLASS__, 'render_changelog_page']
        );

        add_submenu_page(
            self::DASHBOARD_PAGE_SLUG,
            __('Support', 'techdex-client-post-preview'),
            __('Support', 'techdex-client-post-preview'),
            'manage_options',
            self::SUPPORT_PAGE_SLUG,
            [__CLASS__, 'render_support_page']
        );
    }

    public static function register_settings(): void {
        register_setting(
            'cpp_settings',
            self::OPTION_DEFAULT_EXPIRATION_DAYS,
            [
                'type' => 'integer',
                'sanitize_callback' => [__CLASS__, 'sanitize_default_expiration_days'],
                'default' => self::DEFAULT_EXPIRATION_DAYS,
            ]
        );

        register_setting(
            'cpp_settings',
            self::OPTION_LOG_RETENTION_DAYS,
            [
                'type' => 'integer',
                'sanitize_callback' => [__CLASS__, 'sanitize_log_retention_days'],
                'default' => self::DEFAULT_LOG_RETENTION_DAYS,
            ]
        );

        register_setting(
            'cpp_license_settings',
            self::OPTION_LICENSE_KEY,
            [
                'type' => 'string',
                'sanitize_callback' => [__CLASS__, 'sanitize_license_key'],
                'default' => '',
            ]
        );
    }

    public static function register_plugin_action_links(array $links): array {
        $action_links = [
            '<a href="' . esc_url(self::get_dashboard_page_url()) . '">' . esc_html__('Settings', 'techdex-client-post-preview') . '</a>',
            '<a href="' . esc_url(self::get_getting_started_page_url()) . '">' . esc_html__('Getting Started', 'techdex-client-post-preview') . '</a>',
        ];

        return array_merge($action_links, $links);
    }

    public static function register_plugin_row_meta(array $links, string $file): array {
        if (plugin_basename(__FILE__) !== $file) {
            return $links;
        }

        $details_url = self::get_plugin_details_url();

        foreach ($links as $link) {
            if (!is_string($link)) {
                continue;
            }

            if (false !== strpos($link, 'open-plugin-details-modal') || false !== strpos($link, $details_url) || false !== stripos($link, 'View Details')) {
                return $links;
            }
        }

        $links[] = '<a href="' . esc_url($details_url) . '" class="thickbox open-plugin-details-modal" aria-label="' . esc_attr__('View TechDex Client Post Preview details', 'techdex-client-post-preview') . '">' . esc_html__('View Details', 'techdex-client-post-preview') . '</a>';

        return $links;
    }

    public static function set_plugin_author_link_target(): void {
        $plugin_row_id = 'techdex-client-post-preview-techdex-client-post-preview-php';
        $author_url = 'https://techdex.net';
        $author_selector = 'a[href="' . $author_url . '"]';

        echo '<script>';
        echo 'document.addEventListener("DOMContentLoaded",function(){';
        echo 'var row=document.getElementById(' . wp_json_encode($plugin_row_id) . ');';
        echo 'if(!row){return;}';
        echo 'var links=row.querySelectorAll(' . wp_json_encode($author_selector) . ');';
        echo 'links.forEach(function(link){link.setAttribute("target","_blank");link.setAttribute("rel","noopener noreferrer");});';
        echo '});';
        echo '</script>';
    }

    public static function add_meta_box(): void {
        foreach (self::get_supported_post_types() as $post_type) {
            add_meta_box(
                'techdex-client-post-preview',
                __('Client Preview Link', 'techdex-client-post-preview'),
                [__CLASS__, 'render_meta_box'],
                $post_type,
                'side',
                'high'
            );
        }
    }

    public static function render_meta_box(WP_Post $post): void {
        if (!current_user_can('edit_post', $post->ID)) {
            echo '<p>' . esc_html__('You do not have permission to manage preview links for this post.', 'techdex-client-post-preview') . '</p>';
            return;
        }

        if ('auto-draft' === $post->post_status) {
            echo '<p>' . esc_html__('Save the post once before generating a preview link.', 'techdex-client-post-preview') . '</p>';
            return;
        }

        $is_preview_eligible = self::is_post_preview_eligible($post);
        $links = self::get_preview_links((int) $post->ID);
        $has_link = !empty($links);
        $link_limit = self::get_preview_link_limit();
        $link_count = count($links);
        $slot_limit_reached = $is_preview_eligible && !$has_link && !self::can_generate_preview_for_post((int) $post->ID);
        $link_limit_reached = $is_preview_eligible && $has_link && $link_count >= $link_limit;
        $management_url = self::get_preview_links_page_url(['post_id' => (int) $post->ID]);

        $generate_url = wp_nonce_url(
            admin_url('admin-post.php?action=cpp_generate_link&post_id=' . $post->ID),
            'cpp_generate_link_' . $post->ID
        );

        echo '<p>' . esc_html__('Preview this unpublished post, page, or supported custom post type in a published-like front-end view, then share that same expiring link when someone else needs to review it.', 'techdex-client-post-preview') . '</p>';
        echo '<p><strong>' . esc_html__('Post ID:', 'techdex-client-post-preview') . '</strong> ' . esc_html((string) $post->ID) . '</p>';
        /* translators: 1: current number of preview links on this post, 2: preview link limit for the current plan. */
        echo '<p><strong>' . esc_html__('Preview links:', 'techdex-client-post-preview') . '</strong> ' . esc_html(sprintf('%d of %d', $link_count, $link_limit)) . '</p>';

        if (!$is_preview_eligible && 'publish' === $post->post_status) {
            echo '<p><strong>' . esc_html__('This post is live on the site.', 'techdex-client-post-preview') . '</strong></p>';
            echo '<p>' . esc_html__('Preview links are inactive while a post is published, but the saved preview slot still stays in use until you revoke the links. If you move this post back to an unpublished status, the saved links become available again if they still exist.', 'techdex-client-post-preview') . '</p>';
        } elseif ($has_link) {
            echo '<p><strong>' . esc_html__('Saved preview links', 'techdex-client-post-preview') . '</strong></p>';

            foreach ($links as $link) {
                $preview_url = self::build_preview_url((int) $post->ID, (string) $link['id']);
                $status_markup = self::get_editor_status_markup((int) $post->ID, (int) $link['created_at'], (int) $link['expires_at'], $management_url);
                $revoke_url = wp_nonce_url(
                    admin_url('admin-post.php?action=cpp_revoke_link&post_id=' . $post->ID . '&link_id=' . rawurlencode((string) $link['id'])),
                    'cpp_revoke_link_' . $post->ID . '_' . $link['id']
                );

                echo '<div style="margin-bottom:12px;padding:10px;border:1px solid #dcdcde;border-radius:6px;background:#fff;">';
                /* translators: %d: preview link position number. */
                echo '<p style="margin:0 0 8px;"><strong>' . esc_html(sprintf(__('Preview Link %d', 'techdex-client-post-preview'), (int) $link['position'])) . '</strong></p>';
                echo '<p style="margin:0 0 8px;"><input type="text" readonly style="width:100%;" value="' . esc_attr($preview_url) . '" onclick="this.select();" /></p>';
                echo $status_markup; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped

                if ((int) $link['expires_at'] > 0) {
                    echo '<p>' . esc_html(
                        sprintf(
                            /* translators: %s: formatted expiration date and time. */
                            __('Expires: %s', 'techdex-client-post-preview'),
                            wp_date(get_option('date_format') . ' ' . get_option('time_format'), (int) $link['expires_at'])
                        )
                    ) . '</p>';
                } else {
                    echo '<p>' . esc_html__('Never expires.', 'techdex-client-post-preview') . '</p>';
                }

                echo '<p style="margin:8px 0 0;display:flex;gap:8px;flex-wrap:wrap;">';
                echo '<a class="button button-secondary" href="' . esc_url($preview_url) . '" target="_blank" rel="noopener noreferrer">' . esc_html__('Open Preview in New Tab', 'techdex-client-post-preview') . '</a>';
                echo '<a class="button" href="' . esc_url($revoke_url) . '">' . esc_html__('Revoke Link', 'techdex-client-post-preview') . '</a>';
                echo '</p>';
                echo '</div>';
            }
        } else {
            echo '<p>' . esc_html__('No preview links are active yet.', 'techdex-client-post-preview') . '</p>';
        }

        if ($slot_limit_reached) {
            echo '<p style="color:#b32d2e;"><strong>' . esc_html__('Free preview slot limit reached.', 'techdex-client-post-preview') . '</strong> ' . esc_html__('You can manage up to 3 preview slots in the free version. Revoke one from Settings or upgrade to Pro for up to 12.', 'techdex-client-post-preview') . '</p>';
            echo '<p><a class="button button-secondary" href="' . esc_url(self::get_preview_links_page_url()) . '">' . esc_html__('Manage Preview Slots', 'techdex-client-post-preview') . '</a></p>';
        } elseif ($link_limit_reached) {
            echo '<p style="color:#646970;"><strong>' . esc_html__('Link limit reached for this post.', 'techdex-client-post-preview') . '</strong> ' . esc_html__('Manage or revoke one of the existing preview links before adding another.', 'techdex-client-post-preview') . '</p>';
        } elseif ($is_preview_eligible) {
            echo '<p><a class="button button-primary" href="' . esc_url($generate_url) . '">' . esc_html($has_link ? __('Add Preview Link', 'techdex-client-post-preview') : __('Generate Link', 'techdex-client-post-preview')) . '</a></p>';
        }

        echo '<p style="margin-top:12px;color:#50575e;">' . esc_html__('Tip: every time you save the post, the preview link shows the updated saved draft automatically.', 'techdex-client-post-preview') . '</p>';
    }

    public static function handle_generate_link(): void {
        $post_id = self::get_admin_post_id();

        if (!$post_id) {
            wp_die(esc_html__('Invalid post ID.', 'techdex-client-post-preview'));
        }

        if (!current_user_can('edit_post', $post_id)) {
            wp_die(esc_html__('You do not have permission to create preview links for this post.', 'techdex-client-post-preview'));
        }

        check_admin_referer('cpp_generate_link_' . $post_id);

        if (!self::can_generate_preview_for_post($post_id)) {
            $redirect_url = add_query_arg(
                [
                    'post' => $post_id,
                    'action' => 'edit',
                    self::NOTICE_QUERY_ARG => 'limit_reached',
                ],
                admin_url('post.php')
            );

            wp_safe_redirect($redirect_url);
            exit;
        }

        $links = self::get_preview_links($post_id);

        if (count($links) >= self::get_preview_link_limit()) {
            $redirect_url = add_query_arg(
                [
                    'post' => $post_id,
                    'action' => 'edit',
                    self::NOTICE_QUERY_ARG => 'link_limit_reached',
                ],
                admin_url('post.php')
            );

            wp_safe_redirect($redirect_url);
            exit;
        }

        $token = wp_generate_password(48, false, false);
        $links[] = self::create_preview_link_record($token);
        self::save_preview_links($post_id, $links);

        $redirect_url = add_query_arg(
            [
                'post' => $post_id,
                'action' => 'edit',
                self::NOTICE_QUERY_ARG => 'generated',
            ],
            admin_url('post.php')
        );

        wp_safe_redirect($redirect_url);
        exit;
    }

    public static function handle_update_link_expiration(): void {
        $post_id = self::get_admin_post_id();
        $link_id = self::get_admin_link_id();

        if (!$post_id) {
            wp_die(esc_html__('Invalid post ID.', 'techdex-client-post-preview'));
        }

        if (!current_user_can('edit_post', $post_id)) {
            wp_die(esc_html__('You do not have permission to update this preview link.', 'techdex-client-post-preview'));
        }

        if ('' === $link_id) {
            wp_die(esc_html__('No preview link was selected for updating.', 'techdex-client-post-preview'));
        }

        check_admin_referer('cpp_update_link_expiration_' . $post_id . '_' . $link_id);

        if (!self::is_pro_active()) {
            wp_die(esc_html__('Per-link expiration control is only available on paid plans.', 'techdex-client-post-preview'));
        }

        if (!self::has_saved_preview_link($post_id) || '' === $link_id) {
            wp_die(esc_html__('No saved preview link was found for this post.', 'techdex-client-post-preview'));
        }

        $expiration_days = self::get_requested_expiration_days();
        $now = time();
        $updated = false;
        $links = self::get_preview_links($post_id);

        foreach ($links as &$link) {
            if ((string) $link['id'] !== $link_id) {
                continue;
            }

            $link['created_at'] = $now;
            $link['expires_at'] = $now + self::get_expiration_window($expiration_days);
            $updated = true;
            break;
        }
        unset($link);

        if (!$updated) {
            wp_die(esc_html__('That preview link could not be found.', 'techdex-client-post-preview'));
        }

        self::save_preview_links($post_id, $links);

        wp_safe_redirect(self::get_preview_links_page_url(['post_id' => $post_id]));
        exit;
    }

    public static function handle_update_link_label(): void {
        self::handle_agency_link_setting_update('label');
    }

    public static function handle_update_link_passcode(): void {
        self::handle_agency_link_setting_update('passcode');
    }

    public static function handle_clear_link_passcode(): void {
        self::handle_agency_link_setting_update('clear_passcode');
    }

    public static function handle_clear_link_log(): void {
        self::handle_agency_link_setting_update('clear_log');
    }

    public static function handle_renew_link(): void {
        self::handle_link_lifecycle_action('renew');
    }

    public static function handle_manage_link_workflow(): void {
        $post_id = self::get_admin_post_id();
        $link_id = self::get_admin_link_id();

        if (!$post_id || '' === $link_id) {
            wp_die(esc_html__('No preview link was selected.', 'techdex-client-post-preview'));
        }

        check_admin_referer('cpp_manage_link_workflow_' . $post_id . '_' . $link_id);

        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified immediately above for this workflow form.
        $workflow = isset($_POST['link_workflow']) ? sanitize_key(wp_unslash($_POST['link_workflow'])) : '';

        if (!in_array($workflow, ['renew', 'regenerate', 'never_expire', 'toggle_auto_revoke'], true)) {
            wp_die(esc_html__('That preview-link workflow is not available.', 'techdex-client-post-preview'));
        }

        self::handle_link_lifecycle_action($workflow);
    }

    public static function handle_regenerate_link(): void {
        self::handle_link_lifecycle_action('regenerate');
    }

    public static function handle_toggle_auto_revoke_link(): void {
        self::handle_link_lifecycle_action('toggle_auto_revoke');
    }

    public static function handle_never_expire_link(): void {
        self::handle_link_lifecycle_action('never_expire');
    }

    public static function handle_revoke_link(): void {
        $post_id = self::get_admin_post_id();
        $link_id = self::get_safe_link_id_for_revoke($post_id, self::get_admin_link_id());

        if (!$post_id) {
            wp_die(esc_html__('Invalid post ID.', 'techdex-client-post-preview'));
        }

        if (!current_user_can('edit_post', $post_id)) {
            wp_die(esc_html__('You do not have permission to revoke preview links for this post.', 'techdex-client-post-preview'));
        }

        if ('' === $link_id) {
            wp_die(esc_html__('No preview link was selected for revocation.', 'techdex-client-post-preview'));
        }

        check_admin_referer('cpp_revoke_link_' . $post_id . '_' . $link_id);

        $links = array_values(array_filter(
            self::get_preview_links($post_id),
            static function (array $link) use ($link_id): bool {
                return (string) $link['id'] !== $link_id;
            }
        ));

        self::save_preview_links($post_id, $links);

        wp_safe_redirect(self::get_revoke_redirect_url($post_id));
        exit;
    }

    public static function handle_activate_license(): void {
        self::handle_license_action('activate');
    }

    public static function handle_refresh_license(): void {
        self::handle_license_action('check');
    }

    public static function handle_deactivate_license(): void {
        self::handle_license_action('deactivate');
    }

    public static function render_admin_notice(): void {
        $cleanup_notice = get_transient(self::CLEANUP_NOTICE_TRANSIENT);

        if (false !== $cleanup_notice) {
            delete_transient(self::CLEANUP_NOTICE_TRANSIENT);

            $cleanup_count = is_numeric($cleanup_notice) ? (int) $cleanup_notice : 0;

            if ($cleanup_count > 0) {
                echo '<div class="notice notice-warning is-dismissible"><p>' . esc_html(
                    sprintf(
                        /* translators: %d: number of malformed legacy files removed */
                        __('TechDex Client Post Preview removed %d malformed legacy packaging file(s) with backslashes in the filename. Future updates should behave more cleanly now.', 'techdex-client-post-preview'),
                        $cleanup_count
                    )
                ) . '</p></div>';
            }
        }

        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin notice lookup from query string.
        if (!is_admin() || !isset($_GET[self::NOTICE_QUERY_ARG])) {
            return;
        }

        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin notice lookup from query string.
        $notice = sanitize_key(wp_unslash($_GET[self::NOTICE_QUERY_ARG]));
        $messages = [
            'generated' => __('Client preview link generated successfully.', 'techdex-client-post-preview'),
            'revoked' => __('Client preview link revoked successfully.', 'techdex-client-post-preview'),
            'limit_reached' => __('Free version limit reached: you can have up to 3 preview-enabled posts at once. Pro increases that to 12.', 'techdex-client-post-preview'),
            'link_limit_reached' => __('This post has already reached its preview-link limit for the current plan.', 'techdex-client-post-preview'),
            'license_missing_key' => __('Save a license key before trying to activate this site.', 'techdex-client-post-preview'),
            'license_activated' => __('License activated successfully for this site.', 'techdex-client-post-preview'),
            'license_checked' => __('License status refreshed successfully.', 'techdex-client-post-preview'),
            'license_deactivated' => __('License deactivated successfully for this site.', 'techdex-client-post-preview'),
            'license_invalid' => __('That license key could not be validated for this product.', 'techdex-client-post-preview'),
            'license_activation_limit_reached' => __('This license has already reached its allowed number of site activations.', 'techdex-client-post-preview'),
            'license_expired' => __('This license is expired. Renew it in TechDex before using paid features here.', 'techdex-client-post-preview'),
            'license_revoked' => __('This license has been revoked in TechDex.', 'techdex-client-post-preview'),
            'license_disabled' => __('This license is currently disabled in TechDex.', 'techdex-client-post-preview'),
            'license_request_failed' => __('The plugin could not reach the TechDex licensing service. Please try again.', 'techdex-client-post-preview'),
            'link_renewed' => __('Preview link renewed successfully.', 'techdex-client-post-preview'),
            'link_regenerated' => __('Preview link regenerated successfully.', 'techdex-client-post-preview'),
            'link_auto_revoke_updated' => __('Auto-revoke setting updated successfully.', 'techdex-client-post-preview'),
            'link_never_expires' => __('Preview link updated to never expire.', 'techdex-client-post-preview'),
            'link_label_updated' => __('Preview link label updated successfully.', 'techdex-client-post-preview'),
            'link_passcode_updated' => __('Preview link passcode updated successfully.', 'techdex-client-post-preview'),
            'link_passcode_cleared' => __('Preview link passcode cleared successfully.', 'techdex-client-post-preview'),
            'link_log_cleared' => __('Preview link access log cleared successfully.', 'techdex-client-post-preview'),
            'support_sent' => __('Your support request was sent successfully.', 'techdex-client-post-preview'),
            'support_failed' => __('Support request could not be sent. Please try again.', 'techdex-client-post-preview'),
        ];

        if (!isset($messages[$notice])) {
            return;
        }

        $notice_class = 'notice notice-success is-dismissible';

        if (in_array($notice, ['support_failed', 'limit_reached', 'link_limit_reached', 'license_missing_key', 'license_invalid', 'license_activation_limit_reached', 'license_expired', 'license_revoked', 'license_disabled', 'license_request_failed'], true)) {
            $notice_class = 'notice notice-error is-dismissible';
        }

        echo '<div class="' . esc_attr($notice_class) . '"><p>' . esc_html($messages[$notice]) . '</p></div>';
    }

    public static function cleanup_legacy_packaging_artifacts(): void {
        if (!is_admin() || !current_user_can('activate_plugins')) {
            return;
        }

        $plugin_root = trailingslashit(plugin_dir_path(__FILE__));
        $entries = @scandir($plugin_root);

        if (!is_array($entries)) {
            return;
        }

        $cleanup_count = 0;

        foreach ($entries as $entry) {
            if ('.' === $entry || '..' === $entry || false === strpos($entry, '\\')) {
                continue;
            }

            $path = $plugin_root . $entry;

            if (self::delete_legacy_artifact_path($path)) {
                $cleanup_count++;
            }
        }

        if ($cleanup_count > 0) {
            set_transient(self::CLEANUP_NOTICE_TRANSIENT, $cleanup_count, MINUTE_IN_SECONDS * 10);
        }
    }

    private static function delete_legacy_artifact_path(string $path): bool {
        if (!file_exists($path)) {
            return false;
        }

        require_once ABSPATH . 'wp-admin/includes/file.php';

        if (!function_exists('WP_Filesystem')) {
            return false;
        }

        global $wp_filesystem;

        if (!$wp_filesystem || !is_object($wp_filesystem)) {
            WP_Filesystem();
        }

        if (!$wp_filesystem || !is_object($wp_filesystem)) {
            return false;
        }

        if (is_dir($path)) {
            return (bool) $wp_filesystem->delete($path, true, 'd');
        }

        return wp_delete_file($path);
    }

    public static function suppress_third_party_admin_notices($screen): void {
        if (!is_object($screen) || !isset($screen->id) || !self::is_plugin_admin_screen((string) $screen->id)) {
            return;
        }

        remove_all_actions('admin_notices');
        remove_all_actions('all_admin_notices');
        add_action('admin_notices', [__CLASS__, 'render_admin_notice']);
    }

    public static function render_dashboard_page(): void {
        if (!current_user_can('manage_options')) {
            wp_die(esc_html__('You do not have permission to view this page.', 'techdex-client-post-preview'));
        }

        $saved_count = self::get_saved_preview_posts_count();
        $total_count = count(self::get_preview_posts());
        $has_paid_license = self::is_pro_active();
        $current_plan = self::get_current_plan_key();

        self::render_admin_page_header(
            'dashboard',
            __('About TechDex Client Post Preview', 'techdex-client-post-preview'),
            __('Preview unpublished content in a published-like view and share it when outside review is needed.', 'techdex-client-post-preview')
        );

        echo '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px;margin:20px 0;">';
        echo '<div style="background:#fff;border:1px solid #dcdcde;padding:18px;border-radius:8px;"><h2 style="margin-top:0;">' . esc_html__('Preview Slots Used', 'techdex-client-post-preview') . '</h2><p style="font-size:28px;margin:0;">' . esc_html((string) $saved_count) . ' <span style="font-size:15px;color:#646970;">/ ' . esc_html((string) self::get_preview_slot_limit()) . '</span></p></div>';
        echo '<div style="background:#fff;border:1px solid #dcdcde;padding:18px;border-radius:8px;"><h2 style="margin-top:0;">' . esc_html__('Tracked Preview Records', 'techdex-client-post-preview') . '</h2><p style="font-size:28px;margin:0;">' . esc_html((string) $total_count) . '</p></div>';
        echo '<div style="background:#fff;border:1px solid #dcdcde;padding:18px;border-radius:8px;"><h2 style="margin-top:0;">' . esc_html__('Default Expiration', 'techdex-client-post-preview') . '</h2><p style="font-size:28px;margin:0;">' . esc_html((string) self::get_default_expiration_days()) . ' <span style="font-size:15px;color:#646970;">' . esc_html__('days', 'techdex-client-post-preview') . '</span></p></div>';
        echo '</div>';

        echo '<div style="background:#fff;border:1px solid #dcdcde;padding:20px;border-radius:8px;margin-bottom:20px;">';
        echo '<h2 style="margin-top:0;">' . esc_html__('About TechDex Client Post Preview', 'techdex-client-post-preview') . '</h2>';
        echo '<p>' . esc_html__('TechDex Client Post Preview lets publishers, editors, and authors view unpublished WordPress posts, pages, and supported custom post types in a published-like front-end view before anything goes live.', 'techdex-client-post-preview') . '</p>';
        echo '<p>' . esc_html__('Use it for editorial QA, layout checks, and client review, then manage expirations and active preview links in one place without publishing content early.', 'techdex-client-post-preview') . '</p>';
        echo '<p><a href="' . esc_url(self::get_support_page_url()) . '">' . esc_html__('Get support or leave feedback', 'techdex-client-post-preview') . '</a></p>';
        echo '</div>';

        echo '<div style="background:#fff;border:1px solid #dcdcde;padding:20px;border-radius:8px;margin-bottom:20px;">';
        echo '<div style="display:flex;flex-wrap:wrap;gap:20px;align-items:center;">';
        echo '<div style="flex:0 0 160px;">';
        echo '<img src="' . esc_url(self::get_asset_url('dexter-nelson-headshot.jpg')) . '" alt="' . esc_attr__('Dexter Nelson', 'techdex-client-post-preview') . '" style="display:block;width:160px;height:160px;object-fit:cover;border-radius:18px;border:1px solid #dcdcde;box-shadow:0 10px 24px rgba(0,0,0,.08);" />';
        echo '</div>';
        echo '<div style="flex:1 1 320px;min-width:260px;">';
        echo '<h2 style="margin-top:0;">' . esc_html__('About the Creator', 'techdex-client-post-preview') . '</h2>';
        echo '<p>' . esc_html__('TechDex Client Post Preview was created by Dexter Nelson, a developer, publisher, and builder focused on practical digital tools that make client workflows smoother and more human.', 'techdex-client-post-preview') . '</p>';
        echo '<p>' . esc_html__('The goal behind this plugin is simple: help publishers share work-in-progress content professionally, gather feedback faster, and keep momentum moving without forcing clients into WordPress accounts.', 'techdex-client-post-preview') . '</p>';
        echo '<p><a href="https://dexternelson.com" target="_blank" rel="noopener noreferrer">' . esc_html__('Visit dexternelson.com to learn more and explore other projects', 'techdex-client-post-preview') . '</a></p>';
        echo '</div>';
        echo '</div>';
        echo '</div>';

        echo '<div style="background:#fff;border:1px solid #dcdcde;padding:20px;border-radius:8px;margin-bottom:20px;">';
        echo '<h2 style="margin-top:0;">' . esc_html__('Quick Start', 'techdex-client-post-preview') . '</h2>';
        echo '<ol style="margin:0 0 0 18px;">';
        echo '<li>' . esc_html__('Open a post or page draft and save it.', 'techdex-client-post-preview') . '</li>';
        echo '<li>' . esc_html__('Generate a client preview link from the editor sidebar.', 'techdex-client-post-preview') . '</li>';
        echo '<li>' . esc_html__('Share the link with your client so they can review the latest saved version.', 'techdex-client-post-preview') . '</li>';
        echo '<li>' . esc_html__('Manage, revoke, or review those links from the Preview Links tab.', 'techdex-client-post-preview') . '</li>';
        echo '</ol>';
        echo '</div>';

        echo '<div style="margin-bottom:20px;">';
        echo '<h2 style="margin:0 0 12px;">' . esc_html__('Free vs Pro', 'techdex-client-post-preview') . '</h2>';
        echo '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:16px;">';

        echo '<div style="background:#fff;border:1px solid #dcdcde;padding:20px;border-radius:8px;">';
        if ('free' === $current_plan) {
            echo '<p style="margin:0 0 10px;color:#008a20;font-weight:600;">' . esc_html__('Your plan', 'techdex-client-post-preview') . '</p>';
        }
        echo '<h3 style="margin-top:0;">' . esc_html__('Free', 'techdex-client-post-preview') . '</h3>';
        echo '<p style="font-size:24px;margin:0 0 12px;">' . esc_html__('$0', 'techdex-client-post-preview') . '</p>';
        echo '<ul style="margin:0 0 0 18px;">';
        echo '<li>' . esc_html__('1 site activation', 'techdex-client-post-preview') . '</li>';
        echo '<li>' . esc_html__('Up to 3 preview-enabled posts', 'techdex-client-post-preview') . '</li>';
        echo '<li>' . esc_html__('One active preview link per post', 'techdex-client-post-preview') . '</li>';
        echo '<li>' . esc_html__('Fixed 7-day expiration', 'techdex-client-post-preview') . '</li>';
        echo '<li>' . esc_html__('Preview management dashboard and support form', 'techdex-client-post-preview') . '</li>';
        echo '</ul>';
        echo '</div>';

        echo '<div style="background:#fff;border:1px solid #2271b1;padding:20px;border-radius:8px;box-shadow:0 0 0 1px rgba(34,113,177,.08);">';
        if ('pro' === $current_plan) {
            echo '<p style="margin:0 0 10px;color:#008a20;font-weight:600;">' . esc_html__('Your plan', 'techdex-client-post-preview') . '</p>';
        }
        echo '<h3 style="margin-top:0;">' . esc_html__('Pro', 'techdex-client-post-preview') . '</h3>';
        echo '<p style="font-size:24px;margin:0 0 12px;">' . esc_html__('$29.99/year', 'techdex-client-post-preview') . '</p>';
        echo '<ul style="margin:0 0 0 18px;">';
        echo '<li>' . esc_html__('3 site activations', 'techdex-client-post-preview') . '</li>';
        echo '<li>' . esc_html__('Up to 12 preview-enabled posts', 'techdex-client-post-preview') . '</li>';
        echo '<li>' . esc_html__('Up to 2 preview links per post', 'techdex-client-post-preview') . '</li>';
        echo '<li>' . esc_html__('Custom default expiration and per-link expiration control', 'techdex-client-post-preview') . '</li>';
        echo '</ul>';
        echo '<p style="margin:16px 0 0;">';
        if (in_array($current_plan, ['pro', 'agency'], true)) {
            echo '<button type="button" class="button button-secondary" disabled="disabled" aria-disabled="true">' . esc_html__('Get Pro', 'techdex-client-post-preview') . '</button>';
        } else {
            echo '<a class="button button-secondary" href="' . esc_url(self::PRO_PURCHASE_URL) . '" target="_blank" rel="noopener noreferrer">' . esc_html__('Get Pro', 'techdex-client-post-preview') . '</a>';
        }
        echo '</p>';
        echo '</div>';

        echo '<div style="background:#fff;border:1px dashed #c3c4c7;padding:20px;border-radius:8px;">';
        if ('agency' === $current_plan) {
            echo '<p style="margin:0 0 10px;color:#008a20;font-weight:600;">' . esc_html__('Your plan', 'techdex-client-post-preview') . '</p>';
        }
        echo '<h3 style="margin-top:0;">' . esc_html__('Agency', 'techdex-client-post-preview') . '</h3>';
        echo '<p style="font-size:24px;margin:0 0 12px;">' . esc_html__('$99.99/year', 'techdex-client-post-preview') . '</p>';
        echo '<ul style="margin:0 0 0 18px;">';
        echo '<li>' . esc_html__('12 site activations', 'techdex-client-post-preview') . '</li>';
        echo '<li>' . esc_html__('Unlimited preview-enabled posts', 'techdex-client-post-preview') . '</li>';
        echo '<li>' . esc_html__('Up to 3 preview links per post', 'techdex-client-post-preview') . '</li>';
        echo '<li>' . esc_html__('Password-protected preview links', 'techdex-client-post-preview') . '</li>';
        echo '<li>' . esc_html__('Link labeling and organization', 'techdex-client-post-preview') . '</li>';
        echo '<li>' . esc_html__('Access logs and view tracking', 'techdex-client-post-preview') . '</li>';
        echo '<li>' . esc_html__('Never expire, renew, auto-revoke, and regenerate workflows', 'techdex-client-post-preview') . '</li>';
        echo '<li>' . esc_html__('Agency details dashboard with totals and per-link summaries', 'techdex-client-post-preview') . '</li>';
        echo '</ul>';
        echo '<p style="margin:16px 0 0;">';
        if ('agency' === $current_plan) {
            echo '<button type="button" class="button button-secondary" disabled="disabled" aria-disabled="true">' . esc_html__('Get Agency', 'techdex-client-post-preview') . '</button>';
        } else {
            echo '<a class="button button-secondary" href="' . esc_url(self::AGENCY_PURCHASE_URL) . '" target="_blank" rel="noopener noreferrer">' . esc_html__('Get Agency', 'techdex-client-post-preview') . '</a>';
        }
        echo '</p>';
        echo '</div>';

        echo '</div>';
        echo '</div>';

        echo '<div style="background:#fff;border:1px solid #dcdcde;padding:20px;border-radius:8px;">';
        echo '<h2 style="margin-top:0;">' . esc_html__('Default Settings', 'techdex-client-post-preview') . '</h2>';
        echo '<form method="post" action="options.php">';
        settings_fields('cpp_settings');
        echo '<table class="form-table" role="presentation">';
        echo '<tr>';
        echo '<th scope="row"><label for="' . esc_attr(self::OPTION_DEFAULT_EXPIRATION_DAYS) . '">' . esc_html__('Default expiration (days)', 'techdex-client-post-preview') . '</label></th>';
        echo '<td>';
        echo '<input type="number" min="1" step="1" class="small-text" id="' . esc_attr(self::OPTION_DEFAULT_EXPIRATION_DAYS) . '" name="' . esc_attr(self::OPTION_DEFAULT_EXPIRATION_DAYS) . '" value="' . esc_attr((string) self::get_default_expiration_days()) . '"' . ($has_paid_license ? '' : ' disabled="disabled" style="background:#f6f7f7;color:#50575e;"') . ' />';
        echo '<p class="description">' . esc_html(
            $has_paid_license
                ? __('Your active paid license unlocks the default expiration setting for this site.', 'techdex-client-post-preview')
                : __('Free version uses a fixed 7-day default expiration. Upgrade to Pro to customize the default expiration.', 'techdex-client-post-preview')
        ) . '</p>';
        echo '</td>';
        echo '</tr>';
        echo '<tr>';
        echo '<th scope="row"><label for="' . esc_attr(self::OPTION_LOG_RETENTION_DAYS) . '">' . esc_html__('Access log retention', 'techdex-client-post-preview') . '</label></th>';
        echo '<td>';
        if ($has_paid_license) {
            echo '<select id="' . esc_attr(self::OPTION_LOG_RETENTION_DAYS) . '" name="' . esc_attr(self::OPTION_LOG_RETENTION_DAYS) . '">';
            foreach (self::get_log_retention_options() as $retention_days) {
                /* translators: %d: number of days to keep access logs. */
                echo '<option value="' . esc_attr((string) $retention_days) . '"' . selected(self::get_log_retention_days(), $retention_days, false) . '>' . esc_html(sprintf(_n('%d day', '%d days', $retention_days, 'techdex-client-post-preview'), $retention_days)) . '</option>';
            }
            echo '</select>';
        } else {
            echo '<input type="text" class="regular-text" value="' . esc_attr__('0 days', 'techdex-client-post-preview') . '" disabled="disabled" style="max-width:120px;background:#f6f7f7;color:#50575e;" />';
        }
        echo '<p class="description">' . esc_html(
            self::is_agency_active()
                ? __('Agency can keep access logs for 7, 14, 30, 60, or 90 days. The default is 30 days.', 'techdex-client-post-preview')
                : ($has_paid_license
                    ? __('Pro can keep access logs for up to 14 days. Choose 7 or 14 days for automatic pruning.', 'techdex-client-post-preview')
                    : __('Free does not include access logs. Upgrade to Pro or Agency to enable log retention and view tracking.', 'techdex-client-post-preview'))
        ) . '</p>';
        echo '</td>';
        echo '</tr>';
        echo '</table>';
        submit_button(
            __('Save Dashboard Settings', 'techdex-client-post-preview'),
            'secondary',
            'submit',
            false,
            $has_paid_license ? [] : ['disabled' => 'disabled', 'aria-disabled' => 'true']
        );
        echo '<span style="margin-left:8px;color:#646970;">' . esc_html($has_paid_license ? __('Unlocked by your active license', 'techdex-client-post-preview') : __('Pro feature', 'techdex-client-post-preview')) . '</span>';
        echo '</form>';
        echo '</div>';
        echo '</div>';
    }

    public static function render_preview_links_page(): void {
        if (!current_user_can('manage_options')) {
            wp_die(esc_html__('You do not have permission to view client preview links.', 'techdex-client-post-preview'));
        }

        $posts = self::get_preview_posts();
        $saved_count = self::get_saved_preview_posts_count();
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only filter for narrowing the preview-links listing.
        $filter_post_id = isset($_GET['post_id']) ? absint(wp_unslash($_GET['post_id'])) : 0;

        if ($filter_post_id > 0) {
            $posts = array_values(array_filter(
                $posts,
                static function ($post) use ($filter_post_id): bool {
                    return (int) $post->ID === $filter_post_id;
                }
            ));
        }

        self::render_admin_page_header(
            'preview-links',
            __('Client Preview Links', 'techdex-client-post-preview'),
            __('Review every generated preview link in one place and revoke access whenever you need to.', 'techdex-client-post-preview')
        );

        echo '<div style="margin:16px 0;padding:16px 18px;border:1px solid #dcdcde;border-left:4px solid #2271b1;background:#fff;">';
        /* translators: 1: number of used preview slots, 2: total preview slot limit. */
        echo '<p style="margin:0 0 8px;"><strong>' . esc_html__('Current preview slot usage', 'techdex-client-post-preview') . ':</strong> ' . esc_html(sprintf('%d of %d posts using preview slots', $saved_count, self::get_preview_slot_limit())) . '</p>';
        echo '<p style="margin:0;">' . esc_html(
            self::is_pro_active()
                /* translators: %d: maximum preview links allowed per post on the current plan. */
                ? sprintf(__('Your active paid license allows up to %d preview links per post on this site.', 'techdex-client-post-preview'), self::get_preview_link_limit())
                : __('Pro raises the preview slot limit to 12 and adds more preview links per post plus advanced sharing controls.', 'techdex-client-post-preview')
        ) . '</p>';
        echo '</div>';

        if ($filter_post_id > 0) {
            echo '<p><a href="' . esc_url(self::get_preview_links_page_url()) . '">' . esc_html__('Show all preview links', 'techdex-client-post-preview') . '</a></p>';
        }

        if (empty($posts)) {
            echo '<p>' . esc_html__('No preview links have been generated yet.', 'techdex-client-post-preview') . '</p>';
            echo '</div>';
            return;
        }

        echo '<style>
        .cpp-link-row td { background:#fff; vertical-align:middle; }
        .cpp-link-record:nth-of-type(even) .cpp-link-row td { background:#f8fbff; }
        .cpp-link-row.cpp-row-open td { background:#fcfdff; }
        .cpp-link-manager-row td { background:#f5f7fb; border-top:1px solid #e2e8f0; box-shadow: inset 0 1px 0 rgba(255,255,255,.7); }
        .cpp-link-record:nth-of-type(even) .cpp-link-manager-row td { background:#eef5fb; }
        .cpp-toggle-cell { width:42px; text-align:center; }
        .cpp-toggle-button { width:30px; height:30px; display:inline-flex; align-items:center; justify-content:center; border:1px solid #c3c4c7; border-radius:8px; background:#fff; color:#2271b1; cursor:pointer; padding:0; }
        .cpp-toggle-glyph { display:block; font-size:22px; font-weight:700; line-height:1; transform:translateY(-2px); }
        .cpp-link-manager-scroll { overflow-x:auto; overflow-y:hidden; padding-bottom:2px; }
        .cpp-link-manager-strip { display:flex; align-items:center; gap:10px; flex-wrap:nowrap; white-space:nowrap; min-width:max-content; }
        </style>';
        echo '<table class="widefat striped" style="min-width:1400px;">';
        echo '<thead><tr>';
        echo '<th class="cpp-toggle-cell"></th>';
        echo '<th>' . esc_html__('Title', 'techdex-client-post-preview') . '</th>';
        echo '<th>' . esc_html__('Post ID', 'techdex-client-post-preview') . '</th>';
        echo '<th>' . esc_html__('Type', 'techdex-client-post-preview') . '</th>';
        echo '<th>' . esc_html__('Link', 'techdex-client-post-preview') . '</th>';
        echo '<th>' . esc_html__('Label', 'techdex-client-post-preview') . '</th>';
        echo '<th>' . esc_html__('Expiration Status', 'techdex-client-post-preview') . '</th>';
        echo '<th>' . esc_html__('Expires', 'techdex-client-post-preview') . '</th>';
        echo '<th>' . esc_html__('Views', 'techdex-client-post-preview') . '</th>';
        echo '<th>' . esc_html__('Preview Link', 'techdex-client-post-preview') . '</th>';
        echo '<th>' . esc_html__('Actions', 'techdex-client-post-preview') . '</th>';
        echo '</tr></thead>';

        foreach ($posts as $post) {
            $edit_link = get_edit_post_link($post->ID);
            $links = self::get_preview_links((int) $post->ID);

            foreach ($links as $link) {
                $manager_row_id = 'cpp-link-manager-' . (int) $post->ID . '-' . sanitize_html_class((string) $link['id']);
                $preview_url = self::build_preview_url((int) $post->ID, (string) $link['id']);
                $status = self::get_expiration_status_data((int) $link['created_at'], (int) $link['expires_at']);
                $expires_label = (int) $link['expires_at'] > 0
                    ? wp_date(get_option('date_format') . ' ' . get_option('time_format'), (int) $link['expires_at'])
                    : __('Never expires', 'techdex-client-post-preview');
                $revoke_link = wp_nonce_url(
                    add_query_arg(
                        [
                            'action' => 'cpp_revoke_link',
                            'post_id' => $post->ID,
                            'link_id' => $link['id'],
                            'redirect_to' => self::get_preview_links_page_url(),
                        ],
                        admin_url('admin-post.php')
                    ),
                    'cpp_revoke_link_' . $post->ID . '_' . $link['id']
                );
                $details_link = self::get_link_details_page_url([
                    'post_id' => $post->ID,
                    'link_id' => $link['id'],
                ]);
                $current_expiration_days = self::get_link_expiration_days_from_record($link);
                $last_viewed_label = !empty($link['last_viewed_at'])
                    ? wp_date(get_option('date_format') . ' ' . get_option('time_format'), (int) $link['last_viewed_at'])
                    : __('Not viewed yet', 'techdex-client-post-preview');

                echo '<tbody class="cpp-link-record">';
                echo '<tr class="cpp-link-row" data-manager-row="' . esc_attr($manager_row_id) . '">';
                echo '<td class="cpp-toggle-cell">';
                if ('publish' !== $post->post_status && (self::is_pro_active() || self::is_agency_active())) {
                    echo '<button type="button" class="cpp-toggle-button" data-target="' . esc_attr($manager_row_id) . '" aria-expanded="false" aria-label="' . esc_attr__('Show link manager', 'techdex-client-post-preview') . '"><span class="cpp-toggle-glyph">+</span></button>';
                }
                echo '</td>';
                echo '<td><strong>' . esc_html(get_the_title($post) ?: __('(no title)', 'techdex-client-post-preview')) . '</strong></td>';
                echo '<td>' . esc_html((string) $post->ID) . '</td>';
                echo '<td>' . esc_html($post->post_type) . '</td>';
                /* translators: %d: preview link position number. */
                echo '<td>' . esc_html(sprintf(__('Link %d', 'techdex-client-post-preview'), (int) $link['position'])) . '</td>';
                echo '<td>' . (!empty($link['label']) ? '<strong>' . esc_html((string) $link['label']) . '</strong>' : '<span style="color:#646970;">' . esc_html__('No label', 'techdex-client-post-preview') . '</span>') . '</td>';
            if ('publish' === $post->post_status) {
                $status = [
                    'label' => __('Published', 'techdex-client-post-preview'),
                    'background' => '#e7f3ff',
                    'border' => '#2271b1',
                    'text' => '#0a4b78',
                ];
            } elseif ([] === $status) {
                $status = [
                    'label' => __('Unavailable', 'techdex-client-post-preview'),
                    'background' => '#f6f7f7',
                    'border' => '#8c8f94',
                    'text' => '#1d2327',
                ];
                }
                echo '<td><span style="display:inline-block;padding:4px 8px;border-left:4px solid ' . esc_attr($status['border']) . ';background:' . esc_attr($status['background']) . ';color:' . esc_attr($status['text']) . ';border-radius:4px;">' . esc_html($status['label']) . '</span></td>';
                echo '<td>' . esc_html($expires_label) . '</td>';
                echo '<td>';
                /* translators: %d: number of recorded views for this preview link. */
                echo '<div><strong>' . esc_html(sprintf(_n('%d view', '%d views', (int) ($link['view_count'] ?? 0), 'techdex-client-post-preview'), (int) ($link['view_count'] ?? 0))) . '</strong></div>';
                /* translators: %s: formatted last-viewed date and time, or a "not viewed yet" label. */
                echo '<div style="color:#646970;">' . esc_html(sprintf(__('Last viewed: %s', 'techdex-client-post-preview'), $last_viewed_label)) . '</div>';
                echo '<div style="margin-top:6px;"><a href="' . esc_url($details_link) . '">' . esc_html__('View Details', 'techdex-client-post-preview') . '</a></div>';
                echo '</td>';
                echo '<td>';

                if ('publish' === $post->post_status) {
                    echo '<span style="color:#646970;">' . esc_html__('This post is published, so the public permalink is live and the saved preview links are inactive until the post returns to an unpublished status.', 'techdex-client-post-preview') . '</span>';
                } elseif ($preview_url) {
                    echo '<div style="display:flex;align-items:center;gap:8px;white-space:nowrap;">';
                    echo '<input type="text" readonly style="flex:1 1 auto;min-width:0;width:190px;" value="' . esc_attr($preview_url) . '" onclick="this.select();" autocomplete="off" />';
                    echo '<a href="' . esc_url($preview_url) . '" target="_blank" rel="noopener noreferrer" aria-label="' . esc_attr__('Open preview in a new tab', 'techdex-client-post-preview') . '" title="' . esc_attr__('Open preview in a new tab', 'techdex-client-post-preview') . '" style="flex:0 0 auto;width:32px;height:32px;display:inline-flex;align-items:center;justify-content:center;border:1px solid #c3c4c7;border-radius:6px;background:#fff;color:#2271b1;text-decoration:none;">';
                    echo '<span class="dashicons dashicons-external" style="font-size:18px;width:18px;height:18px;"></span>';
                    echo '</a>';
                    echo '</div>';
                } else {
                    echo '<span style="color:#646970;">' . esc_html__('Unavailable', 'techdex-client-post-preview') . '</span>';
                }

                echo '</td>';
                echo '<td>';
                echo '<div style="display:flex;gap:6px;flex-wrap:wrap;min-width:180px;">';

                if ($edit_link) {
                    echo '<a class="button button-secondary" href="' . esc_url($edit_link) . '" target="_blank" rel="noopener noreferrer">' . esc_html__('Edit Post', 'techdex-client-post-preview') . '</a>';
                }

                echo '<a class="button" href="' . esc_url($revoke_link) . '">' . esc_html__('Revoke', 'techdex-client-post-preview') . '</a>';
                echo '</div>';
                echo '</td>';
                echo '</tr>';

                if ('publish' !== $post->post_status && (self::is_pro_active() || self::is_agency_active())) {
                    echo '<tr id="' . esc_attr($manager_row_id) . '" class="cpp-link-manager-row" hidden>';
                    echo '<td></td>';
                    echo '<td colspan="10" style="padding:12px 18px 16px 18px;">';
                    echo '<div class="cpp-link-manager-scroll">';
                    echo '<div class="cpp-link-manager-strip">';

                    if (self::is_pro_active()) {
                        echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin:0;display:flex;align-items:center;gap:6px;flex-wrap:nowrap;">';
                        echo '<span style="font-weight:600;color:#50575e;">' . esc_html__('Link Expiration', 'techdex-client-post-preview') . '</span>';
                        wp_nonce_field('cpp_update_link_expiration_' . $post->ID . '_' . $link['id']);
                        echo '<input type="hidden" name="action" value="cpp_update_link_expiration" />';
                        echo '<input type="hidden" name="post_id" value="' . esc_attr((string) $post->ID) . '" />';
                        echo '<input type="hidden" name="link_id" value="' . esc_attr((string) $link['id']) . '" />';
                        echo '<label for="cpp_expiration_days_manage_' . esc_attr((string) $post->ID . '_' . $link['id']) . '" class="screen-reader-text">' . esc_html__('Expiration days', 'techdex-client-post-preview') . '</label>';
                        echo '<input type="number" min="1" step="1" class="small-text" id="cpp_expiration_days_manage_' . esc_attr((string) $post->ID . '_' . $link['id']) . '" name="expiration_days" value="' . esc_attr((string) $current_expiration_days) . '" style="width:80px;" />';
                        echo '<button type="submit" class="button button-secondary">' . esc_html__('Update Expiration', 'techdex-client-post-preview') . '</button>';
                        echo '</form>';
                    }

                    if (self::is_agency_active()) {
                        echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin:0;display:flex;align-items:center;gap:6px;flex-wrap:nowrap;">';
                        echo '<span style="font-weight:600;color:#50575e;">' . esc_html__('Workflow', 'techdex-client-post-preview') . '</span>';
                        wp_nonce_field('cpp_manage_link_workflow_' . $post->ID . '_' . $link['id']);
                        echo '<input type="hidden" name="action" value="cpp_manage_link_workflow" />';
                        echo '<input type="hidden" name="post_id" value="' . esc_attr((string) $post->ID) . '" />';
                        echo '<input type="hidden" name="link_id" value="' . esc_attr((string) $link['id']) . '" />';
                        echo '<label for="cpp_link_workflow_' . esc_attr((string) $post->ID . '_' . $link['id']) . '" class="screen-reader-text">' . esc_html__('Choose a preview-link workflow', 'techdex-client-post-preview') . '</label>';
                        echo '<select id="cpp_link_workflow_' . esc_attr((string) $post->ID . '_' . $link['id']) . '" name="link_workflow" style="min-width:180px;">';
                        echo '<option value="renew">' . esc_html__('Renew', 'techdex-client-post-preview') . '</option>';
                        echo '<option value="regenerate">' . esc_html__('Regenerate', 'techdex-client-post-preview') . '</option>';

                        if ((int) $link['expires_at'] > 0) {
                            echo '<option value="never_expire">' . esc_html__('Never Expire', 'techdex-client-post-preview') . '</option>';
                        }

                        echo '<option value="toggle_auto_revoke">' . esc_html(!empty($link['auto_revoke']) ? __('Switch Auto-Revoke Off', 'techdex-client-post-preview') : __('Switch Auto-Revoke On', 'techdex-client-post-preview')) . '</option>';
                        echo '</select>';
                        echo '<button type="submit" class="button button-secondary">' . esc_html__('Apply', 'techdex-client-post-preview') . '</button>';
                        echo '</form>';

                        echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin:0;display:flex;align-items:center;gap:6px;flex-wrap:nowrap;">';
                        echo '<span style="font-weight:600;color:#50575e;">' . esc_html__('Label', 'techdex-client-post-preview') . '</span>';
                        wp_nonce_field('cpp_update_link_label_' . $post->ID . '_' . $link['id']);
                        echo '<input type="hidden" name="action" value="cpp_update_link_label" />';
                        echo '<input type="hidden" name="post_id" value="' . esc_attr((string) $post->ID) . '" />';
                        echo '<input type="hidden" name="link_id" value="' . esc_attr((string) $link['id']) . '" />';
                        echo '<label for="cpp_link_label_' . esc_attr((string) $post->ID . '_' . $link['id']) . '" class="screen-reader-text">' . esc_html__('Link label', 'techdex-client-post-preview') . '</label>';
                        echo '<input type="text" id="cpp_link_label_' . esc_attr((string) $post->ID . '_' . $link['id']) . '" name="link_label" value="' . esc_attr((string) ($link['label'] ?? '')) . '" placeholder="' . esc_attr__('Label this link', 'techdex-client-post-preview') . '" style="min-width:160px;width:160px;" autocomplete="off" />';
                        echo '<button type="submit" class="button button-secondary">' . esc_html__('Save Label', 'techdex-client-post-preview') . '</button>';
                        echo '</form>';

                        echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin:0;display:flex;align-items:center;gap:6px;flex-wrap:nowrap;">';
                        echo '<span style="font-weight:600;color:#50575e;">' . esc_html__('Passcode', 'techdex-client-post-preview') . '</span>';
                        wp_nonce_field('cpp_update_link_passcode_' . $post->ID . '_' . $link['id']);
                        echo '<input type="hidden" name="action" value="cpp_update_link_passcode" />';
                        echo '<input type="hidden" name="post_id" value="' . esc_attr((string) $post->ID) . '" />';
                        echo '<input type="hidden" name="link_id" value="' . esc_attr((string) $link['id']) . '" />';
                        echo '<label for="cpp_link_passcode_' . esc_attr((string) $post->ID . '_' . $link['id']) . '" class="screen-reader-text">' . esc_html__('Preview passcode', 'techdex-client-post-preview') . '</label>';
                        echo '<input type="password" id="cpp_link_passcode_' . esc_attr((string) $post->ID . '_' . $link['id']) . '" name="link_passcode" placeholder="' . esc_attr(!empty($link['passcode_hash']) ? __('Update passcode', 'techdex-client-post-preview') : __('Set passcode', 'techdex-client-post-preview')) . '" style="min-width:160px;width:160px;" autocomplete="off" />';
                        echo '<button type="submit" class="button button-secondary">' . esc_html(!empty($link['passcode_hash']) ? __('Update Passcode', 'techdex-client-post-preview') : __('Set Passcode', 'techdex-client-post-preview')) . '</button>';
                        echo '</form>';

                        if (!empty($link['passcode_hash'])) {
                            echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin:0;">';
                            wp_nonce_field('cpp_update_link_clear_passcode_' . $post->ID . '_' . $link['id']);
                            echo '<input type="hidden" name="action" value="cpp_clear_link_passcode" />';
                            echo '<input type="hidden" name="post_id" value="' . esc_attr((string) $post->ID) . '" />';
                            echo '<input type="hidden" name="link_id" value="' . esc_attr((string) $link['id']) . '" />';
                            echo '<button type="submit" class="button button-secondary">' . esc_html__('Clear Passcode', 'techdex-client-post-preview') . '</button>';
                            echo '</form>';
                        }
                    }

                    echo '</div>';
                    echo '</div>';
                    echo '</td>';
                    echo '</tr>';
                }

                echo '</tbody>';
            }
        }

        echo '</table>';
        echo '<script>
        (function () {
            var storageKey = "cppLinkManagerState";

            function getState() {
                try {
                    var raw = window.localStorage.getItem(storageKey);
                    return raw ? JSON.parse(raw) : {};
                } catch (error) {
                    return {};
                }
            }

            function saveState(state) {
                try {
                    window.localStorage.setItem(storageKey, JSON.stringify(state));
                } catch (error) {
                }
            }

            function setRowState(button, shouldOpen) {
                var targetId = button.getAttribute("data-target");
                var managerRow = targetId ? document.getElementById(targetId) : null;
                var parentRow = button.closest(".cpp-link-row");

                if (!managerRow) {
                    return;
                }

                if (shouldOpen) {
                    managerRow.removeAttribute("hidden");
                    button.setAttribute("aria-expanded", "true");
                    button.setAttribute("aria-label", "Hide link manager");
                    button.innerHTML = \'<span class="cpp-toggle-glyph">-</span>\';
                    if (parentRow) {
                        parentRow.classList.add("cpp-row-open");
                    }
                } else {
                    managerRow.setAttribute("hidden", "hidden");
                    button.setAttribute("aria-expanded", "false");
                    button.setAttribute("aria-label", "Show link manager");
                    button.innerHTML = \'<span class="cpp-toggle-glyph">+</span>\';
                    if (parentRow) {
                        parentRow.classList.remove("cpp-row-open");
                    }
                }
            }

            document.querySelectorAll(".cpp-toggle-button").forEach(function (button) {
                var state = getState();
                var targetId = button.getAttribute("data-target");

                if (targetId && state[targetId]) {
                    setRowState(button, true);
                }
            });

            document.addEventListener("click", function (event) {
                var button = event.target.closest(".cpp-toggle-button");
                if (!button) {
                    return;
                }

                var state = getState();
                var targetId = button.getAttribute("data-target");
                var shouldOpen = button.getAttribute("aria-expanded") !== "true";

                setRowState(button, shouldOpen);

                if (targetId) {
                    state[targetId] = shouldOpen;
                    saveState(state);
                }
            });
        })();
        </script>';
        echo '</div>';
    }

    public static function render_getting_started_page(): void {
        if (!current_user_can('manage_options')) {
            wp_die(esc_html__('You do not have permission to view this page.', 'techdex-client-post-preview'));
        }

        self::render_admin_page_header(
            'getting-started',
            __('Getting Started', 'techdex-client-post-preview'),
            __('Learn how to create, share, and manage preview links inside WordPress.', 'techdex-client-post-preview')
        );

        echo '<div style="background:#fff;border:1px solid #dcdcde;padding:20px;border-radius:8px;margin-bottom:20px;">';
        echo '<h2 style="margin-top:0;">' . esc_html__('How to Use TechDex Client Post Preview', 'techdex-client-post-preview') . '</h2>';
        echo '<ol style="margin:0 0 0 18px;">';
        echo '<li>' . esc_html__('Create or open a post, page, or supported custom post type and save it at least once.', 'techdex-client-post-preview') . '</li>';
        echo '<li>' . esc_html__('In the editor sidebar, use the Client Preview Link box to generate a preview link.', 'techdex-client-post-preview') . '</li>';
        echo '<li>' . esc_html__('Send that preview link to your client. It shows the latest saved version of the content.', 'techdex-client-post-preview') . '</li>';
        echo '<li>' . esc_html__('Use the status card in the editor to check whether the link is active, expiring soon, on its last day, or expired.', 'techdex-client-post-preview') . '</li>';
        echo '<li>' . esc_html__('Use the Preview Links tab to review all active links, filter by post, revoke access, and manage expirations from the back end.', 'techdex-client-post-preview') . '</li>';
        echo '</ol>';
        echo '</div>';

        echo '<div style="background:#fff;border:1px solid #dcdcde;padding:20px;border-radius:8px;margin-bottom:20px;">';
        echo '<h2 style="margin-top:0;">' . esc_html__('Free Version Limits', 'techdex-client-post-preview') . '</h2>';
        echo '<ul style="margin:0 0 0 18px;">';
        echo '<li>' . esc_html__('Up to 3 active preview-enabled posts at one time.', 'techdex-client-post-preview') . '</li>';
        echo '<li>' . esc_html__('One active preview link per post.', 'techdex-client-post-preview') . '</li>';
        echo '<li>' . esc_html__('Default expiration is fixed at 7 days in the free version.', 'techdex-client-post-preview') . '</li>';
        echo '</ul>';
        echo '</div>';

        echo '<div style="background:#fff;border:1px solid #dcdcde;padding:20px;border-radius:8px;">';
        echo '<h2 style="margin-top:0;">' . esc_html__('Management Tips', 'techdex-client-post-preview') . '</h2>';
        echo '<p>' . esc_html__('If you prefer managing links from the back end, the Preview Links tab shows the same expiration status signals as the editor and gives you quick revoke and edit actions.', 'techdex-client-post-preview') . '</p>';
        echo '</div>';
        echo '</div>';
    }

    public static function render_details_summary_page(): void {
        if (!current_user_can('manage_options')) {
            wp_die(esc_html__('You do not have permission to view this page.', 'techdex-client-post-preview'));
        }

        if (!self::is_agency_active()) {
            wp_die(esc_html__('The Details summary is available on the Agency plan.', 'techdex-client-post-preview'));
        }

        $posts = self::get_preview_posts();
        $records = [];
        $total_views = 0;
        $protected_links = 0;
        $labeled_links = 0;
        $never_expire_links = 0;

        foreach ($posts as $post) {
            foreach (self::get_preview_links((int) $post->ID) as $link) {
                $total_views += (int) ($link['view_count'] ?? 0);

                if (!empty($link['passcode_hash'])) {
                    $protected_links++;
                }

                if (!empty($link['label'])) {
                    $labeled_links++;
                }

                if ((int) ($link['expires_at'] ?? 0) <= 0) {
                    $never_expire_links++;
                }

                $records[] = [
                    'post' => $post,
                    'link' => $link,
                ];
            }
        }

        self::render_admin_page_header(
            'details',
            __('Details', 'techdex-client-post-preview'),
            __('Review totals, recent activity, and per-link summaries across every preview link on this site.', 'techdex-client-post-preview')
        );

        echo '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px;margin-bottom:20px;">';
        echo '<div style="background:#fff;border:1px solid #dcdcde;padding:18px;border-radius:8px;"><h2 style="margin:0 0 8px;font-size:15px;">' . esc_html__('Total Preview Links', 'techdex-client-post-preview') . '</h2><p style="margin:0;font-size:30px;line-height:1.1;">' . esc_html((string) count($records)) . '</p></div>';
        echo '<div style="background:#fff;border:1px solid #dcdcde;padding:18px;border-radius:8px;"><h2 style="margin:0 0 8px;font-size:15px;">' . esc_html__('Total Views', 'techdex-client-post-preview') . '</h2><p style="margin:0;font-size:30px;line-height:1.1;">' . esc_html((string) $total_views) . '</p></div>';
        echo '<div style="background:#fff;border:1px solid #dcdcde;padding:18px;border-radius:8px;"><h2 style="margin:0 0 8px;font-size:15px;">' . esc_html__('Protected Links', 'techdex-client-post-preview') . '</h2><p style="margin:0;font-size:30px;line-height:1.1;">' . esc_html((string) $protected_links) . '</p></div>';
        echo '<div style="background:#fff;border:1px solid #dcdcde;padding:18px;border-radius:8px;"><h2 style="margin:0 0 8px;font-size:15px;">' . esc_html__('Labeled Links', 'techdex-client-post-preview') . '</h2><p style="margin:0;font-size:30px;line-height:1.1;">' . esc_html((string) $labeled_links) . '</p></div>';
        echo '<div style="background:#fff;border:1px solid #dcdcde;padding:18px;border-radius:8px;"><h2 style="margin:0 0 8px;font-size:15px;">' . esc_html__('Never Expire', 'techdex-client-post-preview') . '</h2><p style="margin:0;font-size:30px;line-height:1.1;">' . esc_html((string) $never_expire_links) . '</p></div>';
        echo '</div>';

        if (empty($records)) {
            echo '<div style="background:#fff;border:1px solid #dcdcde;padding:20px;border-radius:8px;">';
            echo '<p style="margin:0;">' . esc_html__('No preview links have been generated yet.', 'techdex-client-post-preview') . '</p>';
            echo '</div>';
            echo '</div>';

            return;
        }

        echo '<table class="widefat striped" style="min-width:1200px;">';
        echo '<thead><tr>';
        echo '<th>' . esc_html__('Title', 'techdex-client-post-preview') . '</th>';
        echo '<th>' . esc_html__('Link', 'techdex-client-post-preview') . '</th>';
        echo '<th>' . esc_html__('Label', 'techdex-client-post-preview') . '</th>';
        echo '<th>' . esc_html__('Status', 'techdex-client-post-preview') . '</th>';
        echo '<th>' . esc_html__('Expires', 'techdex-client-post-preview') . '</th>';
        echo '<th>' . esc_html__('Views', 'techdex-client-post-preview') . '</th>';
        echo '<th>' . esc_html__('Last Viewed', 'techdex-client-post-preview') . '</th>';
        echo '<th>' . esc_html__('Protection', 'techdex-client-post-preview') . '</th>';
        echo '<th>' . esc_html__('Actions', 'techdex-client-post-preview') . '</th>';
        echo '</tr></thead><tbody>';

        foreach ($records as $record) {
            $post = $record['post'];
            $link = $record['link'];
            $status = self::get_expiration_status_data((int) ($link['created_at'] ?? 0), (int) ($link['expires_at'] ?? 0));
            $expires_label = (int) ($link['expires_at'] ?? 0) > 0
                ? wp_date(get_option('date_format') . ' ' . get_option('time_format'), (int) $link['expires_at'])
                : __('Never expires', 'techdex-client-post-preview');
            $last_viewed_label = !empty($link['last_viewed_at'])
                ? wp_date(get_option('date_format') . ' ' . get_option('time_format'), (int) $link['last_viewed_at'])
                : __('Not viewed yet', 'techdex-client-post-preview');
            $preview_url = self::build_preview_url((int) $post->ID, (string) $link['id']);
            $details_url = self::get_link_details_page_url([
                'post_id' => (int) $post->ID,
                'link_id' => (string) $link['id'],
            ]);

            echo '<tr>';
            /* translators: 1: post ID, 2: post type slug. */
            echo '<td><strong>' . esc_html(get_the_title($post) ?: __('(no title)', 'techdex-client-post-preview')) . '</strong><div style="margin-top:4px;color:#646970;">' . esc_html(sprintf(__('Post ID %1$d • %2$s', 'techdex-client-post-preview'), (int) $post->ID, $post->post_type)) . '</div></td>';
            /* translators: %d: preview link position number. */
            echo '<td>' . esc_html(sprintf(__('Link %d', 'techdex-client-post-preview'), (int) ($link['position'] ?? 1))) . '</td>';
            echo '<td>' . (!empty($link['label']) ? esc_html((string) $link['label']) : '<span style="color:#646970;">' . esc_html__('No label', 'techdex-client-post-preview') . '</span>') . '</td>';
            echo '<td>' . esc_html($status['label'] ?? __('Unavailable', 'techdex-client-post-preview')) . '</td>';
            echo '<td>' . esc_html($expires_label) . '</td>';
            /* translators: %d: number of recorded views for this preview link. */
            echo '<td>' . esc_html(sprintf(_n('%d view', '%d views', (int) ($link['view_count'] ?? 0), 'techdex-client-post-preview'), (int) ($link['view_count'] ?? 0))) . '</td>';
            echo '<td>' . esc_html($last_viewed_label) . '</td>';
            echo '<td>' . esc_html(!empty($link['passcode_hash']) ? __('Passcode', 'techdex-client-post-preview') : __('Open', 'techdex-client-post-preview')) . '</td>';
            echo '<td><a class="button button-secondary" href="' . esc_url($details_url) . '">' . esc_html__('View Details', 'techdex-client-post-preview') . '</a> <a class="button button-secondary" href="' . esc_url($preview_url) . '" target="_blank" rel="noopener noreferrer">' . esc_html__('Open Preview', 'techdex-client-post-preview') . '</a></td>';
            echo '</tr>';
        }

        echo '</tbody></table>';
        echo '</div>';
    }

    public static function render_link_details_page(): void {
        if (!current_user_can('manage_options')) {
            wp_die(esc_html__('You do not have permission to view this page.', 'techdex-client-post-preview'));
        }

        $post_id = self::get_admin_post_id();
        $link_id = self::get_admin_link_id();
        $post = $post_id > 0 ? get_post($post_id) : null;
        $link = ($post && '' !== $link_id) ? self::get_preview_link($post_id, $link_id) : null;

        if (!$post || !$link) {
            wp_die(esc_html__('That preview link could not be found.', 'techdex-client-post-preview'));
        }

        $preview_url = self::build_preview_url($post_id, $link_id);
        $status = self::get_expiration_status_data((int) ($link['created_at'] ?? 0), (int) ($link['expires_at'] ?? 0));
        $expires_label = (int) ($link['expires_at'] ?? 0) > 0
            ? wp_date(get_option('date_format') . ' ' . get_option('time_format'), (int) $link['expires_at'])
            : __('Never expires', 'techdex-client-post-preview');
        $is_agency = self::is_agency_active();

        self::render_admin_page_header(
            'details',
            __('Link Details', 'techdex-client-post-preview'),
            __('Review the full details, tracking history, and protection settings for this preview link.', 'techdex-client-post-preview')
        );

        echo '<div style="background:#fff;border:1px solid #dcdcde;padding:20px;border-radius:8px;margin-bottom:20px;">';
        echo '<h2 style="margin-top:0;">' . esc_html(get_the_title($post) ?: __('(no title)', 'techdex-client-post-preview')) . '</h2>';
        /* translators: 1: post ID, 2: post type slug, 3: preview link position number. */
        echo '<p style="margin:0 0 14px;color:#646970;">' . esc_html(sprintf(__('Post ID %1$d • %2$s • Link %3$d', 'techdex-client-post-preview'), $post_id, $post->post_type, (int) ($link['position'] ?? 1))) . '</p>';
        echo '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px;">';
        echo '<div style="background:#f8f9fa;border:1px solid #e2e8f0;padding:14px;border-radius:8px;"><strong>' . esc_html__('Status', 'techdex-client-post-preview') . '</strong><div style="margin-top:6px;">' . esc_html($status['label'] ?? __('Unavailable', 'techdex-client-post-preview')) . '</div></div>';
        echo '<div style="background:#f8f9fa;border:1px solid #e2e8f0;padding:14px;border-radius:8px;"><strong>' . esc_html__('Expires', 'techdex-client-post-preview') . '</strong><div style="margin-top:6px;">' . esc_html($expires_label) . '</div></div>';
        /* translators: %d: number of recorded views for this preview link. */
        echo '<div style="background:#f8f9fa;border:1px solid #e2e8f0;padding:14px;border-radius:8px;"><strong>' . esc_html__('Views', 'techdex-client-post-preview') . '</strong><div style="margin-top:6px;">' . esc_html(sprintf(_n('%d view', '%d views', (int) ($link['view_count'] ?? 0), 'techdex-client-post-preview'), (int) ($link['view_count'] ?? 0))) . '</div></div>';
        if ($is_agency) {
            echo '<div style="background:#f8f9fa;border:1px solid #e2e8f0;padding:14px;border-radius:8px;"><strong>' . esc_html__('Label', 'techdex-client-post-preview') . '</strong><div style="margin-top:6px;">' . (!empty($link['label']) ? esc_html((string) $link['label']) : '<span style="color:#646970;">' . esc_html__('No label', 'techdex-client-post-preview') . '</span>') . '</div></div>';
            echo '<div style="background:#f8f9fa;border:1px solid #e2e8f0;padding:14px;border-radius:8px;"><strong>' . esc_html__('Passcode Protection', 'techdex-client-post-preview') . '</strong><div style="margin-top:6px;">' . esc_html(!empty($link['passcode_hash']) ? __('Enabled', 'techdex-client-post-preview') : __('Off', 'techdex-client-post-preview')) . '</div></div>';
            echo '<div style="background:#f8f9fa;border:1px solid #e2e8f0;padding:14px;border-radius:8px;"><strong>' . esc_html__('Auto-Revoke', 'techdex-client-post-preview') . '</strong><div style="margin-top:6px;">' . esc_html(!empty($link['auto_revoke']) ? __('On', 'techdex-client-post-preview') : __('Off', 'techdex-client-post-preview')) . '</div></div>';
        }
        echo '</div>';
        echo '<div style="margin-top:16px;display:flex;align-items:center;gap:8px;flex-wrap:wrap;">';
        echo '<input type="text" readonly value="' . esc_attr($preview_url) . '" onclick="this.select();" style="width:420px;max-width:100%;" autocomplete="off" />';
        echo '<a class="button button-secondary" href="' . esc_url($preview_url) . '" target="_blank" rel="noopener noreferrer">' . esc_html__('Open Preview', 'techdex-client-post-preview') . '</a>';
        echo '<a class="button button-secondary" href="' . esc_url(self::get_preview_links_page_url(['post_id' => $post_id])) . '">' . esc_html__('Back to Preview Links', 'techdex-client-post-preview') . '</a>';
        echo '</div>';
        echo '</div>';

        if ($is_agency) {
            echo '<div style="background:#fff;border:1px solid #dcdcde;padding:20px;border-radius:8px;">';
            echo '<h2 style="margin-top:0;">' . esc_html__('Access Log', 'techdex-client-post-preview') . '</h2>';
            echo '<p style="margin:0 0 16px;">';
            echo '<a class="button button-secondary" href="' . esc_url(wp_nonce_url(
                add_query_arg(
                    [
                        'action' => 'cpp_clear_link_log',
                        'post_id' => $post_id,
                        'link_id' => $link_id,
                    ],
                    admin_url('admin-post.php')
                ),
                'cpp_update_link_clear_log_' . $post_id . '_' . $link_id
            )) . '">' . esc_html__('Clear Log', 'techdex-client-post-preview') . '</a>';
            echo '</p>';

            if (empty($link['access_log']) || !is_array($link['access_log'])) {
                echo '<p>' . esc_html__('No access log entries have been recorded for this preview link yet.', 'techdex-client-post-preview') . '</p>';
            } else {
                echo '<table class="widefat striped">';
                echo '<thead><tr>';
                echo '<th>' . esc_html__('Viewed At', 'techdex-client-post-preview') . '</th>';
                echo '<th>' . esc_html__('IP', 'techdex-client-post-preview') . '</th>';
                echo '<th>' . esc_html__('Device', 'techdex-client-post-preview') . '</th>';
                echo '</tr></thead><tbody>';

                foreach ($link['access_log'] as $entry) {
                    if (!is_array($entry)) {
                        continue;
                    }

                    echo '<tr>';
                    echo '<td>' . esc_html(!empty($entry['timestamp']) ? wp_date(get_option('date_format') . ' ' . get_option('time_format'), (int) $entry['timestamp']) : __('Unknown', 'techdex-client-post-preview')) . '</td>';
                    echo '<td>' . esc_html(isset($entry['ip']) ? (string) $entry['ip'] : __('Unknown', 'techdex-client-post-preview')) . '</td>';
                    echo '<td>' . esc_html(isset($entry['user_agent']) ? (string) $entry['user_agent'] : __('Unknown device', 'techdex-client-post-preview')) . '</td>';
                    echo '</tr>';
                }

                echo '</tbody></table>';
            }

            echo '</div>';
        }
        echo '</div>';
    }

    public static function render_readme_page(): void {
        if (!current_user_can('manage_options')) {
            wp_die(esc_html__('You do not have permission to view this page.', 'techdex-client-post-preview'));
        }

        $readme_path = plugin_dir_path(__FILE__) . 'README.md';
        $readme = file_exists($readme_path) ? file_get_contents($readme_path) : '';

        self::render_admin_page_header(
            'readme',
            __('Readme', 'techdex-client-post-preview'),
            __('Reference documentation for TechDex Client Post Preview.', 'techdex-client-post-preview')
        );

        if ('' === $readme) {
            echo '<p>' . esc_html__('The readme is currently unavailable.', 'techdex-client-post-preview') . '</p>';
            echo '</div>';
            return;
        }

        echo '<div style="background:#fff;border:1px solid #dcdcde;padding:20px;border-radius:8px;">';
        echo self::render_readme_markdown($readme); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
        echo '</div>';
        echo '</div>';
    }

    public static function render_license_page(): void {
        if (!current_user_can('manage_options')) {
            wp_die(esc_html__('You do not have permission to view this page.', 'techdex-client-post-preview'));
        }

        $license_key = self::get_saved_license_key();
        $has_license_key = '' !== $license_key;
        $license_state = $has_license_key
            ? self::get_live_license_state_for_screen($license_key)
            : self::get_license_state();
        $plan_label = self::get_current_plan_label();
        $activation_label = !empty($license_state['is_valid'])
            ? __('Activated', 'techdex-client-post-preview')
            : __('Inactive', 'techdex-client-post-preview');
        $activation_note = !empty($license_state['message'])
            ? (string) $license_state['message']
            : __('No active paid license is cached for this site yet.', 'techdex-client-post-preview');
        $expires_text = !empty($license_state['expires_at'])
            ? sprintf(
                /* translators: %s: formatted license expiration date and time. */
                __('Expires: %s', 'techdex-client-post-preview'),
                self::format_license_datetime((string) $license_state['expires_at'])
            )
            : __('Expiration will appear here after activation.', 'techdex-client-post-preview');
        $activation_count_text = $license_state['activation_limit'] > 0
            ? sprintf(
                /* translators: 1: number of active sites using this license, 2: activation limit for the license. */
                __('Sites used: %1$d of %2$d', 'techdex-client-post-preview'),
                (int) $license_state['active_sites'],
                (int) $license_state['activation_limit']
            )
            : __('Site counts will appear here after activation.', 'techdex-client-post-preview');
        $activation_limit_reached = 'activation_limit_reached' === (string) $license_state['service_code']
            || (
                $license_state['activation_limit'] > 0
                && (int) $license_state['active_sites'] >= (int) $license_state['activation_limit']
                && empty($license_state['is_valid'])
            );
        $site_is_activated = !empty($license_state['is_valid']) && 'active' === (string) $license_state['activation_status'];

        self::render_admin_page_header(
            'license',
            __('License', 'techdex-client-post-preview'),
            __('Activate this site with your TechDex license so the plugin can move from Free into Pro or Agency.', 'techdex-client-post-preview')
        );

        echo '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:16px;margin:20px 0;">';
        echo '<div style="background:#fff;border:1px solid #dcdcde;padding:18px;border-radius:8px;"><h2 style="margin-top:0;">' . esc_html__('Current Plan', 'techdex-client-post-preview') . '</h2><p style="font-size:28px;margin:0;">' . esc_html($plan_label) . '</p></div>';
        echo '<div style="background:#fff;border:1px solid #dcdcde;padding:18px;border-radius:8px;"><h2 style="margin-top:0;">' . esc_html__('License Key Status', 'techdex-client-post-preview') . '</h2><p style="font-size:28px;margin:0;">' . esc_html($has_license_key ? __('Saved', 'techdex-client-post-preview') : __('Not Entered', 'techdex-client-post-preview')) . '</p></div>';
        echo '<div style="background:#fff;border:1px solid #dcdcde;padding:18px;border-radius:8px;"><h2 style="margin-top:0;">' . esc_html__('Site Activation', 'techdex-client-post-preview') . '</h2><p style="font-size:28px;margin:0;">' . esc_html($activation_label) . '</p><p style="margin:8px 0 0;color:#646970;">' . esc_html($expires_text) . '</p><p style="margin:6px 0 0;color:#646970;">' . esc_html($activation_count_text) . '</p></div>';
        echo '</div>';

        echo '<div style="background:#fff;border:1px solid #dcdcde;padding:20px;border-radius:8px;margin-bottom:20px;">';
        echo '<h2 style="margin-top:0;">' . esc_html__('License Activation', 'techdex-client-post-preview') . '</h2>';
        echo '<p>' . esc_html__('Set up licensing in two quick steps: save your license key first, then activate this WordPress site against the TechDex licensing service.', 'techdex-client-post-preview') . '</p>';
        echo '<h3 style="margin:18px 0 12px;">' . esc_html__('Step 1: Save Your License Key', 'techdex-client-post-preview') . '</h3>';
        echo '<form method="post" action="options.php">';
        settings_fields('cpp_license_settings');
        echo '<table class="form-table" role="presentation">';
        echo '<tr>';
        echo '<th scope="row"><label for="' . esc_attr(self::OPTION_LICENSE_KEY) . '">' . esc_html__('License Key', 'techdex-client-post-preview') . '</label></th>';
        echo '<td>';
        echo '<input type="text" class="regular-text" id="' . esc_attr(self::OPTION_LICENSE_KEY) . '" name="' . esc_attr(self::OPTION_LICENSE_KEY) . '" value="' . esc_attr($license_key) . '" placeholder="' . esc_attr__('CPP-PRO-XXXX-XXXX-XXXX', 'techdex-client-post-preview') . '" />';
        echo '<p class="description">' . esc_html__('Changing the saved key clears the local activation cache so this site can be revalidated cleanly.', 'techdex-client-post-preview') . '</p>';
        echo '</td>';
        echo '</tr>';
        echo '</table>';
        submit_button(__('Save License Key', 'techdex-client-post-preview'));
        echo '</form>';

        echo '<hr style="margin:24px 0;" />';
        echo '<h3 style="margin-top:0;">' . esc_html__('Step 2: Activate This Site', 'techdex-client-post-preview') . '</h3>';
        echo '<p>' . esc_html($activation_note) . '</p>';
        echo '<p style="margin-top:-4px;color:#646970;">' . esc_html($activation_count_text) . '</p>';
        echo '<div style="display:flex;flex-wrap:wrap;gap:10px;align-items:center;">';

        echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin:0;">';
        wp_nonce_field('cpp_activate_license');
        echo '<input type="hidden" name="action" value="cpp_activate_license" />';
        submit_button(
            __('Activate License', 'techdex-client-post-preview'),
            'primary',
            'submit',
            false,
            ($has_license_key && !$activation_limit_reached && !$site_is_activated) ? [] : ['disabled' => 'disabled']
        );
        echo '</form>';

        echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin:0;">';
        wp_nonce_field('cpp_refresh_license');
        echo '<input type="hidden" name="action" value="cpp_refresh_license" />';
        submit_button(__('Check Status', 'techdex-client-post-preview'), 'secondary', 'submit', false, $has_license_key ? [] : ['disabled' => 'disabled']);
        echo '</form>';

        echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin:0;">';
        wp_nonce_field('cpp_deactivate_license');
        echo '<input type="hidden" name="action" value="cpp_deactivate_license" />';
        submit_button(__('Deactivate on This Site', 'techdex-client-post-preview'), 'secondary', 'submit', false, $has_license_key ? [] : ['disabled' => 'disabled']);
        echo '</form>';
        echo '</div>';

        if ($activation_limit_reached) {
            echo '<div style="margin-top:16px;padding:14px 16px;border:1px solid #dcdcde;border-left:4px solid #2271b1;background:#f6f7f7;">';
            echo '<p style="margin:0 0 10px;"><strong>' . esc_html__('Need more site activations?', 'techdex-client-post-preview') . '</strong> ' . esc_html__('You can purchase another Pro license for more installs or move up to Agency for a larger activation pool.', 'techdex-client-post-preview') . '</p>';
            echo '<div style="display:flex;flex-wrap:wrap;gap:10px;">';
            echo '<a class="button button-secondary" href="' . esc_url(self::PRO_PURCHASE_URL) . '" target="_blank" rel="noopener noreferrer">' . esc_html__('Buy Another Pro License', 'techdex-client-post-preview') . '</a>';
            echo '<a class="button button-primary" href="' . esc_url(self::AGENCY_PURCHASE_URL) . '" target="_blank" rel="noopener noreferrer">' . esc_html__('Upgrade to Agency', 'techdex-client-post-preview') . '</a>';
            echo '</div>';
            echo '</div>';
        }

        if (!empty($license_state['last_checked_at'])) {
            echo '<p style="margin-top:14px;color:#646970;">' . esc_html(
                sprintf(
                    /* translators: %s: formatted date and time when the license was last checked. */
                    __('Last checked: %s', 'techdex-client-post-preview'),
                    self::format_license_datetime((string) $license_state['last_checked_at'])
                )
            ) . '</p>';
        }
        echo '</div>';

        echo '<div style="background:#fff;border:1px solid #dcdcde;padding:20px;border-radius:8px;">';
        echo '<h2 style="margin-top:0;">' . esc_html__('License Details', 'techdex-client-post-preview') . '</h2>';
        echo '<ul style="margin:0 0 0 18px;">';
        echo '<li>' . esc_html(
            !empty($license_state['registered_name'])
                /* translators: %s: registered license holder name. */
                ? sprintf(__('Registered to: %s', 'techdex-client-post-preview'), (string) $license_state['registered_name'])
                : __('Registered name will appear here after activation.', 'techdex-client-post-preview')
        ) . '</li>';
        echo '<li>' . esc_html(
            !empty($license_state['license_email'])
                /* translators: %s: email address associated with the license. */
                ? sprintf(__('License email: %s', 'techdex-client-post-preview'), (string) $license_state['license_email'])
                : __('License email will appear here after activation.', 'techdex-client-post-preview')
        ) . '</li>';
        echo '<li>' . esc_html(
            $license_state['activation_limit'] > 0
                ? sprintf(
                    /* translators: 1: number of active sites using this license, 2: activation limit for the license. */
                    __('Active sites currently recorded: %1$d of %2$d', 'techdex-client-post-preview'),
                    (int) $license_state['active_sites'],
                    (int) $license_state['activation_limit']
                )
                : (!empty($license_state['active_sites'])
                    /* translators: %d: number of active sites currently recorded for this license. */
                    ? sprintf(__('Active sites currently recorded: %d', 'techdex-client-post-preview'), (int) $license_state['active_sites'])
                    : __('Active site counts will appear here after activation.', 'techdex-client-post-preview'))
        ) . '</li>';
        echo '</ul>';
        echo '</div>';
        echo '</div>';
    }

    private static function get_live_license_state_for_screen(string $license_key): array {
        $result = self::request_license_service('check', $license_key);

        if (!is_wp_error($result)) {
            self::store_license_service_response('check', $result);
        }

        return self::get_license_state();
    }

    public static function render_changelog_page(): void {
        if (!current_user_can('manage_options')) {
            wp_die(esc_html__('You do not have permission to view this page.', 'techdex-client-post-preview'));
        }

        self::render_admin_page_header(
            'changelog',
            __('Changelog', 'techdex-client-post-preview'),
            __('Track notable changes and release progress for TechDex Client Post Preview.', 'techdex-client-post-preview')
        );

        foreach (self::get_changelog_summary_entries() as $entry) {
            echo '<div style="background:#fff;border:1px solid #dcdcde;padding:20px;border-radius:8px;margin-bottom:16px;">';
            echo '<div style="display:flex;justify-content:space-between;gap:16px;align-items:flex-start;flex-wrap:wrap;margin-bottom:10px;">';
            echo '<div>';
            echo '<h2 style="margin:0 0 4px;">' . esc_html($entry['version']) . '</h2>';
            echo '<div style="color:#646970;">' . esc_html($entry['date']) . '</div>';
            echo '</div>';
            if (!empty($entry['tag'])) {
                echo '<span style="display:inline-block;padding:4px 10px;border-radius:999px;background:#e7f3ff;color:#0a4b78;font-size:12px;font-weight:600;">' . esc_html($entry['tag']) . '</span>';
            }
            echo '</div>';
            echo '<ul style="margin:0 0 0 18px;list-style:disc outside;">';
            foreach ($entry['items'] as $item) {
                echo '<li>' . esc_html($item) . '</li>';
            }
            echo '</ul>';
            echo '</div>';
        }
        echo '<p style="color:#646970;">' . esc_html__('The dashboard shows a concise release summary. The repository can keep a more detailed development history separately.', 'techdex-client-post-preview') . '</p>';
        echo '</div>';
    }

    public static function render_support_page(): void {
        if (!current_user_can('manage_options')) {
            wp_die(esc_html__('You do not have permission to view this page.', 'techdex-client-post-preview'));
        }

        $current_user = wp_get_current_user();

        self::render_admin_page_header(
            'support',
            __('Support', 'techdex-client-post-preview'),
            __('Send a support request directly from the plugin dashboard.', 'techdex-client-post-preview')
        );

        echo '<div style="background:#fff;border:1px solid #dcdcde;padding:20px;border-radius:8px;">';
        echo '<p>' . esc_html__('For support requests, feedback or suggestions, please fill out the form below to message the plugin author.', 'techdex-client-post-preview') . '</p>';
        echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
        echo '<input type="hidden" name="action" value="cpp_send_support" />';
        wp_nonce_field('cpp_send_support');
        echo '<table class="form-table" role="presentation">';
        echo '<tr><th scope="row"><label for="cpp_support_request_type">' . esc_html__('Request Type', 'techdex-client-post-preview') . '</label></th><td><select id="cpp_support_request_type" name="cpp_support_request_type"><option value="support">' . esc_html__('Support Request', 'techdex-client-post-preview') . '</option><option value="bug">' . esc_html__('Bug Report', 'techdex-client-post-preview') . '</option><option value="feedback">' . esc_html__('Feedback', 'techdex-client-post-preview') . '</option><option value="suggestion">' . esc_html__('Suggestion', 'techdex-client-post-preview') . '</option></select></td></tr>';
        echo '<tr><th scope="row"><label for="cpp_support_priority">' . esc_html__('Priority', 'techdex-client-post-preview') . '</label></th><td><select id="cpp_support_priority" name="cpp_support_priority"><option value="normal">' . esc_html__('Normal', 'techdex-client-post-preview') . '</option><option value="low">' . esc_html__('Low', 'techdex-client-post-preview') . '</option><option value="high">' . esc_html__('High', 'techdex-client-post-preview') . '</option></select></td></tr>';
        echo '<tr><th scope="row"><label for="cpp_support_name">' . esc_html__('Your Name', 'techdex-client-post-preview') . '</label></th><td><input type="text" class="regular-text" id="cpp_support_name" name="cpp_support_name" value="' . esc_attr($current_user->display_name) . '" /></td></tr>';
        echo '<tr><th scope="row"><label for="cpp_support_email">' . esc_html__('Your Email', 'techdex-client-post-preview') . '</label></th><td><input type="email" class="regular-text" id="cpp_support_email" name="cpp_support_email" value="' . esc_attr($current_user->user_email) . '" /></td></tr>';
        echo '<tr><th scope="row"><label for="cpp_support_page_url">' . esc_html__('Affected Page URL', 'techdex-client-post-preview') . '</label></th><td><input type="url" class="regular-text" id="cpp_support_page_url" name="cpp_support_page_url" placeholder="' . esc_attr__('Optional', 'techdex-client-post-preview') . '" /></td></tr>';
        echo '<tr><th scope="row"><label for="cpp_support_steps">' . esc_html__('Steps / Context', 'techdex-client-post-preview') . '</label></th><td><textarea id="cpp_support_steps" name="cpp_support_steps" class="large-text" rows="4" placeholder="' . esc_attr__('Optional details to help reproduce or understand the issue', 'techdex-client-post-preview') . '"></textarea></td></tr>';
        echo '<tr><th scope="row"><label for="cpp_support_message">' . esc_html__('Message', 'techdex-client-post-preview') . '</label></th><td><textarea id="cpp_support_message" name="cpp_support_message" class="large-text" rows="8" required></textarea></td></tr>';
        echo '</table>';
        submit_button(__('Send Support Request', 'techdex-client-post-preview'));
        echo '</form>';
        echo '</div>';
        echo '</div>';
    }

    public static function handle_support_request(): void {
        if (!current_user_can('manage_options')) {
            wp_die(esc_html__('You do not have permission to send support requests.', 'techdex-client-post-preview'));
        }

        check_admin_referer('cpp_send_support');

        $request_type = isset($_POST['cpp_support_request_type']) ? sanitize_key(wp_unslash($_POST['cpp_support_request_type'])) : 'support';
        $priority = isset($_POST['cpp_support_priority']) ? sanitize_key(wp_unslash($_POST['cpp_support_priority'])) : 'normal';
        $name = isset($_POST['cpp_support_name']) ? sanitize_text_field(wp_unslash($_POST['cpp_support_name'])) : '';
        $email = isset($_POST['cpp_support_email']) ? sanitize_email(wp_unslash($_POST['cpp_support_email'])) : '';
        $page_url = isset($_POST['cpp_support_page_url']) ? esc_url_raw(wp_unslash($_POST['cpp_support_page_url'])) : '';
        $steps = isset($_POST['cpp_support_steps']) ? sanitize_textarea_field(wp_unslash($_POST['cpp_support_steps'])) : '';
        $message = isset($_POST['cpp_support_message']) ? sanitize_textarea_field(wp_unslash($_POST['cpp_support_message'])) : '';

        $site_name = wp_specialchars_decode(get_bloginfo('name'), ENT_QUOTES);
        $site_url = home_url('/');
        $admin_url = admin_url();

        $request_type_labels = [
            'support' => 'Support Request',
            'bug' => 'Bug Report',
            'feedback' => 'Feedback',
            'suggestion' => 'Suggestion',
        ];
        $priority_labels = [
            'low' => 'Low',
            'normal' => 'Normal',
            'high' => 'High',
        ];

        $request_type_label = $request_type_labels[$request_type] ?? $request_type_labels['support'];
        $priority_label = $priority_labels[$priority] ?? $priority_labels['normal'];

        $body = "Support Request: TechDex Client Post Preview\n";
        $body .= "====================================\n\n";
        $body .= "Request Details\n";
        $body .= "---------------\n";
        $body .= "Type: {$request_type_label}\n";
        $body .= "Priority: {$priority_label}\n";
        $body .= "Sender Name: {$name}\n";
        $body .= "Sender Email: {$email}\n";
        if ('' !== $page_url) {
            $body .= "Affected Page URL: {$page_url}\n";
        }
        $body .= "\n";
        $body .= "Message\n";
        $body .= "-------\n";
        $body .= "{$message}\n\n";

        if ('' !== $steps) {
            $body .= "Steps / Context\n";
            $body .= "---------------\n";
            $body .= "{$steps}\n\n";
        }

        $body .= "Plugin / Site Metadata\n";
        $body .= "----------------------\n";
        $body .= "Site Name: {$site_name}\n";
        $body .= "Site URL: {$site_url}\n";
        $body .= "Admin URL: {$admin_url}\n";
        $body .= "Plugin Version: " . self::VERSION . "\n";
        $body .= "WordPress Version: " . get_bloginfo('version') . "\n";
        $body .= "PHP Version: " . PHP_VERSION . "\n";
        $body .= "Theme: " . wp_get_theme()->get('Name') . "\n";

        $headers = [];

        if ('' !== $email && is_email($email)) {
            $headers[] = 'Reply-To: ' . $name . ' <' . $email . '>';
        }

        $sent = wp_mail(
            self::SUPPORT_EMAIL,
            'Support Request: TechDex Client Post Preview',
            $body,
            $headers
        );

        $redirect_url = add_query_arg(
            [
                'page' => self::SUPPORT_PAGE_SLUG,
                self::NOTICE_QUERY_ARG => $sent ? 'support_sent' : 'support_failed',
            ],
            admin_url('admin.php')
        );

        wp_safe_redirect($redirect_url);
        exit;
    }

    private static function build_preview_url(int $post_id, ?string $link_id = null): string {
        $post = get_post($post_id);

        if (!$post || !self::is_post_preview_eligible($post)) {
            return '';
        }

        $link = self::get_preview_link($post_id, $link_id);

        if (!$link || '' === (string) $link['token'] || empty($link['token_hash'])) {
            return '';
        }

        return add_query_arg(
            [
                self::QUERY_FLAG => '1',
                self::QUERY_POST => $post_id,
                self::QUERY_LINK => $link['id'],
                self::QUERY_TOKEN => $link['token'],
            ],
            home_url('/')
        );
    }

    private static function get_valid_preview_post() {
        if (null !== self::$request_is_valid) {
            return self::$request_is_valid ? self::$active_post : null;
        }

        self::$request_is_valid = false;

        if (!self::is_preview_request()) {
            return null;
        }

        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public preview requests are validated by signed token and expiration checks, not admin nonces.
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only preview request parameter used for front-end redirect routing.
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only preview request parameter used for front-end routing of signed preview links.
        $post_id = isset($_GET[self::QUERY_POST]) ? absint(wp_unslash($_GET[self::QUERY_POST])) : 0;
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public preview requests are validated by signed token and expiration checks, not admin nonces.
        $token = isset($_GET[self::QUERY_TOKEN]) ? sanitize_text_field(wp_unslash($_GET[self::QUERY_TOKEN])) : '';
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public preview requests are validated by signed token and expiration checks, not admin nonces.
        $link_id = isset($_GET[self::QUERY_LINK]) ? sanitize_text_field(wp_unslash($_GET[self::QUERY_LINK])) : '';

        if (!$post_id || '' === $token) {
            return null;
        }

        $post = get_post($post_id);

        if (!$post || !self::is_post_preview_eligible($post)) {
            return null;
        }

        $matched_link = null;

        foreach (self::get_preview_links($post_id) as $link) {
            if ('' !== $link_id && (string) $link['id'] !== $link_id) {
                continue;
            }

            if ((int) $link['expires_at'] > 0 && (int) $link['expires_at'] < time()) {
                continue;
            }

            if (!wp_check_password($token, (string) $link['token_hash'])) {
                continue;
            }

            $matched_link = $link;
            break;
        }

        if (!$matched_link) {
            return null;
        }

        self::$active_post = $post;
        self::$active_link = $matched_link;
        self::$request_is_valid = true;

        return self::$active_post;
    }

    private static function get_supported_post_types(): array {
        $types = get_post_types(
            [
                'show_ui' => true,
            ],
            'names'
        );

        unset($types['attachment']);

        return array_values($types);
    }

    private static function is_post_preview_eligible(WP_Post $post): bool {
        return !in_array($post->post_status, ['publish', 'trash', 'auto-draft'], true);
    }

    private static function has_saved_preview_link(int $post_id): bool {
        return count(self::get_preview_links($post_id)) > 0;
    }

    private static function has_active_preview_link(int $post_id): bool {
        $post = get_post($post_id);

        if (!$post || !self::is_post_preview_eligible($post)) {
            return false;
        }

        foreach (self::get_preview_links($post_id) as $link) {
            if (0 === (int) $link['expires_at'] || (int) $link['expires_at'] >= time()) {
                return true;
            }
        }

        return false;
    }

    private static function get_requested_preview_post() {
        if (!self::is_preview_request()) {
            return null;
        }

        $post_id = isset($_GET[self::QUERY_POST]) ? absint(wp_unslash($_GET[self::QUERY_POST])) : 0;

        if ($post_id <= 0) {
            return null;
        }

        return get_post($post_id);
    }

    private static function get_preview_posts(): array {
        return get_posts(
            [
                'post_type' => self::get_supported_post_types(),
                'post_status' => 'any',
                'posts_per_page' => -1,
                'orderby' => 'modified',
                'order' => 'DESC',
                'meta_query' => [
                    'relation' => 'OR',
                    [
                        'key' => self::META_LINKS,
                        'compare' => 'EXISTS',
                    ],
                    [
                        'key' => self::META_TOKEN_HASH,
                        'compare' => 'EXISTS',
                    ],
                ],
            ]
        );
    }

    private static function get_saved_preview_posts_count(): int {
        $count = 0;

        foreach (self::get_preview_posts() as $post) {
            if (self::has_saved_preview_link((int) $post->ID)) {
                $count++;
            }
        }

        return $count;
    }

    private static function can_generate_preview_for_post(int $post_id): bool {
        $link_count = count(self::get_preview_links($post_id));

        if ($link_count > 0) {
            return $link_count < self::get_preview_link_limit();
        }

        return self::get_saved_preview_posts_count() < self::get_preview_slot_limit();
    }

    private static function get_preview_slot_limit(): int {
        return self::is_pro_active() ? self::PRO_ACTIVE_POST_LIMIT : self::FREE_ACTIVE_POST_LIMIT;
    }

    private static function get_preview_link_limit(): int {
        if (self::is_agency_active()) {
            return self::AGENCY_LINK_LIMIT;
        }

        if (self::is_pro_active()) {
            return self::PRO_LINK_LIMIT;
        }

        return self::FREE_LINK_LIMIT;
    }

    private static function get_expiration_window(?int $expiration_days = null): int {
        $days = $expiration_days ?? self::get_default_expiration_days();
        $expiration = $days * DAY_IN_SECONDS;
        $expiration = (int) apply_filters('techdex_client_post_preview_expiration', $expiration);
        // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Backward-compatibility filter for older integrations.
        $expiration = (int) apply_filters('client_post_preview_expiration', $expiration);

        return $expiration > 0 ? $expiration : self::DEFAULT_EXPIRATION;
    }

    private static function should_auto_revoke_link(array $link): bool {
        $expires_at = (int) ($link['expires_at'] ?? 0);

        return !empty($link['auto_revoke']) && $expires_at > 0 && $expires_at < time();
    }

    private static function get_admin_post_id(): int {
        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Post ID is sanitized here and nonce verification happens in the calling admin handlers.
        if (isset($_POST['post_id'])) {
            // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Post ID is sanitized here and nonce verification happens in the calling admin handlers.
            return absint(wp_unslash($_POST['post_id']));
        }

        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only fallback for admin screen context and redirects.
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only fallback for admin screen context and redirects.
        return isset($_GET['post_id']) ? absint(wp_unslash($_GET['post_id'])) : 0;
    }

    private static function get_default_expiration_days(): int {
        if (!self::is_pro_active()) {
            return self::DEFAULT_EXPIRATION_DAYS;
        }

        $days = (int) get_option(self::OPTION_DEFAULT_EXPIRATION_DAYS, self::DEFAULT_EXPIRATION_DAYS);

        return $days > 0 ? $days : self::DEFAULT_EXPIRATION_DAYS;
    }

    private static function get_log_retention_options(): array {
        if (self::is_agency_active()) {
            return [7, 14, 30, 60, 90];
        }

        if (self::is_pro_active()) {
            return [7, 14];
        }

        return [0];
    }

    private static function get_default_log_retention_days(): int {
        if (self::is_agency_active()) {
            return self::DEFAULT_LOG_RETENTION_DAYS;
        }

        if (self::is_pro_active()) {
            return 14;
        }

        return 0;
    }

    private static function get_log_retention_days(): int {
        if (!self::is_pro_active()) {
            return 0;
        }

        $days = (int) get_option(self::OPTION_LOG_RETENTION_DAYS, self::get_default_log_retention_days());

        return in_array($days, self::get_log_retention_options(), true) ? $days : self::get_default_log_retention_days();
    }

    private static function prune_access_log_entries($entries): array {
        if (!is_array($entries)) {
            return [];
        }

        if (self::get_log_retention_days() <= 0) {
            return [];
        }

        $cutoff = time() - (self::get_log_retention_days() * DAY_IN_SECONDS);
        $pruned = array_values(array_filter(
            $entries,
            static function ($entry) use ($cutoff): bool {
                return is_array($entry) && !empty($entry['timestamp']) && (int) $entry['timestamp'] >= $cutoff;
            }
        ));

        return array_slice($pruned, 0, self::ACCESS_LOG_LIMIT);
    }

    private static function get_requested_expiration_days(): int {
        if (!self::is_pro_active()) {
            return self::DEFAULT_EXPIRATION_DAYS;
        }

        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Expiration changes are only processed from handlers that verify their admin nonce.
        $days = isset($_POST['expiration_days']) ? absint(wp_unslash($_POST['expiration_days'])) : 0;

        return $days > 0 ? $days : self::get_default_expiration_days();
    }

    private static function get_link_expiration_days_from_record(array $link): int {
        $created_at = (int) ($link['created_at'] ?? 0);
        $expires_at = (int) ($link['expires_at'] ?? 0);

        if ($expires_at <= 0) {
            return self::get_default_expiration_days();
        }

        if ($created_at > 0 && $expires_at > $created_at) {
            $days = (int) ceil(($expires_at - $created_at) / DAY_IN_SECONDS);

            if ($days > 0) {
                return $days;
            }
        }

        return self::get_default_expiration_days();
    }

    private static function get_link_expiration_days(int $post_id): int {
        $link = self::get_preview_link($post_id);

        return $link ? self::get_link_expiration_days_from_record($link) : self::get_default_expiration_days();
    }

    private static function get_admin_link_id(): string {
        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Link ID is sanitized here and nonce verification happens in the calling admin handlers.
        if (isset($_POST['link_id'])) {
            // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Link ID is sanitized here and nonce verification happens in the calling admin handlers.
            return sanitize_text_field(wp_unslash($_POST['link_id']));
        }

        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only fallback for admin screen context and redirects.
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only fallback for admin screen context and redirects.
        return isset($_GET['link_id']) ? sanitize_text_field(wp_unslash($_GET['link_id'])) : '';
    }

    private static function handle_link_lifecycle_action(string $action): void {
        $post_id = self::get_admin_post_id();
        $link_id = self::get_admin_link_id();

        if (!$post_id) {
            wp_die(esc_html__('Invalid post ID.', 'techdex-client-post-preview'));
        }

        if (!current_user_can('edit_post', $post_id)) {
            wp_die(esc_html__('You do not have permission to manage preview links for this post.', 'techdex-client-post-preview'));
        }

        if (!self::is_agency_active()) {
            wp_die(esc_html__('This preview-link workflow is only available on the Agency plan.', 'techdex-client-post-preview'));
        }

        if ('' === $link_id) {
            wp_die(esc_html__('No preview link was selected.', 'techdex-client-post-preview'));
        }

        if ('admin_post_cpp_manage_link_workflow' === current_action()) {
            check_admin_referer('cpp_manage_link_workflow_' . $post_id . '_' . $link_id);
        } else {
            check_admin_referer('cpp_' . $action . '_link_' . $post_id . '_' . $link_id);
        }

        $links = self::get_preview_links($post_id);
        $updated = false;
        $notice = '';

        foreach ($links as &$link) {
            if ((string) $link['id'] !== $link_id) {
                continue;
            }

            switch ($action) {
                case 'renew':
                    self::renew_preview_link($link);
                    $notice = 'link_renewed';
                    break;
                case 'regenerate':
                    self::regenerate_preview_link($link);
                    $notice = 'link_regenerated';
                    break;
                case 'toggle_auto_revoke':
                    $link['auto_revoke'] = empty($link['auto_revoke']);
                    $notice = 'link_auto_revoke_updated';
                    break;
                case 'never_expire':
                    $link['created_at'] = time();
                    $link['expires_at'] = 0;
                    $link['auto_revoke'] = false;
                    $notice = 'link_never_expires';
                    break;
            }

            $updated = true;
            break;
        }
        unset($link);

        if (!$updated) {
            wp_die(esc_html__('That preview link could not be found.', 'techdex-client-post-preview'));
        }

        self::save_preview_links($post_id, $links);
        wp_safe_redirect(self::get_preview_links_page_url(['post_id' => $post_id, self::NOTICE_QUERY_ARG => $notice]));
        exit;
    }

    private static function handle_agency_link_setting_update(string $setting): void {
        $post_id = self::get_admin_post_id();
        $link_id = self::get_admin_link_id();

        if (!$post_id) {
            wp_die(esc_html__('Invalid post ID.', 'techdex-client-post-preview'));
        }

        if (!current_user_can('edit_post', $post_id)) {
            wp_die(esc_html__('You do not have permission to manage preview links for this post.', 'techdex-client-post-preview'));
        }

        if (!self::is_agency_active()) {
            wp_die(esc_html__('This preview-link setting is only available on the Agency plan.', 'techdex-client-post-preview'));
        }

        if ('' === $link_id) {
            wp_die(esc_html__('No preview link was selected.', 'techdex-client-post-preview'));
        }

        check_admin_referer('cpp_update_link_' . $setting . '_' . $post_id . '_' . $link_id);

        $links = self::get_preview_links($post_id);
        $updated = false;
        $notice = '';

        foreach ($links as &$link) {
            if ((string) $link['id'] !== $link_id) {
                continue;
            }

            if ('label' === $setting) {
                $link['label'] = isset($_POST['link_label']) ? sanitize_text_field((string) wp_unslash($_POST['link_label'])) : '';
                $notice = 'link_label_updated';
            } elseif ('passcode' === $setting) {
                // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Passcodes must remain raw so punctuation and spacing survive password hashing.
                $passcode = isset($_POST['link_passcode']) ? trim((string) wp_unslash($_POST['link_passcode'])) : '';

                if ('' === $passcode) {
                    wp_die(esc_html__('Enter a passcode before saving it.', 'techdex-client-post-preview'));
                }

                $link['passcode_hash'] = wp_hash_password($passcode);
                $notice = 'link_passcode_updated';
            } elseif ('clear_passcode' === $setting) {
                $link['passcode_hash'] = '';
                $notice = 'link_passcode_cleared';
            } elseif ('clear_log' === $setting) {
                $link['access_log'] = [];
                $link['view_count'] = 0;
                $link['last_viewed_at'] = 0;
                $notice = 'link_log_cleared';
            }

            $updated = true;
            break;
        }
        unset($link);

        if (!$updated) {
            wp_die(esc_html__('That preview link could not be found.', 'techdex-client-post-preview'));
        }

        self::save_preview_links($post_id, $links);
        wp_safe_redirect(self::get_preview_links_page_url(['post_id' => $post_id, self::NOTICE_QUERY_ARG => $notice]));
        exit;
    }

    private static function renew_preview_link(array &$link): void {
        $duration_days = self::get_link_expiration_days_from_record($link);
        $now = time();
        $link['created_at'] = $now;
        $link['expires_at'] = $now + self::get_expiration_window($duration_days);
    }

    private static function regenerate_preview_link(array &$link): void {
        $duration_days = self::get_link_expiration_days_from_record($link);
        $token = wp_generate_password(48, false, false);
        $link['token'] = $token;
        $link['token_hash'] = wp_hash_password($token);
        $link['created_at'] = time();
        $link['created_by'] = get_current_user_id();

        if ((int) ($link['expires_at'] ?? 0) > 0) {
            $link['expires_at'] = $link['created_at'] + self::get_expiration_window($duration_days);
        } else {
            $link['expires_at'] = 0;
            $link['auto_revoke'] = false;
        }
    }

    private static function get_preview_passcode_cookie_name(string $link_id): string {
        return 'cpp_pass_' . md5($link_id);
    }

    private static function get_preview_passcode_cookie_value(string $link_id, string $passcode_hash): string {
        return hash_hmac('sha256', $link_id . '|' . $passcode_hash, wp_salt('auth'));
    }

    private static function has_valid_preview_passcode_cookie(string $link_id, string $passcode_hash): bool {
        $cookie_name = self::get_preview_passcode_cookie_name($link_id);

        if (!isset($_COOKIE[$cookie_name])) {
            return false;
        }

        $cookie_value = sanitize_text_field(wp_unslash($_COOKIE[$cookie_name]));

        return hash_equals(self::get_preview_passcode_cookie_value($link_id, $passcode_hash), $cookie_value);
    }

    private static function set_preview_passcode_cookie(string $link_id, string $passcode_hash, int $expires_at): void {
        $cookie_name = self::get_preview_passcode_cookie_name($link_id);
        $cookie_value = self::get_preview_passcode_cookie_value($link_id, $passcode_hash);
        $expires = $expires_at > 0 ? $expires_at : time() + WEEK_IN_SECONDS;

        setcookie(
            $cookie_name,
            $cookie_value,
            [
                'expires' => $expires,
                'path' => COOKIEPATH ? COOKIEPATH : '/',
                'domain' => COOKIE_DOMAIN ?: '',
                'secure' => is_ssl(),
                'httponly' => true,
                'samesite' => 'Lax',
            ]
        );

        $_COOKIE[$cookie_name] = $cookie_value;
    }

    private static function render_preview_passcode_gate(WP_Post $post, string $error = ''): void {
        status_header(200);
        nocache_headers();

        echo '<!doctype html><html ' . wp_kses_data(get_language_attributes()) . '><head>';
        echo '<meta charset="' . esc_attr(get_bloginfo('charset')) . '" />';
        echo '<meta name="viewport" content="width=device-width, initial-scale=1" />';
        echo '<title>' . esc_html__('Preview Passcode Required', 'techdex-client-post-preview') . '</title>';
        echo '</head><body style="margin:0;background:#f6f7f7;color:#1d2327;font:16px/1.5 system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;">';
        echo '<div style="max-width:520px;margin:10vh auto;padding:32px;background:#fff;border:1px solid #dcdcde;border-radius:14px;box-shadow:0 18px 40px rgba(0,0,0,.08);">';
        echo '<h1 style="margin:0 0 12px;font-size:32px;">' . esc_html__('Protected Preview', 'techdex-client-post-preview') . '</h1>';
        /* translators: %s: title of the post being previewed. */
        echo '<p style="margin:0 0 18px;color:#50575e;">' . esc_html(sprintf(__('Enter the passcode to open the preview for "%s".', 'techdex-client-post-preview'), get_the_title($post))) . '</p>';

        if ('' !== $error) {
            echo '<div style="margin:0 0 18px;padding:12px 14px;border-left:4px solid #dc2626;background:#fee2e2;color:#991b1b;border-radius:6px;">' . esc_html($error) . '</div>';
        }

        echo '<form method="post">';
        wp_nonce_field('cpp_preview_passcode_' . (string) self::$active_link['id']);
        echo '<label for="cpp_preview_passcode" style="display:block;font-weight:600;margin-bottom:8px;">' . esc_html__('Passcode', 'techdex-client-post-preview') . '</label>';
        echo '<input type="password" id="cpp_preview_passcode" name="cpp_preview_passcode" style="width:100%;padding:12px 14px;border:1px solid #c3c4c7;border-radius:8px;font-size:16px;box-sizing:border-box;" />';
        echo '<p style="margin:16px 0 0;"><button type="submit" name="cpp_preview_passcode_submit" value="1" style="padding:12px 16px;border:0;border-radius:8px;background:#2271b1;color:#fff;font-weight:600;cursor:pointer;">' . esc_html__('Open Preview', 'techdex-client-post-preview') . '</button></p>';
        echo '</form>';
        echo '</div></body></html>';
    }

    private static function get_current_request_url(): string {
        $scheme = is_ssl() ? 'https://' : 'http://';
        $host = isset($_SERVER['HTTP_HOST']) ? sanitize_text_field((string) wp_unslash($_SERVER['HTTP_HOST'])) : wp_parse_url(home_url('/'), PHP_URL_HOST);
        $uri = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : '/';

        return $scheme . $host . $uri;
    }

    private static function get_anonymized_request_ip(): string {
        $ip = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '';

        if ('' === $ip) {
            return __('Unknown', 'techdex-client-post-preview');
        }

        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
            $parts = explode('.', $ip);
            $parts[3] = 'x';
            return implode('.', $parts);
        }

        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
            $parts = explode(':', $ip);
            $parts = array_slice($parts, 0, 4);
            return implode(':', $parts) . ':x:x:x:x';
        }

        return __('Unknown', 'techdex-client-post-preview');
    }

    private static function get_request_user_agent(): string {
        $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field((string) wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '';

        return '' !== $user_agent ? substr($user_agent, 0, 120) : __('Unknown device', 'techdex-client-post-preview');
    }

    public static function sanitize_default_expiration_days($value): int {
        if (!self::is_pro_active()) {
            return self::DEFAULT_EXPIRATION_DAYS;
        }

        $days = absint($value);

        return $days > 0 ? $days : self::DEFAULT_EXPIRATION_DAYS;
    }

    public static function sanitize_log_retention_days($value): int {
        if (!self::is_pro_active()) {
            return 0;
        }

        $days = absint($value);

        return in_array($days, self::get_log_retention_options(), true) ? $days : self::get_default_log_retention_days();
    }

    public static function sanitize_license_key($value): string {
        $license_key = strtoupper(trim(sanitize_text_field((string) $value)));
        $existing_key = self::get_saved_license_key();

        if ($license_key !== $existing_key) {
            delete_option(self::OPTION_LICENSE_STATE);
        }

        return $license_key;
    }

    private static function get_dashboard_page_url(array $args = []): string {
        return add_query_arg(
            array_merge([
                'page' => self::DASHBOARD_PAGE_SLUG,
            ], $args),
            admin_url('admin.php')
        );
    }

    private static function get_preview_links_page_url(array $args = []): string {
        return add_query_arg(
            array_merge([
                'page' => self::PREVIEW_LINKS_PAGE_SLUG,
            ], $args),
            admin_url('admin.php')
        );
    }

    private static function get_getting_started_page_url(): string {
        return add_query_arg(
            [
                'page' => self::GETTING_STARTED_PAGE_SLUG,
            ],
            admin_url('admin.php')
        );
    }

    private static function get_readme_page_url(): string {
        return add_query_arg(
            [
                'page' => self::README_PAGE_SLUG,
            ],
            admin_url('admin.php')
        );
    }

    private static function get_license_page_url(array $args = []): string {
        return add_query_arg(
            array_merge([
                'page' => self::LICENSE_PAGE_SLUG,
            ], $args),
            admin_url('admin.php')
        );
    }

    private static function get_details_page_url(array $args = []): string {
        return add_query_arg(
            array_merge([
                'page' => self::DETAILS_PAGE_SLUG,
            ], $args),
            admin_url('admin.php')
        );
    }

    private static function get_link_details_page_url(array $args = []): string {
        return add_query_arg(
            array_merge([
                'page' => self::LINK_DETAILS_PAGE_SLUG,
            ], $args),
            admin_url('admin.php')
        );
    }

    private static function get_changelog_page_url(): string {
        return add_query_arg(
            [
                'page' => self::CHANGELOG_PAGE_SLUG,
            ],
            admin_url('admin.php')
        );
    }

    private static function get_support_page_url(): string {
        return add_query_arg(
            [
                'page' => self::SUPPORT_PAGE_SLUG,
            ],
            admin_url('admin.php')
        );
    }

    private static function get_plugin_details_url(): string {
        return add_query_arg(
            [
                'tab' => 'plugin-information',
                'plugin' => self::PRODUCT_KEY,
                'TB_iframe' => 'true',
                'width' => 600,
                'height' => 550,
            ],
            admin_url('plugin-install.php')
        );
    }

    private static function is_link_details_page_request(): bool {
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin page detection.
        $page = isset($_GET['page']) ? sanitize_key(wp_unslash($_GET['page'])) : '';

        return self::LINK_DETAILS_PAGE_SLUG === $page;
    }

    private static function get_asset_url(string $filename): string {
        return plugin_dir_url(__FILE__) . 'assets/' . ltrim($filename, '/');
    }

    private static function get_revoke_redirect_url(int $post_id): string {
        $fallback = add_query_arg(
            [
                'post' => $post_id,
                'action' => 'edit',
                self::NOTICE_QUERY_ARG => 'revoked',
            ],
            admin_url('post.php')
        );

        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only redirect target used after a verified revoke request.
        if (!isset($_GET['redirect_to'])) {
            return $fallback;
        }

        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only redirect target used after a verified revoke request.
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only redirect target used after a verified revoke request.
        $redirect_to = sanitize_text_field(wp_unslash($_GET['redirect_to']));
        $redirect_to = rawurldecode($redirect_to);
        $redirect_to = wp_validate_redirect($redirect_to, '');

        if ('' === $redirect_to) {
            return $fallback;
        }

        return add_query_arg(
            [
                self::NOTICE_QUERY_ARG => 'revoked',
            ],
            $redirect_to
        );
    }

    private static function get_safe_link_id_for_revoke(int $post_id, string $requested_link_id): string {
        if ('' !== $requested_link_id) {
            return $requested_link_id;
        }

        $link = self::get_preview_link($post_id);

        return $link ? (string) $link['id'] : '';
    }

    private static function is_preview_request(): bool {
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public preview requests are validated by signed token and expiration checks, not admin nonces.
        return isset($_GET[self::QUERY_FLAG], $_GET[self::QUERY_POST], $_GET[self::QUERY_TOKEN]);
    }

    private static function is_active_preview_request(): bool {
        return null !== self::$request_is_valid ? self::$request_is_valid : (bool) self::get_valid_preview_post();
    }

    private static function get_preview_links(int $post_id): array {
        self::migrate_legacy_preview_link($post_id);

        $stored_links = get_post_meta($post_id, self::META_LINKS, true);

        if (!is_array($stored_links)) {
            return [];
        }

        $links = [];
        $position = 1;

        foreach ($stored_links as $link) {
            if (!is_array($link)) {
                continue;
            }

            $token = isset($link['token']) ? self::normalize_stored_token($post_id, (string) $link['token']) : '';
            $token_hash = isset($link['token_hash']) ? (string) $link['token_hash'] : '';

            if ('' === $token || '' === $token_hash) {
                continue;
            }

            $links[] = [
                'id' => isset($link['id']) && '' !== (string) $link['id'] ? (string) $link['id'] : wp_generate_uuid4(),
                'token' => $token,
                'token_hash' => $token_hash,
                'label' => isset($link['label']) ? sanitize_text_field((string) $link['label']) : '',
                'passcode_hash' => isset($link['passcode_hash']) ? (string) $link['passcode_hash'] : '',
                'expires_at' => isset($link['expires_at']) ? (int) $link['expires_at'] : 0,
                'created_at' => isset($link['created_at']) ? (int) $link['created_at'] : 0,
                'created_by' => isset($link['created_by']) ? (int) $link['created_by'] : 0,
                'auto_revoke' => !empty($link['auto_revoke']),
                'view_count' => isset($link['view_count']) ? (int) $link['view_count'] : 0,
                'last_viewed_at' => isset($link['last_viewed_at']) ? (int) $link['last_viewed_at'] : 0,
                'access_log' => isset($link['access_log']) && is_array($link['access_log']) ? array_values($link['access_log']) : [],
                'position' => $position,
            ];
            $position++;
        }

        $active_links = [];
        $did_cleanup = false;

        foreach ($links as $link) {
            if (self::should_auto_revoke_link($link)) {
                $did_cleanup = true;
                continue;
            }

            $pruned_log = self::prune_access_log_entries($link['access_log'] ?? []);
            if ($pruned_log !== ($link['access_log'] ?? [])) {
                $link['access_log'] = $pruned_log;
                $did_cleanup = true;
            }

            $link['position'] = count($active_links) + 1;
            $active_links[] = $link;
        }

        if ($did_cleanup) {
            self::save_preview_links($post_id, $active_links);
        }

        return $active_links;
    }

    private static function get_preview_link(int $post_id, ?string $link_id = null): ?array {
        $links = self::get_preview_links($post_id);

        if ([] === $links) {
            return null;
        }

        if (null === $link_id || '' === $link_id) {
            return $links[0];
        }

        foreach ($links as $link) {
            if ((string) $link['id'] === $link_id) {
                return $link;
            }
        }

        return null;
    }

    private static function save_preview_links(int $post_id, array $links): void {
        if ([] === $links) {
            delete_post_meta($post_id, self::META_LINKS);
            delete_post_meta($post_id, self::META_TOKEN);
            delete_post_meta($post_id, self::META_TOKEN_HASH);
            delete_post_meta($post_id, self::META_EXPIRES_AT);
            delete_post_meta($post_id, self::META_CREATED_AT);
            delete_post_meta($post_id, self::META_CREATED_BY);
            return;
        }

        $payload = [];

        foreach ($links as $link) {
            if (!is_array($link) || empty($link['id']) || empty($link['token']) || empty($link['token_hash'])) {
                continue;
            }

            $payload[] = [
                'id' => (string) $link['id'],
                'token' => self::encrypt_token((string) $link['token']),
                'token_hash' => (string) $link['token_hash'],
                'label' => sanitize_text_field((string) ($link['label'] ?? '')),
                'passcode_hash' => (string) ($link['passcode_hash'] ?? ''),
                'expires_at' => (int) ($link['expires_at'] ?? 0),
                'created_at' => (int) ($link['created_at'] ?? 0),
                'created_by' => (int) ($link['created_by'] ?? 0),
                'auto_revoke' => !empty($link['auto_revoke']),
                'view_count' => (int) ($link['view_count'] ?? 0),
                'last_viewed_at' => (int) ($link['last_viewed_at'] ?? 0),
                'access_log' => isset($link['access_log']) && is_array($link['access_log']) ? array_values($link['access_log']) : [],
            ];
        }

        if ([] === $payload) {
            delete_post_meta($post_id, self::META_LINKS);
            return;
        }

        update_post_meta($post_id, self::META_LINKS, $payload);
        delete_post_meta($post_id, self::META_TOKEN);
        delete_post_meta($post_id, self::META_TOKEN_HASH);
        delete_post_meta($post_id, self::META_EXPIRES_AT);
        delete_post_meta($post_id, self::META_CREATED_AT);
        delete_post_meta($post_id, self::META_CREATED_BY);
    }

    private static function create_preview_link_record(string $token): array {
        return [
            'id' => wp_generate_uuid4(),
            'token' => $token,
            'token_hash' => wp_hash_password($token),
            'label' => '',
            'passcode_hash' => '',
            'expires_at' => time() + self::get_expiration_window(),
            'created_at' => time(),
            'created_by' => get_current_user_id(),
            'auto_revoke' => false,
            'view_count' => 0,
            'last_viewed_at' => 0,
            'access_log' => [],
        ];
    }

    private static function migrate_legacy_preview_link(int $post_id): void {
        $stored_token = (string) get_post_meta($post_id, self::META_TOKEN, true);
        $token_hash = (string) get_post_meta($post_id, self::META_TOKEN_HASH, true);

        if ('' === $stored_token || '' === $token_hash || is_array(get_post_meta($post_id, self::META_LINKS, true))) {
            return;
        }

        $token = self::normalize_stored_token($post_id, $stored_token);

        if ('' === $token) {
            return;
        }

        $created_at = (int) get_post_meta($post_id, self::META_CREATED_AT, true);
        $created_by = (int) get_post_meta($post_id, self::META_CREATED_BY, true);
        $expires_at = (int) get_post_meta($post_id, self::META_EXPIRES_AT, true);

        self::save_preview_links($post_id, [[
            'id' => wp_generate_uuid4(),
            'token' => $token,
            'token_hash' => $token_hash,
            'label' => '',
            'passcode_hash' => '',
            'expires_at' => $expires_at,
            'created_at' => $created_at,
            'created_by' => $created_by,
            'auto_revoke' => false,
            'view_count' => 0,
            'last_viewed_at' => 0,
            'access_log' => [],
        ]]);
    }

    private static function normalize_stored_token(int $post_id, string $stored_token): string {

        if ('' === $stored_token) {
            return '';
        }

        if (!self::is_encrypted_token($stored_token)) {
            $encrypted_token = self::encrypt_token($stored_token);

            if ($encrypted_token !== $stored_token) {
                update_post_meta($post_id, self::META_TOKEN, $encrypted_token);
            }

            return $stored_token;
        }

        return self::decrypt_token($stored_token);
    }

    private static function is_encrypted_token(string $token): bool {
        return 0 === strpos($token, self::ENCRYPTED_TOKEN_PREFIX);
    }

    private static function encrypt_token(string $token): string {
        if ('' === $token || !function_exists('openssl_encrypt') || !function_exists('openssl_cipher_iv_length')) {
            return $token;
        }

        $iv_length = openssl_cipher_iv_length(self::ENCRYPTION_CIPHER);

        if (!is_int($iv_length) || $iv_length <= 0) {
            return $token;
        }

        try {
            $iv = random_bytes($iv_length);
        } catch (Exception $exception) {
            return $token;
        }

        $ciphertext = openssl_encrypt(
            $token,
            self::ENCRYPTION_CIPHER,
            self::get_encryption_key(),
            OPENSSL_RAW_DATA,
            $iv
        );

        if (!is_string($ciphertext) || '' === $ciphertext) {
            return $token;
        }

        return self::ENCRYPTED_TOKEN_PREFIX . base64_encode($iv . $ciphertext);
    }

    private static function decrypt_token(string $stored_token): string {
        if (!self::is_encrypted_token($stored_token) || !function_exists('openssl_decrypt') || !function_exists('openssl_cipher_iv_length')) {
            return $stored_token;
        }

        $payload = substr($stored_token, strlen(self::ENCRYPTED_TOKEN_PREFIX));
        $decoded = base64_decode($payload, true);
        $iv_length = openssl_cipher_iv_length(self::ENCRYPTION_CIPHER);

        if (!is_string($decoded) || !is_int($iv_length) || $iv_length <= 0 || strlen($decoded) <= $iv_length) {
            return '';
        }

        $iv = substr($decoded, 0, $iv_length);
        $ciphertext = substr($decoded, $iv_length);

        $token = openssl_decrypt(
            $ciphertext,
            self::ENCRYPTION_CIPHER,
            self::get_encryption_key(),
            OPENSSL_RAW_DATA,
            $iv
        );

        return is_string($token) ? $token : '';
    }

    private static function get_encryption_key(): string {
        return hash('sha256', 'techdex-client-post-preview|' . wp_salt('auth'), true);
    }

    private static function render_readme_markdown(string $markdown): string {
        $lines = preg_split("/\r\n|\n|\r/", $markdown);

        if (false === $lines) {
            return '';
        }

        $html = '';
        $in_list = false;

        foreach ($lines as $line) {
            $trimmed = trim($line);

            if ('' === $trimmed) {
                if ($in_list) {
                    $html .= '</ul>';
                    $in_list = false;
                }
                continue;
            }

            if (0 === strpos($trimmed, '# ')) {
                if ($in_list) {
                    $html .= '</ul>';
                    $in_list = false;
                }
                $html .= '<h1>' . self::format_inline_markdown(substr($trimmed, 2)) . '</h1>';
                continue;
            }

            if (0 === strpos($trimmed, '## ')) {
                if ($in_list) {
                    $html .= '</ul>';
                    $in_list = false;
                }
                $html .= '<h2>' . self::format_inline_markdown(substr($trimmed, 3)) . '</h2>';
                continue;
            }

            if (preg_match('/^[-*]\s+(.+)$/', $trimmed, $matches)) {
                if (!$in_list) {
                    $html .= '<ul>';
                    $in_list = true;
                }
                $html .= '<li>' . self::format_inline_markdown($matches[1]) . '</li>';
                continue;
            }

            if (preg_match('/^\d+\.\s+(.+)$/', $trimmed, $matches)) {
                if (!$in_list) {
                    $html .= '<ul>';
                    $in_list = true;
                }
                $html .= '<li>' . self::format_inline_markdown($matches[1]) . '</li>';
                continue;
            }

            if ($in_list) {
                $html .= '</ul>';
                $in_list = false;
            }

            $html .= '<p>' . self::format_inline_markdown($trimmed) . '</p>';
        }

        if ($in_list) {
            $html .= '</ul>';
        }

        return $html;
    }

    private static function format_inline_markdown(string $text): string {
        $escaped = esc_html($text);
        $escaped = preg_replace('/`([^`]+)`/', '<code>$1</code>', $escaped);

        return is_string($escaped) ? $escaped : esc_html($text);
    }

    private static function get_editor_status_markup(int $post_id, int $created_at, int $expires_at, string $management_url): string {
        $status = self::get_expiration_status_data($created_at, $expires_at);

        if ([] === $status) {
            return '';
        }

        return sprintf(
            '<p><a href="%1$s" style="display:block;text-decoration:none;padding:10px 12px;border-left:4px solid %2$s;background:%3$s;color:%4$s;border-radius:6px;"><strong>%5$s</strong><br /><span style="color:%4$s;">%6$s</span></a></p>',
            esc_url($management_url),
            esc_attr($status['border']),
            esc_attr($status['background']),
            esc_attr($status['text']),
            esc_html($status['label']),
            esc_html($status['detail'])
        );
    }

    private static function get_expiration_status_data(int $created_at, int $expires_at): array {
        if ($created_at <= 0) {
            return [];
        }

        if ($expires_at <= 0) {
            return [
                'label' => __('Never Expires', 'techdex-client-post-preview'),
                'detail' => __('This preview link stays active until you revoke or regenerate it.', 'techdex-client-post-preview'),
                'background' => '#e0f2fe',
                'border' => '#0284c7',
                'text' => '#075985',
            ];
        }

        $now = time();
        $is_expired = $expires_at <= $now;
        $total_days = max(1, (int) ceil(max(1, $expires_at - $created_at) / DAY_IN_SECONDS));

        if ($is_expired) {
            return [
                'label' => __('Expired', 'techdex-client-post-preview'),
                'detail' => __('The link has expired. Open the management area to review or renew it later.', 'techdex-client-post-preview'),
                'background' => '#fee2e2',
                'border' => '#dc2626',
                'text' => '#991b1b',
            ];
        }

        $age_days = (int) floor(max(0, $now - $created_at) / DAY_IN_SECONDS) + 1;
        $status = [
            'label' => __('Active', 'techdex-client-post-preview'),
            'detail' => sprintf(
                /* translators: 1: current day number, 2: total number of days before expiration. */
                __('Day %1$d of %2$d. Click to manage or update this preview link.', 'techdex-client-post-preview'),
                $age_days,
                $total_days
            ),
            'background' => '#d1fae5',
            'border' => '#10b981',
            'text' => '#065f46',
        ];

        if ($age_days <= 4) {
            return $status;
        }

        if ($age_days <= 6) {
            $status['label'] = __('Expiring Soon', 'techdex-client-post-preview');
            $status['background'] = '#ffedd5';
            $status['border'] = '#f97316';
            $status['text'] = '#9a3412';

            return $status;
        }

        $status['label'] = __('Last Day', 'techdex-client-post-preview');
        $status['background'] = '#fee2e2';
        $status['border'] = '#dc2626';
        $status['text'] = '#991b1b';

        return $status;
    }

    private static function render_admin_page_header(string $active_tab, string $title, string $description): void {
        echo '<div class="wrap">';
        echo '<div style="display:flex;align-items:center;gap:14px;margin-bottom:14px;">';
        echo '<img src="' . esc_url(self::get_asset_url('icon-128x128.png')) . '" alt="' . esc_attr__('TechDex Client Post Preview', 'techdex-client-post-preview') . '" style="display:block;width:48px;height:48px;border-radius:12px;box-shadow:0 8px 20px rgba(0,0,0,.12);" />';
        echo '<div>';
        echo '<div style="margin:0;font-size:13px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:#50575e;">' . esc_html__('TechDex Client Post Preview', 'techdex-client-post-preview') . '</div>';
        echo '<h1 style="margin:4px 0 0;">' . esc_html($title) . '</h1>';
        echo '</div>';
        echo '</div>';
        echo '<p>' . esc_html($description) . '</p>';
        echo '<nav class="nav-tab-wrapper" style="margin-bottom:20px;">';

        foreach (self::get_admin_tabs() as $tab_id => $tab) {
            $classes = 'nav-tab';
            if ($tab_id === $active_tab) {
                $classes .= ' nav-tab-active';
            }

            echo '<a class="' . esc_attr($classes) . '" href="' . esc_url($tab['url']) . '">' . esc_html($tab['label']) . '</a>';
        }

        echo '</nav>';
    }

    private static function get_admin_tabs(): array {
        $tabs = [
            'dashboard' => [
                'label' => __('Dashboard', 'techdex-client-post-preview'),
                'url' => self::get_dashboard_page_url(),
            ],
            'preview-links' => [
                'label' => __('Preview Links', 'techdex-client-post-preview'),
                'url' => self::get_preview_links_page_url(),
            ],
            'getting-started' => [
                'label' => __('Getting Started', 'techdex-client-post-preview'),
                'url' => self::get_getting_started_page_url(),
            ],
            'readme' => [
                'label' => __('Readme', 'techdex-client-post-preview'),
                'url' => self::get_readme_page_url(),
            ],
            'license' => [
                'label' => __('License', 'techdex-client-post-preview'),
                'url' => self::get_license_page_url(),
            ],
            'support' => [
                'label' => __('Support', 'techdex-client-post-preview'),
                'url' => self::get_support_page_url(),
            ],
        ];

        if (self::is_agency_active()) {
            $tabs['details'] = [
                'label' => __('Details', 'techdex-client-post-preview'),
                'url' => self::get_details_page_url(),
            ];
        }

        $tabs['changelog'] = [
            'label' => __('Changelog', 'techdex-client-post-preview'),
            'url' => self::get_changelog_page_url(),
        ];

        return $tabs;
    }

    private static function get_changelog_summary_entries(): array {
        return [
            [
                'version' => '0.1.10',
                'date' => '2026-04-23',
                'tag' => __('Update', 'techdex-client-post-preview'),
                'items' => [
                    __('Tightened the commercial release build with improved Plugin Check compatibility and cleaner WordPress coding-standard coverage.', 'techdex-client-post-preview'),
                    __('Shortened the readme short description, normalized line endings, and prefixed uninstall globals for cleaner release packaging.', 'techdex-client-post-preview'),
                    __('Clarified read-only request handling so nonce-related lint warnings are reduced without changing the signed preview-link workflow.', 'techdex-client-post-preview'),
                ],
            ],
            [
                'version' => '0.1.9',
                'date' => '2026-04-23',
                'tag' => __('Update', 'techdex-client-post-preview'),
                'items' => [
                    __('Standardized the plugin admin tab wording to use Changelog instead of Change Log.', 'techdex-client-post-preview'),
                    __('Expanded the TechDex sales page with stronger SEO metadata, richer product schema, and AI-discovery references.', 'techdex-client-post-preview'),
                    __('Added an llms.txt product brief so AI systems can discover the canonical sales page, plan tiers, and support links more cleanly.', 'techdex-client-post-preview'),
                    __('Renamed the commercial plugin, release package, and TechDex product paths from Client Post Preview to TechDex Client Post Preview while keeping the existing licensing database name unchanged.', 'techdex-client-post-preview'),
                ],
            ],
            [
                'version' => '0.1.8',
                'date' => '2026-04-21',
                'tag' => __('Update', 'techdex-client-post-preview'),
                'items' => [
                    __('Added a real Agency Details tab with site-wide totals and per-link summary views while keeping separate drill-down detail screens for individual links.', 'techdex-client-post-preview'),
                    __('Tightened tier boundaries so Pro no longer sees Agency-only link details such as passcode state, auto-revoke state, and full access-log history.', 'techdex-client-post-preview'),
                    __('Expanded third-party admin-notice suppression across every TechDex Client Post Preview screen, including hidden detail pages.', 'techdex-client-post-preview'),
                    __('Split access-log retention by tier so Pro is capped at 7 or 14 days while Agency keeps the longer 30, 60, and 90 day options.', 'techdex-client-post-preview'),
                    __('Wired WordPress plugin update checks into the plugin and added a TechDex release metadata endpoint for future update delivery.', 'techdex-client-post-preview'),
                    __('Added a release manager to the TechDex product admin so current ZIP uploads can replace the live package while archiving the previous release by version and timestamp.', 'techdex-client-post-preview'),
                    __('Added a proper Plugins-screen View Details link so fresh installs can open the standard WordPress plugin details modal directly from the plugin row.', 'techdex-client-post-preview'),
                    __('Prevented duplicate Plugins-screen View Details links from appearing on some installs by skipping the extra row-meta link when one is already present.', 'techdex-client-post-preview'),
                    __('Kept the WordPress details modal in sync with detected updates by reusing cached release metadata before falling back to the installed plugin version.', 'techdex-client-post-preview'),
                    __('Fixed the standalone release feed so it no longer relied on WordPress-only helpers and now serves cache-busted direct ZIP download URLs for update checks.', 'techdex-client-post-preview'),
                    __('Corrected release packaging so WordPress installs the plugin from a proper single-folder ZIP instead of creating nested plugin paths.', 'techdex-client-post-preview'),
                    __('Clarified that the Free plan does not include access logs by showing 0-day retention and disabling log history entirely on Free.', 'techdex-client-post-preview'),
                    __('Removed leftover coming-soon positioning from the Agency plan card now that the tier is fully built out.', 'techdex-client-post-preview'),
                ],
            ],
            [
                'version' => '0.1.0',
                'date' => '2026-04-20',
                'tag' => __('Update', 'techdex-client-post-preview'),
                'items' => [
                    __('Completed the Agency feature layer with password-protected preview links, link labels, access logs, and view tracking.', 'techdex-client-post-preview'),
                    __('Added an Agency-only lifecycle workflow system for renew, regenerate, never-expire, and auto-revoke actions.', 'techdex-client-post-preview'),
                    __('Added link-detail screens with per-link summaries, preview-opening actions, and clear-log controls.', 'techdex-client-post-preview'),
                    __('Added configurable access-log retention with automatic pruning plus manual log clearing from the details view.', 'techdex-client-post-preview'),
                    __('Refined the Preview Links manager with collapsible per-link controls, remembered row state, alternating record styling, and streamlined link-management fields.', 'techdex-client-post-preview'),
                ],
            ],
            [
                'version' => '0.1.0',
                'date' => '2026-04-19',
                'tag' => __('Update', 'techdex-client-post-preview'),
                'items' => [
                    __('Moved the advanced per-link lifecycle workflow controls into the Agency tier while keeping per-link expiration updates available on Pro and Agency.', 'techdex-client-post-preview'),
                    __('Refined the Preview Links management layout to better support multiple preview links and cleaner per-link actions.', 'techdex-client-post-preview'),
                    __('Isolated the plugin dashboard screens from third-party admin notices so unrelated plugin promotions no longer appear inside TechDex Client Post Preview.', 'techdex-client-post-preview'),
                ],
            ],
            [
                'version' => '0.1.0',
                'date' => '2026-04-18',
                'tag' => __('Update', 'techdex-client-post-preview'),
                'items' => [
                    __('Completed the Free and Pro tier foundations, including plan-based feature unlocking in the plugin dashboard.', 'techdex-client-post-preview'),
                    __('Added TechDex license activation, per-site activation tracking, and site-count enforcement for Free, Pro, and Agency.', 'techdex-client-post-preview'),
                    __('Added the in-plugin License tab with save, activate, refresh, and deactivate flows plus live license-state summaries.', 'techdex-client-post-preview'),
                    __('Introduced preview slots and per-post preview links, with slot counting staying post-based instead of link-based.', 'techdex-client-post-preview'),
                    __('Added multiple preview links per post by plan: Free gets 1, Pro gets 2, and Agency gets 3.', 'techdex-client-post-preview'),
                    __('Added per-link expiration management from the Preview Links dashboard while keeping the editor focused on link creation and quick opening.', 'techdex-client-post-preview'),
                    __('Added open-preview controls in both the editor and management screens to support WordPress preview bypass workflows.', 'techdex-client-post-preview'),
                    __('Moved advanced link lifecycle workflows into the Agency tier so Renew, Regenerate, Never Expire, and Auto-Revoke stay as higher-end controls.', 'techdex-client-post-preview'),
                    __('Added the TechDex product site pages, thank-you flow, manual license registration flow, and internal licensing admin screens for this product.', 'techdex-client-post-preview'),
                    __('Added purchase-package validation, single-use session registration handling, and manual license creation support in the TechDex licensing app.', 'techdex-client-post-preview'),
                    __('Updated the product messaging to position the plugin as both a client-sharing tool and a WordPress preview-bypass workflow for authors and editors.', 'techdex-client-post-preview'),
                ],
            ],
            [
                'version' => '0.1.0',
                'date' => '2026-04-17',
                'tag' => __('Initial Release', 'techdex-client-post-preview'),
                'items' => [
                    __('Introduced secure client preview links for unpublished WordPress content.', 'techdex-client-post-preview'),
                    __('Added logged-out preview access with token validation and expiration handling.', 'techdex-client-post-preview'),
                    __('Added per-post preview controls in the editor sidebar.', 'techdex-client-post-preview'),
                    __('Added expiration status indicators in both the editor and the plugin admin area.', 'techdex-client-post-preview'),
                    __('Added dashboard tabs for Dashboard, Preview Links, Getting Started, Readme, Changelog, and Support.', 'techdex-client-post-preview'),
                    __('Added support workflow, tier messaging, and creator profile panel.', 'techdex-client-post-preview'),
                    __('Added encrypted-at-rest storage for reusable preview tokens while preserving the existing sharing workflow.', 'techdex-client-post-preview'),
                    __('Updated preview slot behavior so saved links continue consuming a slot until they are revoked, even after publish or expiration.', 'techdex-client-post-preview'),
                    __('Added published-post handling so preview requests redirect to the public permalink while live and resume when moved back to an unpublished status.', 'techdex-client-post-preview'),
                    __('Added bundled plugin branding assets and a branded admin header icon across dashboard screens.', 'techdex-client-post-preview'),
                    __('Refined free, Pro, and Agency plan messaging, including the Pro cap of 12 preview-enabled posts and unlimited Agency positioning.', 'techdex-client-post-preview'),
                    __('Improved admin polish with clearer notice severity styling and safer preview-links status fallbacks.', 'techdex-client-post-preview'),
                    __('Added plugin-side license activation, status refresh, and deactivation against the TechDex licensing service.', 'techdex-client-post-preview'),
                ],
            ],
        ];
    }

    private static function is_plugin_admin_screen(string $screen_id): bool {
        $page_slugs = [
            self::DASHBOARD_PAGE_SLUG,
            self::PREVIEW_LINKS_PAGE_SLUG,
            self::GETTING_STARTED_PAGE_SLUG,
            self::README_PAGE_SLUG,
            self::LICENSE_PAGE_SLUG,
            self::SUPPORT_PAGE_SLUG,
            self::DETAILS_PAGE_SLUG,
            self::LINK_DETAILS_PAGE_SLUG,
            self::CHANGELOG_PAGE_SLUG,
        ];

        foreach ($page_slugs as $page_slug) {
            $screen_ids = [
                'toplevel_page_' . $page_slug,
                'techdex-client-post-preview_page_' . $page_slug,
                'admin_page_' . $page_slug,
            ];

            if (in_array($screen_id, $screen_ids, true)) {
                return true;
            }
        }

        return false;
    }

    private static function handle_license_action(string $operation): void {
        if (!current_user_can('manage_options')) {
            wp_die(esc_html__('You do not have permission to manage licenses for this plugin.', 'techdex-client-post-preview'));
        }

        $nonce_action = match ($operation) {
            'activate' => 'cpp_activate_license',
            'check' => 'cpp_refresh_license',
            'deactivate' => 'cpp_deactivate_license',
            default => '',
        };

        if ('' === $nonce_action) {
            wp_die(esc_html__('Invalid license action.', 'techdex-client-post-preview'));
        }

        check_admin_referer($nonce_action);

        $license_key = self::get_saved_license_key();

        if ('' === $license_key) {
            wp_safe_redirect(self::get_license_page_url([self::NOTICE_QUERY_ARG => 'license_missing_key']));
            exit;
        }

        $result = self::request_license_service($operation, $license_key);

        if (is_wp_error($result)) {
            $state = self::get_license_state();
            $state['message'] = $result->get_error_message();
            $state['last_checked_at'] = current_time('mysql');
            $state['is_valid'] = false;
            update_option(self::OPTION_LICENSE_STATE, $state, false);

            wp_safe_redirect(self::get_license_page_url([self::NOTICE_QUERY_ARG => 'license_request_failed']));
            exit;
        }

        $notice = self::store_license_service_response($operation, $result);
        wp_safe_redirect(self::get_license_page_url([self::NOTICE_QUERY_ARG => $notice]));
        exit;
    }

    private static function request_license_service(string $operation, string $license_key) {
        $service_url = self::get_license_service_url();

        if ('' === $service_url) {
            return new WP_Error('cpp_license_service_missing', __('The TechDex licensing service URL is not configured.', 'techdex-client-post-preview'));
        }

        $response = wp_remote_post(
            $service_url,
            [
                'timeout' => 20,
                'body' => [
                    'action' => $operation,
                    'product_key' => self::PRODUCT_KEY,
                    'license_key' => $license_key,
                    'site_url' => site_url('/'),
                    'home_url' => home_url('/'),
                    'domain_name' => self::get_site_domain(),
                    'instance_hash' => self::get_license_instance_hash(),
                    'plugin_version' => self::VERSION,
                    'wp_version' => get_bloginfo('version'),
                    'php_version' => PHP_VERSION,
                ],
                'user-agent' => 'TechDex Client Post Preview/' . self::VERSION . '; ' . home_url('/'),
            ]
        );

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

        $body = json_decode((string) wp_remote_retrieve_body($response), true);

        if (!is_array($body)) {
            return new WP_Error('cpp_license_service_invalid_response', __('The TechDex licensing service returned an unreadable response.', 'techdex-client-post-preview'));
        }

        $response_code = (int) wp_remote_retrieve_response_code($response);

        if ($response_code >= 500) {
            return new WP_Error('cpp_license_service_unavailable', __('The TechDex licensing service is unavailable right now.', 'techdex-client-post-preview'));
        }

        return $body;
    }

    private static function store_license_service_response(string $operation, array $response): string {
        $state = self::get_default_license_state();
        $state['message'] = isset($response['message']) ? sanitize_text_field((string) $response['message']) : '';
        $state['service_code'] = isset($response['code']) ? sanitize_key((string) $response['code']) : '';
        $state['last_checked_at'] = current_time('mysql');
        $state['instance_hash'] = self::get_license_instance_hash();
        $state['site_url'] = home_url('/');
        $state['domain_name'] = self::get_site_domain();

        if (isset($response['license']) && is_array($response['license'])) {
            $license = $response['license'];
            $state['tier_key'] = sanitize_key((string) ($license['tier_key'] ?? 'free'));
            $state['license_status'] = sanitize_key((string) ($license['status'] ?? 'inactive'));
            $state['registered_name'] = sanitize_text_field((string) ($license['registered_name'] ?? ''));
            $state['license_email'] = sanitize_email((string) ($license['license_email'] ?? ''));
            $state['expires_at'] = sanitize_text_field((string) ($license['expires_at'] ?? ''));
            $state['activation_status'] = sanitize_key((string) ($license['activation_status'] ?? 'inactive'));
            $state['active_sites'] = absint($license['active_sites'] ?? 0);
            $state['activation_limit'] = absint($license['activation_limit'] ?? 0);
        }

        $state['is_valid'] = !empty($response['success'])
            && in_array($state['tier_key'], ['pro', 'agency'], true)
            && 'active' === $state['license_status']
            && 'active' === $state['activation_status'];

        if ('deactivate' === $operation && !empty($response['success'])) {
            $state['is_valid'] = false;
            $state['activation_status'] = 'inactive';
        }

        update_option(self::OPTION_LICENSE_STATE, $state, false);

        return match ($state['service_code']) {
            'activated' => 'license_activated',
            'checked' => 'license_checked',
            'deactivated' => 'license_deactivated',
            'activation_limit_reached' => 'license_activation_limit_reached',
            'license_expired' => 'license_expired',
            'license_revoked' => 'license_revoked',
            'license_disabled' => 'license_disabled',
            'invalid_license' => 'license_invalid',
            default => !empty($response['success']) ? 'license_checked' : 'license_request_failed',
        };
    }

    public static function filter_plugin_updates($transient) {
        if (!is_object($transient) || empty($transient->checked) || !is_array($transient->checked)) {
            return $transient;
        }

        $metadata = self::get_update_metadata();

        if (!$metadata) {
            return $transient;
        }

        $plugin_file = plugin_basename(__FILE__);
        $installed_version = isset($transient->checked[$plugin_file]) ? (string) $transient->checked[$plugin_file] : self::VERSION;
        $update_item = (object) [
            'slug' => self::PRODUCT_KEY,
            'plugin' => $plugin_file,
            'new_version' => (string) ($metadata['version'] ?? self::VERSION),
            'url' => (string) ($metadata['homepage'] ?? ''),
            'package' => (string) ($metadata['download_url'] ?? ''),
            'requires' => (string) ($metadata['requires'] ?? ''),
            'requires_php' => (string) ($metadata['requires_php'] ?? ''),
            'tested' => (string) ($metadata['tested'] ?? ''),
            'icons' => is_array($metadata['icons'] ?? null) ? $metadata['icons'] : [],
            'banners' => is_array($metadata['banners'] ?? null) ? $metadata['banners'] : [],
        ];

        if (version_compare((string) ($metadata['version'] ?? self::VERSION), $installed_version, '>')) {
            $transient->response[$plugin_file] = $update_item;
            unset($transient->no_update[$plugin_file]);
        } else {
            $transient->no_update[$plugin_file] = $update_item;
            unset($transient->response[$plugin_file]);
        }

        return $transient;
    }

    public static function filter_plugin_information($result, string $action, $args) {
        if ('plugin_information' !== $action || !is_object($args) || empty($args->slug) || self::PRODUCT_KEY !== (string) $args->slug) {
            return $result;
        }

        $metadata = self::get_update_metadata(true);

        if (!$metadata) {
            $cached = get_site_transient(self::UPDATE_CACHE_KEY);
            if (is_array($cached) && !empty($cached['version'])) {
                $metadata = $cached;
            }
        }

        if (!$metadata) {
            $metadata = self::get_local_plugin_information_metadata();
        }

        return (object) [
            'name' => (string) ($metadata['name'] ?? __('TechDex Client Post Preview', 'techdex-client-post-preview')),
            'slug' => self::PRODUCT_KEY,
            'version' => (string) ($metadata['version'] ?? self::VERSION),
            'author' => '<a href="https://techdex.net">Dexter Nelson | TechDex Development and Solutions</a>',
            'author_profile' => 'https://techdex.net',
            'homepage' => (string) ($metadata['homepage'] ?? ''),
            'requires' => (string) ($metadata['requires'] ?? ''),
            'requires_php' => (string) ($metadata['requires_php'] ?? ''),
            'tested' => (string) ($metadata['tested'] ?? ''),
            'last_updated' => (string) ($metadata['last_updated'] ?? ''),
            'download_link' => (string) ($metadata['download_url'] ?? ''),
            'sections' => is_array($metadata['sections'] ?? null) ? $metadata['sections'] : [],
            'banners' => is_array($metadata['banners'] ?? null) ? $metadata['banners'] : [],
            'icons' => is_array($metadata['icons'] ?? null) ? $metadata['icons'] : [],
        ];
    }

    private static function get_local_plugin_information_metadata(): array {
        return [
            'name' => __('TechDex Client Post Preview', 'techdex-client-post-preview'),
            'version' => self::VERSION,
            'homepage' => 'https://techdex.net/software/techdex-client-post-preview-plugin/',
            'requires' => '6.5',
            'requires_php' => '7.4',
            'tested' => '6.9',
            'last_updated' => gmdate('Y-m-d H:i:s'),
            'download_url' => 'https://techdex.net/dl/techdex-client-post-preview.zip',
            'sections' => [
                'description' => __('Preview unpublished WordPress content as if it were live, then share that same view with clients or teammates using secure preview links.', 'techdex-client-post-preview'),
                'installation' => implode("\n", [
                    __('1. Upload the techdex-client-post-preview folder to /wp-content/plugins/ or install the ZIP from the WordPress plugin uploader.', 'techdex-client-post-preview'),
                    __('2. Activate TechDex Client Post Preview in WordPress.', 'techdex-client-post-preview'),
                    __('3. Open any post, page, or supported custom post type and generate a preview link from the editor sidebar.', 'techdex-client-post-preview'),
                    __('4. Manage links, licensing, and settings from the TechDex Client Post Preview dashboard.', 'techdex-client-post-preview'),
                ]),
                'changelog' => implode("\n", [
                    '= ' . self::VERSION . ' =',
                    __('* Local plugin details fallback used when the remote release feed is temporarily unavailable.', 'techdex-client-post-preview'),
                    __('* Includes plugin description, install steps, and direct package download details for the WordPress details modal.', 'techdex-client-post-preview'),
                ]),
            ],
            'icons' => [
                '1x' => 'https://techdex.net/software/techdex-client-post-preview-plugin/techdex-client-post-preview-images/icon-128x128.png',
                '2x' => 'https://techdex.net/software/techdex-client-post-preview-plugin/techdex-client-post-preview-images/icon-256x256.png',
            ],
            'banners' => [
                'low' => 'https://techdex.net/software/techdex-client-post-preview-plugin/techdex-client-post-preview-images/banner-772x250.png',
                'high' => 'https://techdex.net/software/techdex-client-post-preview-plugin/techdex-client-post-preview-images/banner-1544x500.png',
            ],
        ];
    }

    private static function get_update_metadata(bool $force = false): ?array {
        if (!$force) {
            $cached = get_site_transient(self::UPDATE_CACHE_KEY);
            if (is_array($cached)) {
                return $cached;
            }
        }

        $service_url = self::get_update_service_url();

        if ('' === $service_url) {
            return null;
        }

        $response = wp_remote_get(
            add_query_arg(
                [
                    'product_key' => self::PRODUCT_KEY,
                    'installed_version' => self::VERSION,
                    'site_url' => home_url('/'),
                ],
                $service_url
            ),
            [
                'timeout' => 15,
                'user-agent' => 'TechDex Client Post Preview/' . self::VERSION . '; ' . home_url('/'),
            ]
        );

        if (is_wp_error($response) || (int) wp_remote_retrieve_response_code($response) >= 400) {
            return null;
        }

        $metadata = json_decode((string) wp_remote_retrieve_body($response), true);

        if (!is_array($metadata) || empty($metadata['version'])) {
            return null;
        }

        set_site_transient(self::UPDATE_CACHE_KEY, $metadata, 6 * HOUR_IN_SECONDS);

        return $metadata;
    }

    private static function is_pro_active(): bool {
        $state = self::get_license_state();
        $paid_plan_active = !empty($state['is_valid']) && in_array($state['tier_key'], ['pro', 'agency'], true);

        $paid_plan_active = (bool) apply_filters('techdex_client_post_preview_is_pro', $paid_plan_active, $state);

        // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Backward-compatibility filter for older integrations.
        return (bool) apply_filters('client_post_preview_is_pro', $paid_plan_active, $state);
    }

    private static function is_agency_active(): bool {
        $state = self::get_license_state();

        return !empty($state['is_valid']) && 'agency' === (string) $state['tier_key'];
    }

    private static function get_current_plan_label(): string {
        $state = self::get_license_state();

        if (!empty($state['is_valid'])) {
            if ('agency' === $state['tier_key']) {
                return __('Agency', 'techdex-client-post-preview');
            }

            return __('Pro', 'techdex-client-post-preview');
        }

        return __('Free', 'techdex-client-post-preview');
    }

    private static function get_current_plan_key(): string {
        $state = self::get_license_state();

        if (!empty($state['is_valid']) && in_array($state['tier_key'], ['pro', 'agency'], true)) {
            return $state['tier_key'];
        }

        return 'free';
    }

    private static function get_license_state(): array {
        $saved = get_option(self::OPTION_LICENSE_STATE, []);
        $state = is_array($saved) ? array_merge(self::get_default_license_state(), $saved) : self::get_default_license_state();

        $state['is_valid'] = !empty($state['is_valid']);
        $state['active_sites'] = absint($state['active_sites']);
        $state['activation_limit'] = absint($state['activation_limit']);

        return $state;
    }

    private static function get_default_license_state(): array {
        return [
            'is_valid' => false,
            'tier_key' => 'free',
            'license_status' => 'inactive',
            'activation_status' => 'inactive',
            'registered_name' => '',
            'license_email' => '',
            'expires_at' => '',
            'last_checked_at' => '',
            'active_sites' => 0,
            'activation_limit' => 0,
            'message' => '',
            'service_code' => '',
            'instance_hash' => self::get_license_instance_hash(),
            'site_url' => home_url('/'),
            'domain_name' => self::get_site_domain(),
        ];
    }

    private static function get_license_service_url(): string {
        $default_url = 'https://techdex.net/software/techdex-client-post-preview-plugin/pulse.php';

        $default_url = (string) apply_filters('techdex_client_post_preview_license_service_url', $default_url);

        // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Backward-compatibility filter for older integrations.
        return esc_url_raw((string) apply_filters('client_post_preview_license_service_url', $default_url));
    }

    private static function get_update_service_url(): string {
        $service_url = (string) apply_filters('techdex_client_post_preview_update_service_url', self::UPDATE_SERVICE_URL);
        // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Backward-compatibility filter for older integrations.
        return esc_url_raw((string) apply_filters('cpp_update_service_url', $service_url));
    }

    private static function get_license_instance_hash(): string {
        return hash('sha256', home_url('/') . '|' . site_url('/') . '|' . self::PRODUCT_KEY);
    }

    private static function get_site_domain(): string {
        $host = wp_parse_url(home_url('/'), PHP_URL_HOST);

        return is_string($host) ? $host : '';
    }

    private static function format_license_datetime(string $datetime): string {
        $timestamp = strtotime($datetime);

        if (false === $timestamp) {
            return $datetime;
        }

        return wp_date(get_option('date_format') . ' ' . get_option('time_format'), $timestamp);
    }

    private static function get_saved_license_key(): string {
        return strtoupper(trim((string) get_option(self::OPTION_LICENSE_KEY, '')));
    }

}

TechDex_Client_Post_Preview_Plugin::init();

