<?php
namespace EncypherProvenance;

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

require_once __DIR__ . '/class-encypher-logger.php';
require_once __DIR__ . '/class-encypher-signing-status.php';
require_once __DIR__ . '/class-encypher-environment-guard.php';
require_once __DIR__ . '/class-encypher-capabilities.php';
if (! class_exists(ContentStatus::class)) {
    $content_status_file = __DIR__ . '/class-encypher-content-status.php';
    if (is_readable($content_status_file)) {
        require_once $content_status_file;
    }
}

/**
 * Handles admin settings and editor integrations.
 */
class Admin
{

    /**
     * Tier names recognized for account display and quota messaging. Commercial
     * authorization comes only from Capabilities.
     */
    public const KNOWN_TIERS = ['free', 'enterprise', 'strategic_partner', 'demo'];

    /** Whether Encypher's current server projection grants reader whitelabeling. */
    public static function allows_whitelabel(): bool
    {
        return Capabilities::allows(Capabilities::WHITE_LABEL);
    }

    /**
     * Option holding the unknown-tier admin notice state.
     */
    private const UNKNOWN_TIER_OPTION = 'encypher_unknown_tier_notice';
    private const LOCAL_COVERAGE_CACHE_KEY = 'encypher_provenance_local_coverage_v2';
    private const LOCAL_COVERAGE_CACHE_TTL = 120;

    /**
     * Per-user flag: the AI-crawler CDN analytics CTA on the coverage card was
     * dismissed (the small x). Stored as user meta so a dismissal is personal.
     */
    private const CDN_CTA_DISMISSED_META = '_encypher_cdn_cta_dismissed';


    public function register_hooks(): void
    {
        add_action('admin_menu', [$this, 'register_admin_menu']);
        add_action('admin_init', [$this, 'register_settings']);
        add_action('admin_init', [$this, 'add_privacy_policy_content']);
        add_action('admin_enqueue_scripts', [$this, 'enqueue_settings_page_assets']);
        add_action('admin_head', [$this, 'output_admin_menu_icon_css']);
        add_action('enqueue_block_editor_assets', [$this, 'enqueue_block_editor_assets']);
        add_action('add_meta_boxes', [$this, 'register_classic_meta_box'], 10, 2);
        add_action('admin_enqueue_scripts', [$this, 'enqueue_classic_assets']);
        add_action('post_submitbox_misc_actions', [$this, 'render_emergency_publish_control']);
        add_action('admin_enqueue_scripts', [$this, 'enqueue_deactivation_survey_assets']);
        add_action('admin_notices', [$this, 'render_unknown_tier_notice']);
        add_action('wp_ajax_encypher_dismiss_unknown_tier', [$this, 'ajax_dismiss_unknown_tier']);
        add_action('wp_ajax_encypher_dismiss_cdn_cta', [$this, 'ajax_dismiss_cdn_cta']);
        add_action('admin_post_encypher_cancel_media_scan', [$this, 'handle_cancel_media_scan']);
        add_action('admin_post_encypher_retry_registry', [$this, 'handle_retry_registry']);
        add_action('admin_notices', [$this, 'render_registry_retry_notice']);
        // Native content-list provenance integration (Posts/Pages): a single
        // Provenance column, a status filter, meaningful row actions, and a
        // "Sign with Encypher" bulk action backed by the existing deferred-sign
        // queue. Replaces the removed custom Content and Bulk Sign pages. The
        // Media Library provenance UI is owned solely by MediaLibraryUi.
        add_filter('manage_posts_columns', [$this, 'add_content_provenance_column']);
        add_filter('manage_pages_columns', [$this, 'add_content_provenance_column']);
        add_action('manage_posts_custom_column', [$this, 'render_content_provenance_column'], 10, 2);
        add_action('manage_pages_custom_column', [$this, 'render_content_provenance_column'], 10, 2);
        add_action('restrict_manage_posts', [$this, 'render_provenance_list_filter']);
        add_action('pre_get_posts', [$this, 'filter_list_by_provenance']);
        add_filter('post_row_actions', [$this, 'add_provenance_row_action'], 10, 2);
        add_filter('page_row_actions', [$this, 'add_provenance_row_action'], 10, 2);
        add_action('admin_post_encypher_sign_single', [$this, 'handle_single_sign_action']);
        add_action('admin_notices', [$this, 'render_bulk_sign_notice']);
        add_action('added_post_meta', [$this, 'invalidate_local_coverage_for_meta'], 10, 4);
        add_action('updated_post_meta', [$this, 'invalidate_local_coverage_for_meta'], 10, 4);
        add_action('deleted_post_meta', [$this, 'invalidate_local_coverage_for_meta'], 10, 4);
        add_action('save_post', [$this, 'clear_local_coverage_cache']);
        add_action('trashed_post', [$this, 'clear_local_coverage_cache']);
        add_action('untrashed_post', [$this, 'clear_local_coverage_cache']);
        add_action('before_delete_post', [$this, 'clear_local_coverage_cache']);
        add_action('update_option_encypher_provenance_settings', [$this, 'clear_local_coverage_cache']);
        // "Sign with Encypher" native bulk action, backed by the existing
        // deferred-sign queue. WordPress fires the hyphenated
        // bulk_actions-edit-{type} / handle_bulk_actions-edit-{type} hooks.
        $settings = get_option('encypher_provenance_settings', []);
        $configured_types = isset($settings['post_types']) && is_array($settings['post_types'])
            ? $settings['post_types']
            : ['post', 'page'];
        foreach (array_unique(array_map('sanitize_key', $configured_types)) as $post_type) {
            if ('' === $post_type) {
                continue;
            }
            if (! in_array($post_type, ['post', 'page'], true)) {
                add_filter("manage_{$post_type}_posts_columns", [$this, 'add_content_provenance_column']);
                add_action("manage_{$post_type}_posts_custom_column", [$this, 'render_content_provenance_column'], 10, 2);
            }
            add_filter("bulk_actions-edit-{$post_type}", [$this, 'add_bulk_sign_action']);
            add_filter("handle_bulk_actions-edit-{$post_type}", [$this, 'handle_bulk_sign_action'], 10, 3);
        }
    }
    public function render_emergency_publish_control(): void
    {
        if (! current_user_can('manage_options') || ! current_user_can('publish_posts')) {
            return;
        }
        $settings = get_option('encypher_provenance_settings', []);
        if (! is_array($settings) || empty($settings['protected_publish_enabled'])) {
            return;
        }
        $post_id = get_the_ID();
        ?>
        <div class="misc-pub-section">
            <?php wp_nonce_field('encypher_emergency_publish_' . $post_id, 'encypher_emergency_nonce'); ?>
            <label>
                <input type="checkbox" name="encypher_emergency_unsigned" value="1" />
                <?php esc_html_e('Emergency: publish unsigned', 'encypher-provenance'); ?>
            </label>
            <input class="widefat" type="text" name="encypher_emergency_reason" placeholder="<?php esc_attr_e('Required audit reason', 'encypher-provenance'); ?>" />
            <p class="description"><?php esc_html_e('Admin-only bypass. The unsigned release and reason are written to the provenance audit trail.', 'encypher-provenance'); ?></p>
        </div>
        <?php
    }


    /**
     * Normalize the API-reported tier used for display and quota messaging.
     *
     * Known tiers pass through, with the demo label using Enterprise display
     * treatment. An unknown tier keeps the last-known display value and reports
     * unknown => true so the caller can surface the admin notice. Pure (no WP
     * I/O) for standalone testability.
     *
     * @param mixed  $raw_tier        Tier value from the API response.
     * @param string $last_known_good Previously stored effective tier.
     *
     * @return array{tier:string,unknown:bool,raw:string}
     */
    public static function resolve_account_tier($raw_tier, string $last_known_good): array
    {
        $raw = is_string($raw_tier) && '' !== trim($raw_tier) ? trim($raw_tier) : 'free';

        if (in_array($raw, self::KNOWN_TIERS, true)) {
            return [
                'tier' => 'demo' === $raw ? 'enterprise' : $raw,
                'unknown' => false,
                'raw' => $raw,
            ];
        }

        $fallback = in_array($last_known_good, self::KNOWN_TIERS, true) ? $last_known_good : 'free';
        if ('demo' === $fallback) {
            $fallback = 'enterprise';
        }

        return ['tier' => $fallback, 'unknown' => true, 'raw' => $raw];
    }

    /**
     * Resolve the effective tier from an API-reported tier name and maintain
     * the unknown-tier admin notice flag: unknown tiers keep the last stored
     * tier and flag a dismissible notice; known tiers clear it. Dismissal is
     * preserved while the API keeps reporting the same unknown name; a
     * different unknown name re-surfaces the notice.
     *
     * @param mixed $raw_tier Tier value from the API response.
     */
    public static function apply_account_tier($raw_tier): string
    {
        $settings = get_option('encypher_provenance_settings', []);
        $last_known = is_array($settings) && isset($settings['tier']) ? (string) $settings['tier'] : 'free';

        $resolved = self::resolve_account_tier($raw_tier, $last_known);

        if ($resolved['unknown']) {
            $existing = get_option(self::UNKNOWN_TIER_OPTION, []);
            $dismissed = is_array($existing)
                && isset($existing['tier']) && $existing['tier'] === $resolved['raw']
                && ! empty($existing['dismissed']);
            update_option(self::UNKNOWN_TIER_OPTION, [
                'tier' => sanitize_text_field($resolved['raw']),
                'dismissed' => $dismissed,
            ], false);
        } else {
            delete_option(self::UNKNOWN_TIER_OPTION);
        }

        return $resolved['tier'];
    }

    /**
     * Render a dismissible warning when the API reported a tier this plugin
     * version does not recognize (C3: never a silent downgrade to free).
     */
    public function render_unknown_tier_notice(): void
    {
        if (! current_user_can('manage_options')) {
            return;
        }

        $state = get_option(self::UNKNOWN_TIER_OPTION, []);
        if (! is_array($state) || empty($state['tier']) || ! empty($state['dismissed'])) {
            return;
        }

        $nonce = wp_create_nonce('encypher_dismiss_unknown_tier');
        ?>
        <div class="notice notice-warning encypher-unknown-tier-notice" style="position:relative;padding-right:48px;">
            <p>
                <?php
                printf(
                    /* translators: %s: unrecognized tier name reported by the API */
                    esc_html__('Encypher: your account tier %s is not recognized by this plugin version; features may be limited. Please update the plugin.', 'encypher-provenance'),
                    '<strong>' . esc_html((string) $state['tier']) . '</strong>'
                );
                ?>
            </p>
            <button
                type="button"
                class="notice-dismiss"
                onclick="jQuery.post(ajaxurl, {action:'encypher_dismiss_unknown_tier', _wpnonce:'<?php echo esc_js($nonce); ?>'}); jQuery(this).closest('.encypher-unknown-tier-notice').remove();"
                style="position:absolute;top:8px;right:8px;">
                <span class="screen-reader-text"><?php esc_html_e('Dismiss this notice', 'encypher-provenance'); ?></span>
            </button>
        </div>
        <?php
    }

    /**
     * Persist dismissal of the unknown-tier notice for the currently
     * reported tier name (a different unknown tier re-surfaces the notice).
     */
    public function ajax_dismiss_unknown_tier(): void
    {
        if (! current_user_can('manage_options')) {
            wp_die();
        }
        check_ajax_referer('encypher_dismiss_unknown_tier');
        $state = get_option(self::UNKNOWN_TIER_OPTION, []);
        if (is_array($state) && ! empty($state['tier'])) {
            $state['dismissed'] = true;
            update_option(self::UNKNOWN_TIER_OPTION, $state, false);
        }
        wp_send_json_success();
    }

    /**
     * Persist the current user's dismissal of the AI-crawler CDN analytics CTA
     * on the connected coverage card.
     */
    public function ajax_dismiss_cdn_cta(): void
    {
        if (! current_user_can('manage_options')) {
            wp_die();
        }
        check_ajax_referer('encypher_dismiss_cdn_cta');
        if (function_exists('update_user_meta') && function_exists('get_current_user_id')) {
            update_user_meta(get_current_user_id(), self::CDN_CTA_DISMISSED_META, 1);
        }
        wp_send_json_success();
    }

    /**
     * Cancel an active media scan and return to Setup & Health.
     */
    public function handle_cancel_media_scan(): void
    {
        if (! current_user_can('manage_options')) {
            wp_die(esc_html__('You are not allowed to manage media scans.', 'encypher-provenance'));
        }
        check_admin_referer('encypher_cancel_media_scan');
        if (class_exists(MediaScan::class)) {
            MediaScan::cancel();
        }
        wp_safe_redirect(add_query_arg('encypher_scan_cancelled', '1', admin_url('admin.php?page=encypher')));
        exit;
    }

    /**
     * Retry a publisher-registry delivery through its canonical coordinator.
     */
    public function handle_retry_registry(): void
    {
        $post_id = isset($_POST['post_id']) ? absint(wp_unslash($_POST['post_id'])) : 0;
        if ($post_id < 1 || ! current_user_can('edit_post', $post_id)) {
            wp_die(esc_html__('You are not allowed to retry this registry delivery.', 'encypher-provenance'));
        }
        check_admin_referer('encypher_retry_registry_' . $post_id);

        $result = class_exists(PublisherPublication::class)
            ? PublisherPublication::retry($post_id, (int) get_current_user_id())
            : new \WP_Error('registry_unavailable', __('Registry delivery is unavailable.', 'encypher-provenance'));
        $status = is_wp_error($result) ? 'failed' : 'queued';
        $redirect = wp_get_referer();
        if (! is_string($redirect) || '' === $redirect) {
            $redirect = admin_url('edit.php');
        }
        wp_safe_redirect(add_query_arg('encypher_registry_retry', $status, $redirect));
        exit;
    }

    /**
     * Show bounded operator feedback after a registry retry action.
     */
    public function render_registry_retry_notice(): void
    {
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only redirect outcome written by handle_retry_registry().
        $status = isset($_GET['encypher_registry_retry']) ? sanitize_key(wp_unslash($_GET['encypher_registry_retry'])) : '';
        if (! in_array($status, ['queued', 'failed'], true)) {
            return;
        }
        $class = 'queued' === $status ? 'notice notice-success is-dismissible' : 'notice notice-error is-dismissible';
        $message = 'queued' === $status
            ? __('Registry delivery retry queued.', 'encypher-provenance')
            : __('Registry delivery could not be retried. Review its delivery details and try again.', 'encypher-provenance');
        echo '<div class="' . esc_attr($class) . '"><p>' . esc_html($message) . '</p></div>';
    }

    public function output_admin_menu_icon_css(): void
    {
        echo '<style>';
        echo '#adminmenu #toplevel_page_encypher .wp-menu-image img{width:20px;height:20px;max-width:20px;max-height:20px;padding:7px 0;box-sizing:content-box;filter:brightness(0) invert(.65);}';
        echo '#adminmenu #toplevel_page_encypher:hover .wp-menu-image img{filter:brightness(0) invert(1);}';
        echo '#adminmenu .wp-has-current-submenu#toplevel_page_encypher .wp-menu-image img{filter:brightness(0) invert(1);}';
        echo '</style>';
    }

    /**
     * Register the sole WordPress-native Encypher cockpit.
     */
    public function register_admin_menu(): void
    {
        $icon_svg = ENCYPHER_PROVENANCE_PLUGIN_URL . 'assets/images/brand/encypher-mark-navy.svg';

        add_menu_page(
            __('Encypher Provenance', 'encypher-provenance'),
            __('Encypher', 'encypher-provenance'),
            'manage_options',
            'encypher',
            [$this, 'render_settings_page'],
            $icon_svg,
            30
        );
    }

    public function register_settings(): void
    {
        register_setting('encypher_provenance_settings_group', 'encypher_provenance_settings', [
            'type' => 'array',
            'sanitize_callback' => [$this, 'sanitize_settings'],
            'default' => [
                'api_base_url' => 'https://api.encypher.com/api/v1',
                'api_key' => '',
                'connect_email' => '',
                'connect_session_id' => '',
                'auto_verify' => true,
                'connection_last_status' => 'unknown',
                'connection_last_checked_at' => '',
                'auto_mark_on_publish' => true,
                'auto_mark_on_update' => true,
                'protected_publish_enabled' => true,
                'allow_non_production_signing' => false,
                'protected_publish_roles' => [],
                'protected_publish_terms' => [],
                'tier' => 'free',
                'signing_mode' => 'managed',
                'signing_profile_id' => '',
                'organization_id' => '',
                'organization_name' => '',
                'usage' => [
                    'api_calls' => [
                        'used' => 0,
                        'limit' => 100000,
                        'remaining' => 100000,
                        'percentage_used' => 0,
                        'is_unlimited' => false,
                        'reset_date' => '',
                    ],
                ],
                'post_types' => ['post', 'page'],
                'manifest_mode' => 'micro',
                'ecc' => true,
                'embed_c2pa' => true,
                'legacy_safe' => false,
                'local_hashing' => true,
                'allow_full_asset_upload' => true,
                'auto_sign_images' => true,
                'media_badges' => true,
                'distribution_target' => '',
                'index_for_attribution' => true,
                'expires_at' => '',
                'license_type' => '',
                'license_url' => '',
                'error_webhook_url' => '',
                'error_webhook_threshold' => 3,
            ],
        ]);
        // PRD Surface-1: no Settings API sections/fields are registered and
        // do_settings_sections() is not used. The connected Setup & Health page
        // hand-renders only the seven contracted WordPress-local behavior
        // settings; every organization-policy control (tier, coalition,
        // alerting/webhook, signing mode/BYOK, rights defaults, enterprise
        // features, analytics/pricing) lives in the Encypher dashboard.
        // sanitize_settings() preserves stored values for keys the form omits.
    }

    /**
     * Normalizes an API base URL to always end with /api/v1 (no trailing slash).
     * Accepts both https://api.encypher.com/ and https://api.encypher.com/api/v1.
     */
    private static function normalize_api_base_url(string $url): string
    {
        $url = rtrim(trim($url), '/');
        if ('' === $url) {
            return '';
        }
        if (! str_ends_with($url, '/api/v1')) {
            $url .= '/api/v1';
        }
        return $url;
    }

    public function sanitize_settings(array $settings): array
    {
        $current = get_option('encypher_provenance_settings', []);
        if (! is_array($current)) {
            $current = [];
        }

        // Start with the stored option so controls moved to the Encypher
        // dashboard, install/claim state, and keys introduced by newer plugin
        // versions survive every reduced cockpit save unchanged.
        $sanitized = $current;
        $api_base_url_changed = false;
        $api_key_changed = false;
        $settings_valid = true;

        $is_behavior_save = ! empty($settings['_cockpit_behavior_save'])
            || array_key_exists('post_types', $settings)
            || array_key_exists('badge_position', $settings)
            || array_key_exists('api_base_url', $settings)
            || array_key_exists('api_key', $settings);

        if (array_key_exists('api_base_url', $settings)) {
            $previous_api_base_url = self::normalize_api_base_url(
                (string) ($current['api_base_url'] ?? 'https://api.encypher.com/api/v1')
            );
            $api_base_url = self::normalize_api_base_url(
                esc_url_raw(trim((string) $settings['api_base_url']))
            );
            if ('' === $api_base_url) {
                $api_base_url = 'https://api.encypher.com/api/v1';
            }

            if (ApiUrlGuard::is_safe($api_base_url)) {
                $sanitized['api_base_url'] = $api_base_url;
                $api_base_url_changed = $api_base_url !== $previous_api_base_url;
            } else {
                add_settings_error(
                    'encypher_provenance_settings',
                    'api_base_url_unsafe',
                    __('API Base URL rejected: it must use https:// and cannot point at localhost or a private/internal network address. Reverted to the previous value.', 'encypher-provenance'),
                    'error'
                );
                $settings_valid = false;
                $sanitized['api_base_url'] = isset($current['api_base_url'])
                    ? (string) $current['api_base_url']
                    : 'https://api.encypher.com/api/v1';
            }
        }

        // The credential input is deliberately blank on every render. A blank
        // value keeps the stored key only while the API origin stays unchanged.
        // If a caller changes api_base_url without re-entering the key, clear
        // the hidden credential so a settings-only user cannot redirect the
        // stored bearer token to a new endpoint.
        $submitted_api_key = array_key_exists('api_key', $settings)
            ? sanitize_text_field((string) $settings['api_key'])
            : null;
        if (is_string($submitted_api_key) && '' !== $submitted_api_key) {
            $current_api_key = (string) ($current['api_key'] ?? '');
            $api_key_changed = '' === $current_api_key
                || ! hash_equals($current_api_key, $submitted_api_key);
            $sanitized['api_key'] = $submitted_api_key;
        } elseif ($api_base_url_changed && ! empty($current['api_key'])) {
            $sanitized['api_key'] = '';
            add_settings_error(
                'encypher_provenance_settings',
                'api_key_reentry_required',
                __('API key cleared because the API Base URL changed. Re-enter the key to connect this site to the new endpoint.', 'encypher-provenance'),
                'warning'
            );
        } elseif (array_key_exists('api_key', $settings) && ! array_key_exists('api_key', $sanitized)) {
            $sanitized['api_key'] = '';
        }
        if (
            $api_key_changed
            || $api_base_url_changed
            || '' === trim((string) ($sanitized['api_key'] ?? ''))
        ) {
            Capabilities::clear();
        }

        if ($is_behavior_save) {
            foreach (['protected_publish_enabled', 'auto_mark_on_publish', 'auto_mark_on_update', 'auto_sign_images', 'show_badge', 'media_badges', 'local_hashing', 'allow_full_asset_upload', 'allow_non_production_signing'] as $checkbox) {
                $sanitized[$checkbox] = isset($settings[$checkbox]) && (bool) $settings[$checkbox];
            }

            if (array_key_exists('wordpress_ai_enabled', $settings)
                || ! empty($current['wordpress_ai_enabled'])) {
                $sanitized['wordpress_ai_enabled'] = isset($settings['wordpress_ai_enabled'])
                    && (bool) $settings['wordpress_ai_enabled'];
            }

            $post_types = isset($settings['post_types']) && is_array($settings['post_types'])
                ? array_values(array_unique(array_filter(array_map('sanitize_key', $settings['post_types']))))
                : [];
            $sanitized['post_types'] = $post_types;
            $roles = isset($settings['protected_publish_roles']) && is_array($settings['protected_publish_roles'])
                ? array_values(array_unique(array_filter(array_map('sanitize_key', $settings['protected_publish_roles']))))
                : [];
            $sanitized['protected_publish_roles'] = $roles;

            $term_policy = [];
            $term_lines = isset($settings['protected_publish_terms_input'])
                ? preg_split('/\R/', sanitize_textarea_field((string) $settings['protected_publish_terms_input']))
                : [];
            foreach (is_array($term_lines) ? $term_lines : [] as $line) {
                [$taxonomy, $slugs] = array_pad(explode(':', (string) $line, 2), 2, '');
                $taxonomy = sanitize_key($taxonomy);
                $values = array_values(array_unique(array_filter(array_map(
                    'sanitize_title',
                    array_map('trim', explode(',', $slugs))
                ))));
                if ('' !== $taxonomy && taxonomy_exists($taxonomy) && ! empty($values)) {
                    $term_policy[$taxonomy] = $values;
                }
            }
            $sanitized['protected_publish_terms'] = $term_policy;

            $valid_positions = ['top', 'bottom', 'bottom-right'];
            $position = isset($settings['badge_position'])
                ? sanitize_key((string) $settings['badge_position'])
                : (isset($current['badge_position']) ? (string) $current['badge_position'] : 'bottom');
            $sanitized['badge_position'] = in_array($position, $valid_positions, true)
                ? $position
                : 'bottom';
        }

        // This legacy policy control is no longer rendered, but explicitly
        // submitted values are still validated for backwards-compatible
        // imports. When omitted, its stored value is preserved like every
        // other hidden option.
        if (array_key_exists('digital_source_type', $settings)) {
            $digital_source_type = sanitize_text_field(trim((string) $settings['digital_source_type']));
            $sanitized['digital_source_type'] = ('' === $digital_source_type
                || isset(self::DIGITAL_SOURCE_TYPES[$digital_source_type]))
                ? $digital_source_type
                : '';
        } elseif (! array_key_exists('digital_source_type', $sanitized)) {
            $sanitized['digital_source_type'] = '';
        }

        // Branding is local presentation configured by a WordPress admin, but
        // permission to configure or render it comes only from Encypher's
        // current server entitlement projection.
        if (! self::allows_whitelabel()) {
            $sanitized['show_badge'] = true;
            $sanitized['show_branding'] = true;
            unset($sanitized['brand_logo_id']);
            $sanitized['powered_by_encypher'] = true;
        } elseif ($is_behavior_save) {
            $sanitized['powered_by_encypher'] = isset($settings['powered_by_encypher'])
                && (bool) $settings['powered_by_encypher'];

            if (array_key_exists('brand_logo_id', $settings)) {
                $sanitized['brand_logo_id'] = $this->sanitize_brand_logo_id(
                    (int) $settings['brand_logo_id'],
                    isset($current['brand_logo_id']) ? (int) $current['brand_logo_id'] : 0,
                    $settings_valid
                );
            }
        }

        if ($is_behavior_save && $settings_valid) {
            add_settings_error(
                'encypher_provenance_settings',
                'encypher_settings_saved',
                __('Settings saved.', 'encypher-provenance'),
                'success'
            );
        }

        return $sanitized;
    }

    /**
     * Validate a submitted brand-logo attachment id. Only a local raster image
     * (PNG, JPG, WebP) is accepted; SVG and any other mime is rejected with a
     * settings notice and the previously stored id is kept. A zero/negative id
     * clears the logo.
     */
    private function sanitize_brand_logo_id(int $submitted_id, int $previous_id, bool &$settings_valid): int
    {
        if ($submitted_id <= 0) {
            return 0;
        }

        $mime = (string) get_post_mime_type($submitted_id);
        if (! in_array($mime, ['image/png', 'image/jpeg', 'image/webp'], true)) {
            add_settings_error(
                'encypher_provenance_settings',
                'brand_logo_invalid',
                __('Brand logo must be a PNG, JPG, or WebP image. SVG and other file types are not allowed. Reverted to the previous logo.', 'encypher-provenance'),
                'error'
            );
            $settings_valid = false;
            return $previous_id;
        }

        return $submitted_id;
    }



    /**
     * Return the five newest local failures in the compact cockpit shape.
     *
     * ErrorLog is already a bounded local ring buffer. This additional limit
     * keeps the settings page diagnostic, not a replacement log viewer. Every
     * string is normalized before the template performs output escaping, and
     * likely credentials are redacted from old records written before current
     * sanitization existed.
     *
     * @param int $limit Requested row count, clamped to the cockpit maximum.
     * @return array<int,array<string,mixed>>
     */
    public function get_recent_failures(int $limit = 5): array
    {
        if (! class_exists(ErrorLog::class)) {
            return [];
        }

        $limit = max(1, min(5, $limit));
        $raw = array_slice(ErrorLog::get_raw_log(), 0, ErrorLog::MAX_ENTRIES);
        $records = [];
        foreach ($raw as $entry) {
            if (! is_array($entry)) {
                continue;
            }
            $timestamp = isset($entry['timestamp']) ? (string) $entry['timestamp'] : '';
            $sort_time = strtotime($timestamp);
            $entry['_timestamp_sort'] = false === $sort_time ? 0 : $sort_time;
            $records[] = $entry;
        }
        usort($records, static function (array $left, array $right): int {
            return (int) $right['_timestamp_sort'] <=> (int) $left['_timestamp_sort'];
        });

        $grouped_records = [];
        $group_indexes = [];
        foreach ($records as $entry) {
            $error_code = sanitize_key((string) ($entry['error_code'] ?? ''));
            $root_cause = '' !== $error_code
                ? $error_code
                : self::safe_failure_text((string) ($entry['error_message'] ?? ''), 160);
            $group_key = implode('|', [
                (string) ((int) ($entry['post_id'] ?? 0)),
                sanitize_key((string) ($entry['context'] ?? '')),
                $root_cause,
            ]);
            if (isset($group_indexes[$group_key])) {
                $grouped_records[$group_indexes[$group_key]]['_occurrences']++;
                continue;
            }
            $entry['_occurrences'] = 1;
            $group_indexes[$group_key] = count($grouped_records);
            $grouped_records[] = $entry;
        }

        $recent = [];
        foreach (array_slice($grouped_records, 0, $limit) as $entry) {
            $post_id = max(0, (int) ($entry['post_id'] ?? 0));
            $context = sanitize_key((string) ($entry['context'] ?? ''));
            $operation_labels = [
                'auto_sign'   => __('Auto sign', 'encypher-provenance'),
                'manual_sign' => __('Manual sign', 'encypher-provenance'),
                'verify'      => __('Verify', 'encypher-provenance'),
                'connection'  => __('Connection', 'encypher-provenance'),
            ];
            if (! isset($operation_labels[$context])) {
                $context = 'other';
            }

            $title = self::safe_failure_text((string) ($entry['post_title'] ?? ''), 100);
            if ('' === $title && $post_id > 0 && function_exists('get_the_title')) {
                $title = self::safe_failure_text((string) get_the_title($post_id), 100);
            }
            if ('' === $title && $post_id > 0) {
                /* translators: %d: WordPress content ID. */
                $title = sprintf(__('Content #%d', 'encypher-provenance'), $post_id);
            } elseif ('' === $title) {
                $title = __('Site connection', 'encypher-provenance');
            }

            $post_type = isset($entry['post_type']) ? sanitize_key((string) $entry['post_type']) : '';
            if ('' === $post_type && $post_id > 0 && function_exists('get_post_type')) {
                $post_type = sanitize_key((string) get_post_type($post_id));
            }
            $type_label = $post_id > 0 ? __('Content', 'encypher-provenance') : __('Site', 'encypher-provenance');
            if ('' !== $post_type && function_exists('get_post_type_object')) {
                $type_object = get_post_type_object($post_type);
                if ($type_object && isset($type_object->labels->singular_name)) {
                    $type_label = self::safe_failure_text((string) $type_object->labels->singular_name, 40);
                }
            } elseif ('' !== $post_type) {
                $type_label = self::safe_failure_text(ucwords(str_replace(['-', '_'], ' ', $post_type)), 40);
            }

            $technical_reason = self::safe_failure_text((string) ($entry['error_message'] ?? $entry['error_code'] ?? ''), 160);
            if ('' === $technical_reason) {
                $technical_reason = __('The operation did not complete.', 'encypher-provenance');
            }
            $reason = self::failure_guidance($context, $technical_reason);

            $timestamp_value = (int) ($entry['_timestamp_sort'] ?? 0);
            $timestamp_iso = $timestamp_value > 0 ? gmdate('Y-m-d\TH:i:s\Z', $timestamp_value) : '';
            $timestamp_display = self::format_timestamp($timestamp_value);

            $post = $post_id > 0 && function_exists('get_post') ? get_post($post_id) : null;
            $target_exists = $post_id > 0
                && (! function_exists('get_post') || ($post && 'trash' !== ($post->post_status ?? '')));
            $details = $target_exists
                ? self::canonical_details_for_post($post_id)
                : ['display_state' => 'needs_attention', 'assurance_state' => 'unknown'];
            $terminal_success = $target_exists
                && in_array((string) $details['display_state'], ['signed', 'verified'], true)
                && 'failed' !== (string) $details['assurance_state'];
            // A retry dispatch is still signing. Only a confirmed canonical
            // terminal success resolves a historical signing failure.
            $active = $post_id > 0 ? ($target_exists && ! $terminal_success) : true;

            $retryable = $active && $post_id > 0 && in_array($context, ['auto_sign', 'manual_sign'], true);
            if ($retryable) {
                $action_type = 'retry';
                $action_label = __('Retry signing', 'encypher-provenance');
                $action_url = function_exists('rest_url')
                    ? rest_url('encypher-provenance/v1/retry')
                    : '/wp-json/encypher-provenance/v1/retry';
            } elseif ($target_exists && ! $terminal_success) {
                $action_type = 'review';
                $action_label = __('Review content', 'encypher-provenance');
                $action_url = function_exists('get_edit_post_link') ? (string) get_edit_post_link($post_id, 'raw') : '';
                if ('' === $action_url) {
                    $action_url = admin_url('post.php?post=' . $post_id . '&action=edit');
                }
            } elseif ($post_id > 0) {
                $action_type = $terminal_success ? 'resolved' : 'unavailable';
                $action_label = '';
                $action_url = '';
            } else {
                $action_type = 'check_connection';
                $action_label = __('Check connection', 'encypher-provenance');
                $action_url = admin_url('admin.php?page=encypher#encypher-health-title');
            }

            $recent[] = [
                'post_id'          => $post_id,
                'title'            => $title,
                'type'             => $type_label,
                'operation'        => $context,
                'operation_label'  => $operation_labels[$context] ?? __('Other', 'encypher-provenance'),
                'reason'           => $reason,
                'technical_reason' => $technical_reason,
                'occurrences'      => max(1, (int) ($entry['_occurrences'] ?? 1)),
                'timestamp'        => $timestamp_iso,
                'active'           => $active,
                'resolved'         => $terminal_success,
                'target_exists'    => $target_exists,
                'display_state'    => (string) $details['display_state'],
                'assurance_state'  => (string) $details['assurance_state'],
                'timestamp_label'  => $timestamp_display,
                'action_type'      => $action_type,
                'action_label'     => $action_label,
                'action_url'       => $action_url,
            ];
        }

        return $recent;
    }

    /**
     * Translate backend diagnostics into the next action an administrator can
     * take. The redacted diagnostic remains available in a disclosure.
     */
    private static function failure_guidance(string $context, string $technical_reason): string
    {
        if ('connection' === $context) {
            return __('Encypher could not reach the saved connection. Check the connection and try again.', 'encypher-provenance');
        }
        if ('verify' === $context) {
            return __('We could not confirm this content. Retry verification or review the content for recent changes.', 'encypher-provenance');
        }
        if (preg_match('/encoding|parameter|configuration|forbidden|unauthori[sz]ed|permission/iu', $technical_reason)) {
            return __('Signing settings are not compatible with this content. Review signing settings or contact your Encypher administrator.', 'encypher-provenance');
        }
        if (in_array($context, ['auto_sign', 'manual_sign'], true)) {
            return __('Signing did not complete. Retry signing for this content.', 'encypher-provenance');
        }
        return __('This operation did not complete. Review the details and try again.', 'encypher-provenance');
    }

    /**
     * Strip markup, redact likely credentials, collapse whitespace, and bound
     * text copied from the persistent failure ring.
     */
    private static function safe_failure_text(string $value, int $max_length): string
    {
        // phpcs:ignore WordPressVIPMinimum.Functions.StripTags.StripTagsOneParameter -- standalone test fallback; WordPress runtime always provides wp_strip_all_tags().
        $value = function_exists('wp_strip_all_tags') ? wp_strip_all_tags($value, true) : strip_tags($value);
        $value = preg_replace('/(authorization\s*:\s*bearer\s+)[^\s,;]+/iu', '$1[redacted]', $value) ?? '';
        $value = preg_replace('/((?:api[_ -]?key|token|secret|password|credential)\s*[:=]\s*)[^\s,;]+/iu', '$1[redacted]', $value) ?? '';
        $value = preg_replace('/\b(?:sk|pk|enc)_[A-Za-z0-9_-]{8,}\b/u', '[redacted]', $value) ?? '';
        $value = preg_replace('/\s+/u', ' ', trim($value)) ?? '';
        $max_length = max(1, $max_length);
        if (function_exists('mb_strlen') && mb_strlen($value, 'UTF-8') > $max_length) {
            return rtrim(mb_substr($value, 0, $max_length - 3, 'UTF-8')) . '...';
        }
        if (! function_exists('mb_strlen') && strlen($value) > $max_length) {
            return rtrim(substr($value, 0, $max_length - 3)) . '...';
        }
        return $value;
    }

    /**
     * Read the frozen canonical status contract. Missing wiring fails closed.
     *
     * @return array{display_state:string,assurance_state:string}
     */
    private static function canonical_details_for_post(int $post_id): array
    {
        if (class_exists(ContentStatus::class)) {
            $details = ContentStatus::details_for_post($post_id);
            return [
                'display_state' => (string) ($details['display_state'] ?? 'needs_attention'),
                'assurance_state' => (string) ($details['assurance_state'] ?? 'unknown'),
            ];
        }

        return ['display_state' => 'needs_attention', 'assurance_state' => 'unknown'];
    }

    /**
     * Format every cockpit timestamp in the site's configured timezone.
     *
     * @param int|string $value Unix timestamp or parseable stored timestamp.
     */
    private static function format_timestamp($value): string
    {
        if (is_int($value)) {
            $timestamp = $value;
        } elseif (function_exists('wp_timezone') && preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', (string) $value)) {
            $local = \DateTimeImmutable::createFromFormat('!Y-m-d H:i:s', (string) $value, wp_timezone());
            $timestamp = false === $local ? false : $local->getTimestamp();
        } else {
            $timestamp = strtotime((string) $value);
        }
        if (false === $timestamp || $timestamp <= 0) {
            return __('Not yet available', 'encypher-provenance');
        }
        if (function_exists('wp_date')) {
            return wp_date(get_option('date_format') . ' ' . get_option('time_format'), $timestamp);
        }
        return gmdate('Y-m-d H:i', $timestamp) . ' UTC';
    }
    public function render_settings_page(): void
    {
        if (! current_user_can('manage_options')) {
            return;
        }

        $settings = get_option('encypher_provenance_settings', []);
        if (! is_array($settings)) {
            $settings = [];
        }

        $is_connected = ! empty($settings['api_key']);
        $connection_status = isset($settings['connection_last_status'])
            ? (string) $settings['connection_last_status']
            : 'unknown';
        $last_checked_at = isset($settings['connection_last_checked_at'])
            ? (string) $settings['connection_last_checked_at']
            : '';
        $last_checked_label = self::format_timestamp($last_checked_at);
        $site_domain = (string) (wp_parse_url(get_site_url(), PHP_URL_HOST) ?: __('your site', 'encypher-provenance'));
        $org_name = isset($settings['organization_name']) ? trim((string) $settings['organization_name']) : '';
        $tier = isset($settings['tier']) ? trim((string) $settings['tier']) : '';
        $plan_label = '' === $tier
            ? __('Free', 'encypher-provenance')
            : ucwords(str_replace(['-', '_'], ' ', $tier));
        $wp_ai_visible = ! empty($settings['wordpress_ai_enabled'])
            || (function_exists('is_plugin_active') && is_plugin_active('wordpress-ai/wordpress-ai.php'));
        $is_connected_health = 'connected' === $connection_status;
        $protection_mode_enabled = ! array_key_exists('protected_publish_enabled', $settings)
            || ! empty($settings['protected_publish_enabled']);
        $claim_state = class_exists(Rest::class) ? Rest::workspace_claim_state($settings) : 'claimed';
        $coverage = $is_connected ? $this->get_local_coverage_summary($settings) : null;
        $media_scan = ($is_connected && class_exists(MediaScan::class)) ? MediaScan::get_state() : null;
        $media_scan_last_run_label = is_array($media_scan)
            ? self::format_timestamp((string) (
                $media_scan['completed_at']
                ?: ($media_scan['updated_at'] ?: $media_scan['started_at'])
            ))
            : self::format_timestamp('');
        $cdn_cta_dismissed = $this->cdn_cta_dismissed();
        $cdn_cta_nonce = function_exists('wp_create_nonce') ? wp_create_nonce('encypher_dismiss_cdn_cta') : '';
        $queue_stall = $is_connected ? self::detect_queue_stall() : ['stalled' => false];
        $queue_last_attempt_label = self::format_timestamp((string) ($queue_stall['last_attempt_at'] ?? ''));
        $queue_next_attempt_label = self::format_timestamp((string) ($queue_stall['next_attempt_at'] ?? ''));
        $queue_heartbeat_label = self::format_timestamp((string) ($queue_stall['heartbeat_at'] ?? ''));
        $recent_failures = $is_connected ? $this->get_recent_failures() : [];
        if ($is_connected_health) {
            foreach ($recent_failures as &$failure) {
                if ('connection' === ($failure['operation'] ?? '')) {
                    $failure['active'] = false;
                    $failure['resolved'] = true;
                    $failure['action_type'] = 'resolved';
                    $failure['action_label'] = '';
                    $failure['action_url'] = '';
                }
            }
            unset($failure);
        }
        $active_failure_count = count(array_filter(
            $recent_failures,
            static fn(array $failure): bool => ! empty($failure['active'])
                && ('connection' !== ($failure['operation'] ?? '') || ! $is_connected_health)
        ));
        $coverage_protected = (int) ($coverage['signed'] ?? 0) + (int) ($coverage['verified'] ?? 0);
        $coverage_attention = (int) ($coverage['edited'] ?? 0) + (int) ($coverage['needs_attention'] ?? 0);
        $coverage_unsigned = (int) ($coverage['not_signed'] ?? 0);
        $coverage_signing = (int) ($coverage['signing'] ?? 0);
        $coverage_total = array_sum(array_map(
            static fn(string $state): int => (int) ($coverage[$state] ?? 0),
            ['not_signed', 'signing', 'signed', 'verified', 'edited', 'needs_attention']
        ));
        $coverage_percent = $coverage_total > 0
            ? (int) round(($coverage_protected / $coverage_total) * 100)
            : 0;
        $verification_stats = $is_connected ? $this->get_public_verification_stats() : ['recent' => 0, 'lifetime' => 0];
        $reader_path = ($is_connected && class_exists(SiteHealth::class))
            ? SiteHealth::reader_path_summary()
            : ['status' => 'unknown', 'success' => 0, 'failed' => 0, 'unavailable' => 0];
        if (! $protection_mode_enabled || ! empty($queue_stall['stalled']) || (int) ($queue_stall['failed_count'] ?? 0) > 0 || $coverage_attention > 0 || $active_failure_count > 0 || 'broken' === ($reader_path['status'] ?? 'unknown')) {
            $overall_state = 'action';
        } elseif (! $is_connected_health) {
            $overall_state = 'check';
        } elseif ($coverage_signing > 0 || $coverage_unsigned > 0) {
            $overall_state = 'progress';
        } else {
            $overall_state = 'healthy';
        }


        include dirname(__DIR__) . '/admin/partials/settings-page.php';
    }
    /**
     * Return exhaustive canonical display-state counts for eligible public
     * content. Drafts are reported separately and never enter the denominator.
     *
     * @param array<string,mixed> $settings Plugin settings.
     * @return array<string,mixed>
     */
    public function get_local_coverage_summary(array $settings): array
    {
        $states = ['not_signed', 'signing', 'signed', 'verified', 'edited', 'needs_attention'];
        $empty = array_fill_keys($states, 0);
        $empty['drafts'] = 0;
        $empty['by_type'] = [];
        $eligible_types = $this->eligible_public_post_types($settings);
        if ([] === $eligible_types) {
            $empty['media'] = $this->media_coverage_counts();
            $empty['most_verified'] = $this->most_verified_posts();
            $empty['external_referrers'] = $this->external_referrer_count();
            return $empty;
        }

        $cached = get_transient(self::LOCAL_COVERAGE_CACHE_KEY);
        if (is_array($cached) && ($cached['types'] ?? null) === array_keys($eligible_types) && isset($cached['summary']) && is_array($cached['summary'])) {
            return $cached['summary'];
        }

        $summary = $empty;
        foreach ($eligible_types as $post_type => $label) {
            $type_summary = array_fill_keys($states, 0);
            $type_summary['label'] = $label;
            $type_summary['drafts'] = 0;
            $type_summary['urls'] = [];
            foreach ($states as $state) {
                $type_summary['urls'][$state] = add_query_arg(
                    ['post_type' => $post_type, 'encypher_provenance' => $state],
                    admin_url('edit.php')
                );
            }

            $page = 1;
            do {
                $public_query = new \WP_Query([
                    'post_type'              => $post_type,
                    'post_status'            => 'publish',
                    'posts_per_page'         => 100,
                    'paged'                  => $page,
                    'fields'                 => 'ids',
                    'no_found_rows'          => true,
                    'cache_results'          => false,
                    'update_post_meta_cache' => true,
                    'update_post_term_cache' => false,
                ]);
                $public_ids = (array) $public_query->posts;
                foreach ($public_ids as $raw_post_id) {
                    $details = self::canonical_details_for_post((int) $raw_post_id);
                    $state = (string) $details['display_state'];
                    if (! in_array($state, $states, true)) {
                        $state = 'needs_attention';
                    }
                    $type_summary[$state]++;
                    $summary[$state]++;
                }
                $page++;
            } while (100 === count($public_ids));

            $draft_query = new \WP_Query([
                'post_type'      => $post_type,
                'post_status'    => 'draft',
                'posts_per_page' => 1,
                'fields'         => 'ids',
                'no_found_rows'  => false,
            ]);
            $type_summary['drafts'] = max(0, (int) $draft_query->found_posts);
            $summary['drafts'] += $type_summary['drafts'];
            $summary['by_type'][$post_type] = $type_summary;
        }

        $summary['media'] = $this->media_coverage_counts();
        $summary['most_verified'] = $this->most_verified_posts();
        $summary['external_referrers'] = $this->external_referrer_count();

        set_transient(self::LOCAL_COVERAGE_CACHE_KEY, [
            'types'   => array_keys($eligible_types),
            'summary' => $summary,
        ], self::LOCAL_COVERAGE_CACHE_TTL);

        return $summary;
    }

    /**
     * Top public posts by lifetime verify count for the coverage card. Bounded
     * (3 rows, ids only) with a meta_value_num orderby; each row links to the
     * post's edit screen. Empty until content has actually been verified.
     *
     * @return array<int,array{id:int,title:string,count:int,edit_url:string}>
     */
    private function most_verified_posts(): array
    {
        if (! class_exists('WP_Query')) {
            return [];
        }
        $query = new \WP_Query([
            'post_type'      => 'any',
            'post_status'    => 'publish',
            'posts_per_page' => 3,
            'fields'         => 'ids',
            'no_found_rows'  => true,
            // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- admin coverage card: top 3 by the local verify counter, cached in the coverage transient; not a front-end query.
            'meta_key'       => '_encypher_verify_hits',
            'orderby'        => 'meta_value_num',
            'order'          => 'DESC',
        ]);

        $posts = is_array($query->posts) ? $query->posts : [];
        $out = [];
        foreach ($posts as $raw_id) {
            $post_id = (int) $raw_id;
            $count = (int) get_post_meta($post_id, '_encypher_verify_hits', true);
            if ($count <= 0) {
                continue;
            }
            $title = function_exists('get_the_title') ? (string) get_the_title($post_id) : '';
            if ('' === $title) {
                /* translators: %d: post ID used as a fallback title. */
                $title = sprintf(__('Post #%d', 'encypher-provenance'), $post_id);
            }
            $edit_url = function_exists('get_edit_post_link') ? (string) get_edit_post_link($post_id, 'raw') : '';
            $out[] = [
                'id'       => $post_id,
                'title'    => $title,
                'count'    => $count,
                'edit_url' => $edit_url,
            ];
        }
        return $out;
    }

    /**
     * Number of distinct external sites seen referring public verifies, from
     * the bounded encypher_verify_external_referrers option (domain => count).
     */
    private function external_referrer_count(): int
    {
        $map = get_option('encypher_verify_external_referrers', []);
        return is_array($map) ? count($map) : 0;
    }

    /**
     * Highest-count external referrer domains (up to $limit) for a
     * non-identifying title attribute on the coverage card. Domains only.
     *
     * @return string[]
     */
    private function external_referrer_top_domains(int $limit = 5): array
    {
        $map = get_option('encypher_verify_external_referrers', []);
        if (! is_array($map) || [] === $map) {
            return [];
        }
        arsort($map);
        return array_slice(array_keys($map), 0, max(1, $limit));
    }

    /**
     * Whether the current user dismissed the AI-crawler CDN analytics CTA.
     */
    private function cdn_cta_dismissed(): bool
    {
        if (! function_exists('get_current_user_id') || ! function_exists('get_user_meta')) {
            return false;
        }
        return (bool) get_user_meta(get_current_user_id(), self::CDN_CTA_DISMISSED_META, true);
    }

    /**
     * WP-Cron hook name for the deferred-sign queue (mirrors Rest).
     */
    public const DEFERRED_SIGN_HOOK = 'encypher_deferred_sign';


    /**
     * WP-Cron is considered "not firing" when a due event is overdue by more
     * than this (seconds).
     */
    private const STALL_CRON_LAG = 300; // 5 minutes

    /**
     * Return actionable queue and worker diagnostics from the real scheduler,
     * canonical signing leases, and current failure meta.
     *
     * Pending work is stalled as soon as it has no fresh worker heartbeat.
     * This catches a dead worker before a much older cron-overdue threshold.
     *
     * @return array<string,mixed>
     */
    public static function detect_queue_stall(): array
    {
        $now = time();
        $deferred = self::pending_cron_events(self::DEFERRED_SIGN_HOOK);
        $last_heartbeat = 0;
        $has_fresh_heartbeat = false;
        foreach ($deferred['post_ids'] as $post_id) {
            $lease = get_post_meta((int) $post_id, '_encypher_signing_in_progress', true);
            $heartbeat = is_array($lease)
                ? max(0, (int) ($lease['heartbeat_at'] ?? 0))
                : (is_numeric($lease) ? max(0, (int) $lease) : 0);
            $last_heartbeat = max($last_heartbeat, $heartbeat);
            if (! empty($lease) && ! SigningStatus::flag_is_stale($lease, $now)) {
                $has_fresh_heartbeat = true;
            }
        }

        $failed_count = 0;
        $last_failure_attempt = 0;
        if (class_exists('WP_Query')) {
            $failed_query = new \WP_Query([
                'post_type'      => 'any',
                'post_status'    => 'any',
                'posts_per_page' => 1,
                'fields'         => 'ids',
                'no_found_rows'  => false,
                // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- bounded on-demand queue diagnostic.
                'meta_query'     => [
                    ['key' => '_encypher_last_sign_error', 'compare' => 'EXISTS'],
                ],
            ]);
            $failed_count = max(0, (int) $failed_query->found_posts);
            $failed_ids = is_array($failed_query->posts) ? $failed_query->posts : [];
            if (! empty($failed_ids)) {
                $failure = get_post_meta((int) $failed_ids[0], '_encypher_last_sign_error', true);
                $raw_failure_time = is_array($failure) ? ($failure['time'] ?? 0) : 0;
                $parsed_failure_time = is_numeric($raw_failure_time)
                    ? (int) $raw_failure_time
                    : strtotime((string) $raw_failure_time);
                $last_failure_attempt = false === $parsed_failure_time ? 0 : max(0, $parsed_failure_time);
            }
        }

        $scan = class_exists(MediaScan::class) ? MediaScan::get_state() : ['status' => 'idle'];
        $scan_running = 'running' === ($scan['status'] ?? '');
        $scan_updated = $scan_running ? strtotime((string) ($scan['updated_at'] ?? '')) : false;
        $scan_fresh = $scan_running
            && false !== $scan_updated
            && ($now - (int) $scan_updated) <= SigningStatus::STALE_AFTER_SECONDS;
        $scan_next = $scan_running && function_exists('wp_next_scheduled')
            ? wp_next_scheduled(MediaScan::CRON_HOOK)
            : false;

        $result = [
            'stalled'             => false,
            'reason'              => '',
            'pending_count'       => (int) $deferred['count'] + ($scan_running ? 1 : 0),
            'failed_count'        => $failed_count,
            'deferred_pending'    => (int) $deferred['count'],
            'oldest_deferred_age' => null === $deferred['oldest_ts'] ? 0 : max(0, $now - (int) $deferred['oldest_ts']),
            'scan_running'        => $scan_running,
            'cron_lag'            => 0,
            'heartbeat_fresh'     => $has_fresh_heartbeat || $scan_fresh,
            'heartbeat_at'        => $last_heartbeat > 0
                ? gmdate('c', $last_heartbeat)
                : ($scan_updated ? gmdate('c', (int) $scan_updated) : ''),
            'last_attempt_at'     => max($last_heartbeat, $last_failure_attempt) > 0
                ? gmdate('c', max($last_heartbeat, $last_failure_attempt))
                : (string) ($scan['updated_at'] ?? ''),
            'next_attempt_at'     => null !== $deferred['oldest_ts']
                ? gmdate('c', (int) $deferred['oldest_ts'])
                : ($scan_next ? gmdate('c', (int) $scan_next) : ''),
        ];

        if ($deferred['count'] > 0 && ! $has_fresh_heartbeat) {
            $result['stalled'] = true;
            $result['reason'] = 'worker_heartbeat';
        } elseif ($scan_running && ! $scan_fresh) {
            $result['stalled'] = true;
            $result['reason'] = 'media_scan_heartbeat';
        } elseif ($scan_running && false === $scan_next) {
            $result['stalled'] = true;
            $result['reason'] = 'media_scan';
        } elseif (null !== $deferred['oldest_ts'] && ($now - (int) $deferred['oldest_ts']) > self::STALL_CRON_LAG) {
            $result['stalled'] = true;
            $result['reason'] = 'deferred_sign';
            $result['cron_lag'] = $now - (int) $deferred['oldest_ts'];
        } elseif ($scan_next && ($now - (int) $scan_next) > self::STALL_CRON_LAG) {
            $result['stalled'] = true;
            $result['reason'] = 'media_scan';
            $result['cron_lag'] = $now - (int) $scan_next;
        }

        return $result;
    }

    /**
     * Inspect real WP-Cron events for one hook.
     *
     * @return array{count:int,oldest_ts:int|null,post_ids:int[]}
     */
    private static function pending_cron_events(string $hook): array
    {
        $out = ['count' => 0, 'oldest_ts' => null, 'post_ids' => []];
        if (! function_exists('_get_cron_array')) {
            return $out;
        }
        $cron = _get_cron_array();
        if (! is_array($cron)) {
            return $out;
        }
        foreach ($cron as $timestamp => $hooks) {
            if (! is_array($hooks) || ! isset($hooks[$hook]) || ! is_array($hooks[$hook])) {
                continue;
            }
            $out['count'] += count($hooks[$hook]);
            $ts = (int) $timestamp;
            if (null === $out['oldest_ts'] || $ts < $out['oldest_ts']) {
                $out['oldest_ts'] = $ts;
            }
            foreach ($hooks[$hook] as $event) {
                $args = is_array($event) ? (array) ($event['args'] ?? []) : [];
                if (isset($args[0]) && is_numeric($args[0])) {
                    $out['post_ids'][] = (int) $args[0];
                }
            }
        }
        $out['post_ids'] = array_values(array_unique(array_filter($out['post_ids'])));
        return $out;
    }

    /**
     * Public verification successes derived from canonical reader verdicts.
     *
     * New outcome maps count only trusted and valid_untrusted as successful.
     * Legacy hit counters remain a migration fallback until the first outcome
     * map is written.
     *
     * @return array{recent:int,lifetime:int}
     */
    public function get_public_verification_stats(): array
    {
        $outcomes = get_option('encypher_verify_outcomes', null);
        $lifetime = is_array($outcomes)
            ? max(0, (int) ($outcomes['trusted'] ?? 0)) + max(0, (int) ($outcomes['valid_untrusted'] ?? 0))
            : max(0, (int) get_option('encypher_verify_hits', 0));

        return [
            'recent'   => array_sum(self::verification_daily_map(30)),
            'lifetime' => $lifetime,
        ];
    }

    /**
     * Trailing-window canonical reader successes, with legacy fallback.
     *
     * @param int $days Number of trailing days to include (>= 1), oldest first.
     * @return array<string,int> Map of 'Y-m-d' => success count, oldest first.
     */
    public static function verification_daily_map(int $days): array
    {
        if ($days < 1) {
            $days = 1;
        }

        $legacy = get_option('encypher_verify_hits_daily', []);
        if (! is_array($legacy)) {
            $legacy = [];
        }

        $map = [];
        for ($i = $days - 1; $i >= 0; $i--) {
            $ts = time() - ($i * 86400);
            $ymd = gmdate('Y-m-d', $ts);
            $outcomes = get_option('encypher_verify_outcomes_' . $ymd, null);
            if (is_array($outcomes)) {
                $map[$ymd] = max(0, (int) ($outcomes['trusted'] ?? 0))
                    + max(0, (int) ($outcomes['valid_untrusted'] ?? 0));
                continue;
            }
            $per_day = (int) get_option('encypher_verify_hits_day_' . gmdate('Ymd', $ts), 0);
            $legacy_day = isset($legacy[$ymd]) ? (int) $legacy[$ymd] : 0;
            $map[$ymd] = max(0, $per_day) + max(0, $legacy_day);
        }

        return $map;
    }

    /**
     * Bounded WP_Query counts of attachments by source-provenance bucket,
     * using the SAME predicates as the media list filter (MediaScan) so the
     * coverage card and the filter never disagree.
     *
     * @return array{carries:int,none:int,unscanned:int,filter_url:string}
     */
    private function media_coverage_counts(): array
    {
        $counts = ['carries' => 0, 'none' => 0, 'unscanned' => 0, 'filter_url' => ''];
        if (! class_exists(MediaScan::class)) {
            return $counts;
        }
        foreach (['carries', 'none', 'unscanned'] as $bucket) {
            $query = new \WP_Query([
                'post_type'      => 'attachment',
                'post_status'    => 'inherit',
                'posts_per_page' => 1,
                'fields'         => 'ids',
                'no_found_rows'  => false,
                // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Cockpit media coverage counts are admin-only and each query fetches one ID.
                'meta_query'     => MediaScan::media_meta_query($bucket),
            ]);
            $counts[$bucket] = (int) $query->found_posts;
        }
        $counts['filter_url'] = add_query_arg(['encypher_media_provenance' => 'none'], admin_url('upload.php'));
        return $counts;
    }

    /**
     * Dependency-free inline SVG sparkline of the last N daily public
     * verification buckets (default 30), zero-filled for missing days. Pure
     * (reads the per-day verify options, merging the legacy daily array); no
     * JS library.
     *
     * @param int $days Number of trailing daily slots to plot.
     * @return string SVG markup (all attribute values escaped).
     */
    public function verification_sparkline_svg(int $days = 30): string
    {
        if ($days < 2) {
            $days = 2;
        }
        $series = array_values(self::verification_daily_map($days));

        $width  = 174.0;
        $height = 36.0;
        $pad    = 3.0;
        $draw_w = $width - (2 * $pad);
        $draw_h = $height - (2 * $pad);
        $max    = max($series);
        if ($max < 1) {
            $max = 1;
        }

        $count  = count($series);
        $step   = $draw_w / ($count - 1);
        $points = [];
        foreach ($series as $index => $value) {
            $x        = $pad + ($index * $step);
            $y        = $pad + ($draw_h * (1 - ($value / $max)));
            $points[] = round($x, 1) . ',' . round($y, 1);
        }
        $points_attr = implode(' ', $points);
        $total       = array_sum($series);

        return '<svg class="encypher-sparkline" xmlns="http://www.w3.org/2000/svg"'
            . ' viewBox="0 0 ' . (int) $width . ' ' . (int) $height . '"'
            . ' width="' . (int) $width . '" height="' . (int) $height . '"'
            . ' preserveAspectRatio="none" role="img" focusable="false"'
            . ' aria-label="' . esc_attr(sprintf(
                /* translators: 1: number of days, 2: total verifications in the window. */
                __('Public verifications over the last %1$d days: %2$s total', 'encypher-provenance'),
                $days,
                number_format_i18n($total)
            )) . '">'
            . '<polyline fill="none" stroke="#1b2f50" stroke-width="1.5"'
            . ' stroke-linejoin="round" stroke-linecap="round" points="'
            . esc_attr($points_attr) . '" />'
            . '</svg>';
    }

    /**
     * Echo the verification sparkline through a tight SVG kses allowlist.
     */
    public function render_verification_sparkline(): void
    {
        echo wp_kses($this->verification_sparkline_svg(), [
            'svg'      => [
                'class'               => true,
                'xmlns'               => true,
                'viewbox'             => true,
                'width'               => true,
                'height'              => true,
                'preserveaspectratio' => true,
                'role'                => true,
                'focusable'           => true,
                'aria-label'          => true,
            ],
            'polyline' => [
                'fill'            => true,
                'stroke'          => true,
                'stroke-width'    => true,
                'stroke-linejoin' => true,
                'stroke-linecap'  => true,
                'points'          => true,
            ],
        ]);
    }

    /**
     * @param array<string,mixed> $settings Plugin settings.
     * @return array<string,string> Eligible post-type slugs keyed to labels.
     */
    private function eligible_public_post_types(array $settings): array
    {
        $configured = isset($settings['post_types']) && is_array($settings['post_types'])
            ? $settings['post_types']
            : ['post', 'page'];
        $public_types = get_post_types(['public' => true], 'objects');
        $eligible = [];
        foreach (array_unique(array_filter(array_map('sanitize_key', $configured))) as $post_type) {
            if (isset($public_types[$post_type])) {
                $eligible[$post_type] = (string) $public_types[$post_type]->label;
            }
        }
        return $eligible;
    }

    /**
     * @return array<int|string,mixed> Meta predicate for one canonical display state.
     */
    private function provenance_meta_query(string $state): array
    {
        $signed = ['c2pa_protected', 'signed'];
        $verified = ['c2pa_verified', 'verified'];
        $failures = ['verification_failed', 'embedding_failed', 'media_signing_failed', 'tampered', 'signing_stalled'];
        $not_running = [
            ['key' => '_encypher_signing_in_progress', 'compare' => 'NOT EXISTS'],
            ['key' => '_encypher_provenance_generation_pending', 'compare' => 'NOT EXISTS'],
        ];

        if ('signing' === $state) {
            return [
                'relation' => 'OR',
                ['key' => '_encypher_signing_in_progress', 'compare' => 'EXISTS'],
                ['key' => '_encypher_provenance_generation_pending', 'compare' => 'EXISTS'],
            ];
        }
        if ('needs_attention' === $state) {
            return [
                'relation' => 'OR',
                ['key' => '_encypher_provenance_status', 'value' => $failures, 'compare' => 'IN'],
                ['key' => '_encypher_last_sign_error', 'compare' => 'EXISTS'],
                ['key' => '_encypher_publication_assurance', 'value' => 'failed', 'compare' => 'LIKE'],
            ];
        }
        if ('edited' === $state) {
            return array_merge([
                'relation' => 'AND',
                ['key' => '_encypher_provenance_status', 'value' => 'modified', 'compare' => '='],
                ['key' => '_encypher_last_sign_error', 'compare' => 'NOT EXISTS'],
            ], $not_running);
        }
        if (in_array($state, ['signed', 'verified'], true)) {
            return array_merge([
                'relation' => 'AND',
                [
                    'key' => '_encypher_provenance_status',
                    'value' => 'verified' === $state ? $verified : $signed,
                    'compare' => 'IN',
                ],
                ['key' => '_encypher_last_sign_error', 'compare' => 'NOT EXISTS'],
                [
                    'relation' => 'OR',
                    ['key' => '_encypher_publication_assurance', 'compare' => 'NOT EXISTS'],
                    ['key' => '_encypher_publication_assurance', 'value' => 'failed', 'compare' => 'NOT LIKE'],
                ],
            ], $not_running);
        }
        if ('not_signed' === $state) {
            return array_merge([
                'relation' => 'AND',
                [
                    'relation' => 'OR',
                    ['key' => '_encypher_provenance_status', 'compare' => 'NOT EXISTS'],
                    [
                        'key' => '_encypher_provenance_status',
                        'value' => array_merge($signed, $verified, $failures, ['modified']),
                        'compare' => 'NOT IN',
                    ],
                ],
                ['key' => '_encypher_last_sign_error', 'compare' => 'NOT EXISTS'],
            ], $not_running);
        }

        return ['key' => '_encypher_provenance_status', 'value' => '__invalid_display_state__', 'compare' => '='];
    }

    public function invalidate_local_coverage_for_meta($meta_id, $post_id, $meta_key, $meta_value): void
    {
        if (in_array($meta_key, ['_encypher_provenance_status', '_encypher_last_sign_error', '_encypher_signing_in_progress', '_encypher_provenance_generation_pending', '_encypher_publication_assurance', '_encypher_marked', '_encypher_source_has_c2pa', '_encypher_source_scanned_at'], true)) {
            $this->clear_local_coverage_cache();
        }
    }

    public function clear_local_coverage_cache(): void
    {
        self::flush_local_coverage_cache();
    }

    /**
     * Static coverage-cache invalidator so background workers (e.g. the media
     * scan cron) can drop the cache without an Admin instance.
     */
    public static function flush_local_coverage_cache(): void
    {
        delete_transient(self::LOCAL_COVERAGE_CACHE_KEY);
    }



    public function enqueue_settings_page_assets(string $hook_suffix): void
    {
        if ('toplevel_page_encypher' !== $hook_suffix) {
            return;
        }

        $settings_option = get_option('encypher_provenance_settings', []);
        if (! is_array($settings_option)) {
            $settings_option = [];
        }

        wp_enqueue_style(
            'encypher-provenance-settings',
            ENCYPHER_PROVENANCE_PLUGIN_URL . 'assets/css/settings-page.css',
            [],
            ENCYPHER_PROVENANCE_VERSION
        );

        wp_enqueue_script(
            'encypher-provenance-settings',
            ENCYPHER_PROVENANCE_PLUGIN_URL . 'assets/js/settings-page.js',
            ['jquery'],
            ENCYPHER_PROVENANCE_VERSION,
            true
        );

        // The Branding section uses the WP media frame to pick a logo. Load it
        // only when Encypher's current capability projection allows branding.
        if (self::allows_whitelabel() && function_exists('wp_enqueue_media')) {
            wp_enqueue_media();
        }

        wp_localize_script(
            'encypher-provenance-settings',
            'EncypherBrandingData',
            [
                'mediaTitle'  => __('Select brand logo', 'encypher-provenance'),
                'mediaButton' => __('Use this logo', 'encypher-provenance'),
                'svgRejected' => __('SVG files are not allowed. Choose a PNG, JPG, or WebP image.', 'encypher-provenance'),
            ]
        );

        wp_localize_script(
            'encypher-provenance-settings',
            'wpApiSettings',
            [
                'root' => esc_url_raw(rest_url()),
                'nonce' => wp_create_nonce('wp_rest'),
            ]
        );

        wp_localize_script(
            'encypher-provenance-settings',
            'EncypherSettingsData',
            [
                'markUrl' => ENCYPHER_PROVENANCE_PLUGIN_URL . 'assets/images/brand/encypher-mark-navy.svg',
                'loaderUrl' => ENCYPHER_PROVENANCE_PLUGIN_URL . 'assets/images/brand/encypher-loader-navy.svg',
                'strings' => [
                    'connectStarted' => __('Opening Encypher in a new tab. Approve this site there, then return here while this page checks for completion.', 'encypher-provenance'),
                    'connectCompleted' => __('WordPress connected successfully.', 'encypher-provenance'),
                    'oauthFailed' => __('The connection did not complete. Nothing was changed. Retry or use Advanced connection.', 'encypher-provenance'),
                    'oauthCancelled' => __('Connection cancelled. This site is still not connected.', 'encypher-provenance'),
                    'oauthRetry' => __('Retry connection', 'encypher-provenance'),
                ],
            ]
        );
    }

    /**
     * Load the deactivation exit survey only on the plugins list screen, and
     * only for admins who could reach the manage_options REST route. The modal
     * is best-effort: it never blocks the deactivation it wraps.
     */
    public function enqueue_deactivation_survey_assets(string $hook_suffix): void
    {
        if ('plugins.php' !== $hook_suffix) {
            return;
        }
        if (function_exists('current_user_can') && ! current_user_can('manage_options')) {
            return;
        }

        wp_enqueue_style(
            'encypher-deactivation-survey',
            ENCYPHER_PROVENANCE_PLUGIN_URL . 'assets/css/deactivation-survey.css',
            [],
            ENCYPHER_PROVENANCE_VERSION
        );

        wp_enqueue_script(
            'encypher-deactivation-survey',
            ENCYPHER_PROVENANCE_PLUGIN_URL . 'assets/js/deactivation-survey.js',
            [],
            ENCYPHER_PROVENANCE_VERSION,
            true
        );

        wp_localize_script(
            'encypher-deactivation-survey',
            'EncypherDeactivationSurvey',
            [
                'restUrl'    => esc_url_raw(rest_url('encypher-provenance/v1/deactivation-feedback')),
                'nonce'      => wp_create_nonce('wp_rest'),
                'pluginFile' => plugin_basename(ENCYPHER_PROVENANCE_PLUGIN_FILE),
                'reasons'    => [
                    'connection_problems' => __('Connection problems', 'encypher-provenance'),
                    'too_complex'         => __('Too complex to set up', 'encypher-provenance'),
                    'did_not_need'        => __('I did not need it', 'encypher-provenance'),
                    'something_broke'     => __('Something broke on my site', 'encypher-provenance'),
                ],
                'strings'    => [
                    'title'       => __('Quick question before you go', 'encypher-provenance'),
                    'subtitle'    => __('What is making you deactivate Encypher Provenance?', 'encypher-provenance'),
                    'placeholder' => __('Anything else? (optional)', 'encypher-provenance'),
                    'skip'        => __('Skip and deactivate', 'encypher-provenance'),
                    'dismiss'     => __('Close', 'encypher-provenance'),
                ],
            ]
        );
    }

    /**
     * Register a suggested privacy policy section (admin_init) describing,
     * factually, what the plugin sends to the Encypher API and what it keeps
     * only on this site. Kept short and dash-free so a site owner can copy it
     * straight into their published policy.
     */
    public function add_privacy_policy_content(): void
    {
        if (! function_exists('wp_add_privacy_policy_content')) {
            return;
        }

        $content  = '<p>' . esc_html__('Encypher Provenance signs your content and lets readers verify it. This explains what the plugin sends to the Encypher API and what it keeps only on your site.', 'encypher-provenance') . '</p>';

        $content .= '<p><strong>' . esc_html__('Information sent to the Encypher API', 'encypher-provenance') . '</strong></p>';
        $content .= '<ul>';
        $content .= '<li>' . esc_html__('Verification events: when a reader verifies a piece of content, the plugin reports the post, its URL, and whether verification succeeded so your workspace can show verification activity.', 'encypher-provenance') . '</li>';
        $content .= '<li>' . esc_html__('Install status telemetry: the plugin reports connection and setup status, including an activation funnel object that records when the site connected, first signed content, and claimed its workspace.', 'encypher-provenance') . '</li>';
        $content .= '<li>' . esc_html__('Deactivation feedback: only if you choose to submit it, the reason you select and any note you write on the deactivation survey are sent so we can improve the plugin.', 'encypher-provenance') . '</li>';
        $content .= '</ul>';

        $content .= '<p><strong>' . esc_html__('Information kept only on your site', 'encypher-provenance') . '</strong></p>';
        $content .= '<ul>';
        $content .= '<li>' . esc_html__('Verification counters: running totals and per day counts of verifications.', 'encypher-provenance') . '</li>';
        $content .= '<li>' . esc_html__('External referrer domains: a bounded map of the domain names that send readers to verification, without IP addresses, paths, or query strings.', 'encypher-provenance') . '</li>';
        $content .= '<li>' . esc_html__('Coverage data: which of your posts and pages are signed.', 'encypher-provenance') . '</li>';
        $content .= '<li>' . esc_html__('Media scan results: the progress and findings of the media library scan.', 'encypher-provenance') . '</li>';
        $content .= '</ul>';

        wp_add_privacy_policy_content('Encypher Provenance', wp_kses_post($content));
    }

    public function render_api_base_url_field(): void
    {
        $options = get_option('encypher_provenance_settings', []);
        $value = isset($options['api_base_url']) && '' !== $options['api_base_url']
            ? esc_url($options['api_base_url'])
            : 'https://api.encypher.com/api/v1';
        ?>
        <label class="screen-reader-text" for="api_base_url"><?php esc_html_e('Self-hosted API endpoint', 'encypher-provenance'); ?></label>
        <input type="url" id="api_base_url" class="regular-text" value="<?php echo esc_attr($value); ?>" placeholder="https://api.encypher.com/api/v1" />
        <p class="description">
            <?php esc_html_e('Keep the hosted endpoint unless your organization operates a self-hosted Encypher API.', 'encypher-provenance'); ?>
        </p>
        <?php
    }

    public function render_api_key_field(): void
    {
        $options = get_option('encypher_provenance_settings', []);
        $has_stored_key = isset($options['api_key']) && '' !== $options['api_key'];
        $placeholder = $has_stored_key ? '••••••••••••••••••••' : '';
        ?>
        <label class="screen-reader-text" for="api_key"><?php esc_html_e('Encypher API key', 'encypher-provenance'); ?></label>
        <input type="password" id="api_key" class="regular-text" value="" placeholder="<?php echo esc_attr($placeholder); ?>" autocomplete="new-password" />
        <p class="description">
            <?php if ($has_stored_key) : ?>
                <?php esc_html_e('A key is on file. Leave this blank to keep it, or enter a new key to replace it.', 'encypher-provenance'); ?>
            <?php else : ?>
                <?php esc_html_e('Use an API key only when guided account connection is not appropriate.', 'encypher-provenance'); ?>
            <?php endif; ?>
        </p>
        <?php
    }

    /**
     * Render the recommended "Connect an existing Encypher account" card.
     *
     * Dashboard OAuth handoff only: no address field and no magic-link action
     * (PRD Surface-1 / T2.8). The JS opens Encypher in a new tab, polls the
     * dashboard-issued connection session, and returns cleanly to Not connected
     * on failure, cancel, or expiry while preserving every stored setting.
     */
    public function render_magic_link_connect_field(): void
    {
        ?>
        <div class="encypher-oauth-connect">
            <button type="button" id="encypher-oauth-start-btn" class="button button-primary button-hero">
                <?php esc_html_e('Connect an existing Encypher account', 'encypher-provenance'); ?>
            </button>
            <button type="button" id="encypher-oauth-cancel-btn" class="button" hidden>
                <?php esc_html_e('Cancel', 'encypher-provenance'); ?>
            </button>
            <p class="description">
                <?php esc_html_e('Approve the connection in the Encypher dashboard that opens in a new tab; there is no delivery step and nothing is sent to your inbox.', 'encypher-provenance'); ?>
            </p>
            <div id="encypher-oauth-status" role="status" aria-live="polite"></div>
        </div>
        <?php
    }


    public function enqueue_block_editor_assets(): void
    {
        // License catalog loader (thin client; fetches builtin + org templates
        // from the REST proxy). Loaded first; editor-sidebar + block-rights depend on it.
        wp_enqueue_script(
            'encypher-license-catalog',
            ENCYPHER_PROVENANCE_PLUGIN_URL . 'assets/js/license-catalog.js',
            ['wp-api-fetch'],
            ENCYPHER_PROVENANCE_VERSION,
            true
        );

        $asset_path = ENCYPHER_PROVENANCE_PLUGIN_URL . 'assets/js/editor-sidebar.js';
        wp_enqueue_script(
            'encypher-provenance-editor-sidebar',
            $asset_path,
            ['wp-plugins', 'wp-edit-post', 'wp-editor', 'wp-components', 'wp-element', 'wp-data', 'wp-api-fetch', 'encypher-license-catalog'],
            ENCYPHER_PROVENANCE_VERSION,
            true
        );

        wp_localize_script(
            'encypher-provenance-editor-sidebar',
            'EncypherProvenanceConfig',
            [
                'nonce' => wp_create_nonce('wp_rest'),
                'markUrl' => ENCYPHER_PROVENANCE_PLUGIN_URL . 'assets/images/brand/encypher-mark-navy.svg',
                'loaderUrl' => ENCYPHER_PROVENANCE_PLUGIN_URL . 'assets/images/brand/encypher-loader-navy.svg',
            ]
        );

        wp_enqueue_style(
            'encypher-provenance-editor-css',
            ENCYPHER_PROVENANCE_PLUGIN_URL . 'assets/css/editor.css',
            [],
            ENCYPHER_PROVENANCE_VERSION
        );

        if (Capabilities::allows(Capabilities::SEGMENT_RIGHTS)) {
            // Block-level rights inspector panel.
            wp_enqueue_script(
                'encypher-block-rights',
                ENCYPHER_PROVENANCE_PLUGIN_URL . 'assets/js/block-rights.js',
                ['wp-blocks', 'wp-hooks', 'wp-compose', 'wp-block-editor', 'wp-components', 'wp-element', 'wp-data', 'wp-api-fetch', 'encypher-license-catalog'],
                ENCYPHER_PROVENANCE_VERSION,
                true
            );
            wp_enqueue_style(
                'encypher-block-rights-css',
                ENCYPHER_PROVENANCE_PLUGIN_URL . 'assets/css/block-rights.css',
                ['encypher-provenance-editor-css'],
                ENCYPHER_PROVENANCE_VERSION
            );
        }

        // Image-block provenance hints (reads attachment REST meta).
        wp_enqueue_script(
            'encypher-editor-media-hints',
            ENCYPHER_PROVENANCE_PLUGIN_URL . 'assets/js/editor-media-hints.js',
            ['wp-blocks', 'wp-hooks', 'wp-compose', 'wp-block-editor', 'wp-components', 'wp-element', 'wp-data', 'wp-api-fetch'],
            ENCYPHER_PROVENANCE_VERSION,
            true
        );

    }

    public function enqueue_classic_assets(string $hook_suffix): void
    {
        if (! in_array($hook_suffix, ['post.php', 'post-new.php'], true)) {
            return;
        }

        if (function_exists('get_current_screen')) {
            $screen = get_current_screen();
            if ($screen && method_exists($screen, 'is_block_editor') && $screen->is_block_editor()) {
                return;
            }
        }

        wp_enqueue_script(
            'encypher-provenance-classic',
            ENCYPHER_PROVENANCE_PLUGIN_URL . 'assets/js/classic-meta-box.js',
            ['jquery'],
            ENCYPHER_PROVENANCE_VERSION,
            true
        );

        wp_localize_script(
            'encypher-provenance-classic',
            'EncypherProvenanceClassic',
            [
                'restUrl' => esc_url_raw(rest_url('encypher-provenance/v1/')),
                'nonce' => wp_create_nonce('wp_rest'),
            ]
        );

        wp_enqueue_style(
            'encypher-provenance-editor-css',
            ENCYPHER_PROVENANCE_PLUGIN_URL . 'assets/css/editor.css',
            [],
            ENCYPHER_PROVENANCE_VERSION
        );
    }

    public function register_classic_meta_box(string $current_post_type = '', $post = null): void
    {
        $settings = get_option('encypher_provenance_settings', []);
        $post_types = isset($settings['post_types']) && is_array($settings['post_types'])
            ? $settings['post_types'] : ['post', 'page'];
        foreach ($post_types as $post_type) {
            if ($current_post_type && $post_type !== $current_post_type) {
                continue;
            }
            $uses_block_editor = $post && function_exists('use_block_editor_for_post')
                ? use_block_editor_for_post($post)
                : (function_exists('use_block_editor_for_post_type') && use_block_editor_for_post_type($post_type));
            if ($uses_block_editor) {
                continue;
            }
            add_meta_box(
                'encypher_provenance_meta_box',
                __('Encypher Provenance', 'encypher-provenance'),
                [$this, 'render_classic_meta_box'],
                $post_type,
                'side',
                'default'
            );
        }
    }

    public function render_classic_meta_box($post): void
    {
        wp_nonce_field('encypher_provenance_meta_box', 'encypher_provenance_meta_box_nonce');
        $post_id = (int) $post->ID;
        $details = self::canonical_details_for_post($post_id);
        $status = (string) $details['display_state'];
        $verification_url = get_post_meta($post_id, '_encypher_provenance_verification_url', true);
        if (! $verification_url && 'attachment' === (string) ($post->post_type ?? '')) {
            $verification_url = get_post_meta($post_id, '_encypher_image_manifest_url', true);
        }
        ?>
        <div class="encypher-provenance-classic" data-post-id="<?php echo esc_attr($post_id); ?>" data-post-type="<?php echo esc_attr((string) ($post->post_type ?? '')); ?>" data-encypher-display-state="<?php echo esc_attr($status); ?>" data-encypher-assurance-state="<?php echo esc_attr((string) $details['assurance_state']); ?>">
            <p class="status-row">
                <strong><?php esc_html_e('Status:', 'encypher-provenance'); ?></strong>
                <span class="status-value" data-status="<?php echo esc_attr($status); ?>"><?php echo esc_html($this->classic_status_label($status)); ?></span>
            </p>
            <p class="verification-link"<?php echo $verification_url ? '' : ' style="display:none;"'; ?>>
                <a href="<?php echo $verification_url ? esc_url($verification_url) : '#'; ?>" target="_blank" rel="noopener">
                    <?php esc_html_e('View provenance report', 'encypher-provenance'); ?>
                </a>
            </p>
            <p class="encypher-classic-actions">
                <a href="#" class="button encypher-provenance-verify" data-post-id="<?php echo esc_attr($post_id); ?>">
                    <?php esc_html_e('Verify', 'encypher-provenance'); ?>
                </a>
                <button type="button" class="button encypher-provenance-retry" data-post-id="<?php echo esc_attr($post_id); ?>">
                    <?php esc_html_e('Retry signing', 'encypher-provenance'); ?>
                </button>
            </p>
            <p class="status-message" aria-live="polite"></p>
        </div>
        <?php
    }

    /**
     * Human-readable, tier-independent status label for the Classic meta box,
     * mirroring the Gutenberg status hero. Canonical status values only.
     */
    private function classic_status_label(string $status): string
    {
        $labels = [
            'not_signed'      => __('Not yet signed', 'encypher-provenance'),
            'signing'         => __('Signing', 'encypher-provenance'),
            'signed'          => __('Signed', 'encypher-provenance'),
            'verified'        => __('Verified', 'encypher-provenance'),
            'edited'          => __('Edited', 'encypher-provenance'),
            'needs_attention' => __('Needs attention', 'encypher-provenance'),
        ];

        return $labels[$status] ?? $labels['not_signed'];
    }

    /**
     * Resolve the canonical authoring state shared by both editors.
     */
    private function classic_status_state(int $post_id): string
    {
        return (string) self::canonical_details_for_post($post_id)['display_state'];
    }

    /**
     * Auto-mark content when publishing a post.
     *
     * @param int $post_id Post ID
     * @param \WP_Post $post Post object
     */
    public function auto_mark_on_publish(int $post_id, $post): void
    {
        // Check if auto-mark is enabled
        $settings = get_option('encypher_provenance_settings', []);
        if (empty($settings['auto_mark_on_publish'])) {
            return;
        }

        // Check if post type is enabled
        if (!in_array($post->post_type, $settings['post_types'] ?? ['post', 'page'], true)) {
            return;
        }

        // Check per-post override
        if (get_post_meta($post_id, '_encypher_skip_marking', true)) {
            return;
        }

        // Check if already marked (avoid double-marking on publish)
        if (get_post_meta($post_id, '_encypher_marked', true)) {
            return;
        }

        // Mark the content
        $this->mark_post_content($post_id, $post, 'c2pa.created');
    }

    /**
     * Auto-mark content when updating a post.
     *
     * @param int $post_id Post ID
     * @param \WP_Post $post_after Post object after update
     * @param \WP_Post $post_before Post object before update
     */
    public function auto_mark_on_update(int $post_id, $post_after, $post_before): void
    {
        // Only process published posts
        if ($post_after->post_status !== 'publish') {
            return;
        }

        // Don't trigger on initial publish (status change from non-publish to publish)
        // The auto_mark_on_publish hook will handle this
        if ($post_before->post_status !== 'publish') {
            return;
        }

        // Check if auto-mark on update is enabled
        $settings = get_option('encypher_provenance_settings', []);
        if (empty($settings['auto_mark_on_update'])) {
            return;
        }

        // Check if post type is enabled
        if (!in_array($post_after->post_type, $settings['post_types'] ?? ['post', 'page'], true)) {
            return;
        }

        // Check per-post override
        if (get_post_meta($post_id, '_encypher_skip_marking', true)) {
            return;
        }

        // Check if content changed
        if ($post_after->post_content === $post_before->post_content) {
            return;
        }

        // Check if already marked
        $is_marked = get_post_meta($post_id, '_encypher_marked', true);

        // Re-mark with edit action
        $action = $is_marked ? 'c2pa.edited' : 'c2pa.created';
        $this->mark_post_content($post_id, $post_after, $action);
    }

    /**
     * Mark post content with C2PA manifest.
     *
     * @param int $post_id Post ID
     * @param \WP_Post $post Post object
     * @param string $action C2PA action type (c2pa.created or c2pa.edited)
     */
    private function mark_post_content(int $post_id, $post, string $action): void
    {
        // This will be implemented to call the REST API endpoint
        // For now, we'll set a flag to indicate marking is needed
        update_post_meta($post_id, '_encypher_needs_marking', true);
        update_post_meta($post_id, '_encypher_action_type', $action);

        // Log for debugging
        Logger::log(sprintf(
            'Encypher: Post %d needs marking with action %s',
            $post_id,
            $action
        ));
    }

    /**
     * Render auto-mark on publish field.
     */
    public function render_auto_mark_on_publish_field(): void
    {
        $options = get_option('encypher_provenance_settings', []);
        $checked = isset($options['auto_mark_on_publish']) ? (bool) $options['auto_mark_on_publish'] : true;
        ?>
        <label>
            <input type="checkbox" name="encypher_provenance_settings[auto_mark_on_publish]" value="1" <?php checked($checked); ?> />
            <?php esc_html_e('Automatically embed C2PA manifests when publishing new posts', 'encypher-provenance'); ?>
        </label>
        <p class="description"><?php esc_html_e('Recommended for all users. Ensures every published post has cryptographic proof of origin.', 'encypher-provenance'); ?></p>
        <?php
    }

    /**
     * Render auto-mark on update field.
     */
    public function render_auto_mark_on_update_field(): void
    {
        $options = get_option('encypher_provenance_settings', []);
        $checked = isset($options['auto_mark_on_update']) ? (bool) $options['auto_mark_on_update'] : true;
        ?>
        <label>
            <input type="checkbox" name="encypher_provenance_settings[auto_mark_on_update]" value="1" <?php checked($checked); ?> />
            <?php esc_html_e('Re-sign manifests when content is updated', 'encypher-provenance'); ?>
        </label>
        <p class="description"><?php esc_html_e('Preserves provenance chain through ingredient references. Uses c2pa.edited action.', 'encypher-provenance'); ?></p>
        <?php
    }

    public function render_protected_publish_field(): void
    {
        $options = get_option('encypher_provenance_settings', []);
        $checked = ! is_array($options)
            || ! array_key_exists('protected_publish_enabled', $options)
            || ! empty($options['protected_publish_enabled']);
        ?>
        <p class="encypher-protected-mode-current <?php echo $checked ? 'is-protected' : 'is-unprotected'; ?>" role="status">
            <strong>
                <?php echo esc_html($checked
                    ? __('Current mode: Protected publishing (fail closed)', 'encypher-provenance')
                    : __('Current mode: Standard WordPress publishing (fail open)', 'encypher-provenance')); ?>
            </strong>
        </p>
        <label>
            <input type="checkbox" name="encypher_provenance_settings[protected_publish_enabled]" value="1" <?php checked($checked); ?> />
            <?php esc_html_e('Hold publication until signing succeeds', 'encypher-provenance'); ?>
        </label>
        <p class="description"><?php esc_html_e('When protected publishing is on, new content stays non-public until its exact title, excerpt, and body are signed. Updates keep the prior signed version live. A signing failure keeps the candidate held and requires retry or cancel; Encypher never releases it unsigned.', 'encypher-provenance'); ?></p>
        <?php if (! $checked) : ?>
            <p class="notice notice-warning inline"><?php esc_html_e('Protection is off. WordPress can publish new or changed content even when signing fails.', 'encypher-provenance'); ?></p>
        <?php endif; ?>
        <?php
    }

    public function render_protected_publish_roles_field(): void
    {
        $options = get_option('encypher_provenance_settings', []);
        $selected = is_array($options) && isset($options['protected_publish_roles'])
            ? (array) $options['protected_publish_roles']
            : [];
        $roles = function_exists('wp_roles') ? wp_roles()->roles : [];
        ?>
        <fieldset>
            <?php foreach ($roles as $slug => $role) : ?>
                <label class="encypher-policy-option">
                    <input type="checkbox" name="encypher_provenance_settings[protected_publish_roles][]" value="<?php echo esc_attr((string) $slug); ?>" <?php checked(in_array((string) $slug, $selected, true)); ?> />
                    <?php echo esc_html((string) ($role['name'] ?? $slug)); ?>
                </label>
            <?php endforeach; ?>
        </fieldset>
        <p class="description">
            <?php echo esc_html(empty($selected)
                ? __('Current role policy: every eligible publisher is protected.', 'encypher-provenance')
                : __('Current role policy: only the selected publishing roles are protected.', 'encypher-provenance')); ?>
        </p>
        <p class="description"><?php esc_html_e('Roles limit who enters protected publishing. Leave every role clear to protect all eligible publishers.', 'encypher-provenance'); ?></p>
        <?php
    }

    public function render_protected_publish_terms_field(): void
    {
        $options = get_option('encypher_provenance_settings', []);
        $policy = is_array($options) && isset($options['protected_publish_terms']) && is_array($options['protected_publish_terms'])
            ? $options['protected_publish_terms']
            : [];
        $lines = [];
        foreach ($policy as $taxonomy => $slugs) {
            if (is_array($slugs) && ! empty($slugs)) {
                $lines[] = sanitize_key((string) $taxonomy) . ':' . implode(',', array_map('sanitize_title', $slugs));
            }
        }
        ?>
        <textarea class="large-text code" rows="3" name="encypher_provenance_settings[protected_publish_terms_input]" placeholder="category:news,investigations"><?php echo esc_textarea(implode("\n", $lines)); ?></textarea>
        <p class="description">
            <?php echo esc_html(empty($policy)
                ? __('Current desk policy: every desk is protected.', 'encypher-provenance')
                : __('Current desk policy: only content matching a listed taxonomy term is protected.', 'encypher-provenance')); ?>
        </p>
        <p class="description"><?php esc_html_e('Optional desk policy, one taxonomy:slug,slug rule per line. Leave blank to protect every desk.', 'encypher-provenance'); ?></p>
        <?php
    }

    public function render_non_production_signing_field(): void
    {
        $options = get_option('encypher_provenance_settings', []);
        $checked = is_array($options) && ! empty($options['allow_non_production_signing']);
        $status = EnvironmentGuard::status();
        ?>
        <label>
            <input type="checkbox" name="encypher_provenance_settings[allow_non_production_signing]" value="1" <?php checked($checked); ?> />
            <?php esc_html_e('Allow signing in this non-production environment', 'encypher-provenance'); ?>
        </label>
        <p class="description"><?php echo esc_html(sprintf(
            /* translators: %s: WordPress environment type. */
            __('Current environment: %s. Keep this off on database clones.', 'encypher-provenance'),
            (string) ($status['environment'] ?? 'production')
        )); ?></p>
        <?php
    }


    /**
     * Active IPTC digitalsourcetype terms (cv.iptc.org, release 2024-10-23)
     * offered in the settings picker. Keyed by full URI; values are UI labels.
     * Retired terms (digitalArt, minorHumanEdits, softwareImage) are excluded.
     */
    public const DIGITAL_SOURCE_TYPES = [
        'http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation' => 'Digital creation (human, non-generative tools)',
        'http://cv.iptc.org/newscodes/digitalsourcetype/humanEdits' => 'Human-edited media',
        'http://cv.iptc.org/newscodes/digitalsourcetype/digitalCapture' => 'Digital capture (camera / recorder)',
        'http://cv.iptc.org/newscodes/digitalsourcetype/computationalCapture' => 'Computational capture (e.g. smartphone HDR)',
        'http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia' => 'Created using Generative AI',
        'http://cv.iptc.org/newscodes/digitalsourcetype/compositeWithTrainedAlgorithmicMedia' => 'Edited using Generative AI',
        'http://cv.iptc.org/newscodes/digitalsourcetype/composite' => 'Composite of elements (mixed sources)',
        'http://cv.iptc.org/newscodes/digitalsourcetype/compositeCapture' => 'Composite of captured elements',
        'http://cv.iptc.org/newscodes/digitalsourcetype/compositeSynthetic' => 'Composite including generative AI elements',
        'http://cv.iptc.org/newscodes/digitalsourcetype/algorithmicallyEnhanced' => 'Algorithmically altered (sharpening, noise reduction)',
        'http://cv.iptc.org/newscodes/digitalsourcetype/algorithmicMedia' => 'Pure algorithmic media (no training data)',
        'http://cv.iptc.org/newscodes/digitalsourcetype/dataDrivenMedia' => 'Data-driven media',
        'http://cv.iptc.org/newscodes/digitalsourcetype/screenCapture' => 'Screen capture',
        'http://cv.iptc.org/newscodes/digitalsourcetype/virtualRecording' => 'Virtual event recording',
        'http://cv.iptc.org/newscodes/digitalsourcetype/negativeFilm' => 'Digitised from negative film',
        'http://cv.iptc.org/newscodes/digitalsourcetype/positiveFilm' => 'Digitised from transparency',
        'http://cv.iptc.org/newscodes/digitalsourcetype/print' => 'Digitised from print',
    ];


    public function render_auto_sign_images_field(): void
    {
        $options = get_option('encypher_provenance_settings', []);
        $checked = isset($options['auto_sign_images']) ? (bool) $options['auto_sign_images'] : true;
        ?>
        <label>
            <input type="checkbox" name="encypher_provenance_settings[auto_sign_images]" value="1" <?php checked($checked); ?> />
            <?php esc_html_e('Sign featured images with C2PA when signing post content', 'encypher-provenance'); ?>
        </label>
        <p class="description"><?php esc_html_e('Uses the /sign/media endpoint to embed a C2PA manifest in the featured image file.', 'encypher-provenance'); ?></p>
        <?php
    }

    public function render_local_hashing_field(): void
    {
        $options = get_option('encypher_provenance_settings', []);
        $entitled = Capabilities::allows(Capabilities::LOCAL_HASH_SIGNING);
        $enabled = $entitled && ! empty($options['local_hashing']);
        ?>
        <label for="encypher-local-hashing" class="screen-reader-text">
            <?php esc_html_e('Media signing method', 'encypher-provenance'); ?>
        </label>
        <select
            id="encypher-local-hashing"
            name="encypher_provenance_settings[local_hashing]"
            aria-describedby="encypher-local-hashing-description"
        >
            <option value="1" <?php selected($enabled); ?> <?php disabled(! $entitled); ?>><?php esc_html_e('Local hashing (requires Encypher entitlement)', 'encypher-provenance'); ?></option>
            <option value="0" <?php selected(! $enabled); ?>><?php esc_html_e('Secure server hashing', 'encypher-provenance'); ?></option>
        </select>
        <p id="encypher-local-hashing-description" class="description"><?php esc_html_e('Local hashing computes the media digest on this WordPress server and sends only the data needed to sign supported assets. Secure server hashing sends the media file to Encypher over HTTPS. If local hashing is unavailable for the file or plan, full-upload signing remains the fallback while full media uploads are allowed.', 'encypher-provenance'); ?></p>
        <?php
    }

    public function render_allow_full_asset_upload_field(): void
    {
        $options = get_option('encypher_provenance_settings', []);
        $enabled = isset($options['allow_full_asset_upload']) ? (bool) $options['allow_full_asset_upload'] : true;
        $local_hash_entitled = Capabilities::allows(Capabilities::LOCAL_HASH_SIGNING);
        ?>
        <label for="encypher-allow-full-asset-upload">
            <input
                type="checkbox"
                id="encypher-allow-full-asset-upload"
                name="encypher_provenance_settings[allow_full_asset_upload]"
                value="1"
                aria-describedby="encypher-allow-full-asset-upload-description"
                <?php checked($enabled); ?>
            />
            <?php esc_html_e('Allow full media assets to be sent to Encypher', 'encypher-provenance'); ?>
        </label>
        <p id="encypher-allow-full-asset-upload-description" class="description"><?php esc_html_e('Turn this off to prevent media payload egress. Text signing remains available.', 'encypher-provenance'); ?></p>
        <?php if (! $enabled && ! $local_hash_entitled) : ?>
            <p class="notice notice-warning inline"><?php esc_html_e('Your current plan cannot use local hash signing. With full asset uploads off, media will not be signed.', 'encypher-provenance'); ?></p>
        <?php endif; ?>
        <?php
    }


    /**
     * Render post types field.
     */
    public function render_post_types_field(): void
    {
        $options = get_option('encypher_provenance_settings', []);
        $selected_types = isset($options['post_types']) ? (array) $options['post_types'] : ['post', 'page'];
        $post_types = function_exists('get_post_types')
            ? get_post_types(['public' => true], 'objects')
            : [];
        if (empty($post_types)) {
            $post_types = [];
            foreach (array_values(array_unique(array_merge(['post', 'page'], $selected_types))) as $slug) {
                $post_types[$slug] = (object) ['name' => $slug, 'label' => ucfirst((string) $slug)];
            }
        }
        ?>
        <fieldset>
            <?php foreach ($post_types as $post_type): ?>
                <label style="display:block; margin-bottom:5px;">
                    <input type="checkbox"
                           name="encypher_provenance_settings[post_types][]"
                           value="<?php echo esc_attr($post_type->name); ?>"
                           <?php checked(in_array($post_type->name, $selected_types, true)); ?> />
                    <?php echo esc_html($post_type->label); ?>
                </label>
            <?php endforeach; ?>
        </fieldset>
        <p class="description"><?php esc_html_e('Select which post types should be automatically marked with C2PA manifests.', 'encypher-provenance'); ?></p>
        <?php
    }


    public function render_show_badge_field(): void
    {
        $options = get_option('encypher_provenance_settings', []);
        $can_customize_badge = self::allows_whitelabel();
        // Reader branding is fixed unless the current server projection grants
        // white_label. sanitize_settings enforces the same rule on submission.
        $checked = ! $can_customize_badge
            ? true
            : (isset($options['show_badge']) ? (bool) $options['show_badge'] : true);
        ?>
        <label for="encypher-show-badge">
            <input id="encypher-show-badge" type="checkbox" name="encypher_provenance_settings[show_badge]" value="1" <?php checked($checked); ?> <?php disabled(! $can_customize_badge); ?> />
            <?php esc_html_e('Display a provenance badge on signed content', 'encypher-provenance'); ?>
        </label>
        <?php if (! $can_customize_badge) : ?>
            <p class="description"><?php esc_html_e('Encypher has not enabled reader branding controls for this account.', 'encypher-provenance'); ?></p>
        <?php else : ?>
            <p class="description"><?php esc_html_e('The badge is shown only when signed provenance exists.', 'encypher-provenance'); ?></p>
        <?php endif; ?>
        <?php
    }

    /**
     * Render badge position field.
     */
    public function render_badge_position_field(): void
    {
        $options = get_option('encypher_provenance_settings', []);
        $value = isset($options['badge_position']) ? (string) $options['badge_position'] : 'bottom';
        $visible = isset($options['show_badge']) ? (bool) $options['show_badge'] : true;
        ?>
        <label class="screen-reader-text" for="encypher-badge-position"><?php esc_html_e('Badge position', 'encypher-provenance'); ?></label>
        <select id="encypher-badge-position" name="encypher_provenance_settings[badge_position]" <?php disabled(! $visible); ?>>
            <option value="bottom" <?php selected($value, 'bottom'); ?>><?php esc_html_e('Bottom of content', 'encypher-provenance'); ?></option>
            <option value="top" <?php selected($value, 'top'); ?>><?php esc_html_e('Top of content', 'encypher-provenance'); ?></option>
            <option value="bottom-right" <?php selected($value, 'bottom-right'); ?>><?php esc_html_e('Bottom-right floating badge', 'encypher-provenance'); ?></option>
        </select>
        <?php
    }

    /**
     * Render the reader toggle for provenance info marks on media.
     */
    public function render_media_badges_field(): void
    {
        $options = get_option('encypher_provenance_settings', []);
        $enabled = isset($options['media_badges']) ? (bool) $options['media_badges'] : true;
        ?>
        <label for="encypher-media-badges">
            <input id="encypher-media-badges" type="checkbox" name="encypher_provenance_settings[media_badges]" value="1" <?php checked($enabled); ?> />
            <?php esc_html_e('Show clickable Encypher info marks on images, video, and audio', 'encypher-provenance'); ?>
        </label>
        <p class="description"><?php esc_html_e('Adds a small corner mark to provenance-bearing media. Readers click it to check that exact asset in the shared verification dialog. The page markup is never changed.', 'encypher-provenance'); ?></p>
        <?php
    }

    /**
     * Render the enterprise brand-logo media picker (attachment id only).
     */
    public function render_brand_logo_field(): void
    {
        $options  = get_option('encypher_provenance_settings', []);
        $logo_id  = isset($options['brand_logo_id']) ? (int) $options['brand_logo_id'] : 0;
        $logo_url = $logo_id > 0 && function_exists('wp_get_attachment_image_url')
            ? (string) wp_get_attachment_image_url($logo_id, 'medium')
            : '';
        ?>
        <div class="encypher-brand-logo-field" data-has-logo="<?php echo $logo_id > 0 ? '1' : '0'; ?>">
            <input type="hidden" id="encypher-brand-logo-id" name="encypher_provenance_settings[brand_logo_id]" value="<?php echo esc_attr((string) $logo_id); ?>" />
            <img class="encypher-brand-logo-preview" src="<?php echo esc_url($logo_url); ?>" alt="" style="<?php echo '' === $logo_url ? 'display:none;' : ''; ?>max-width:160px;height:auto;display:block;margin-bottom:8px;" />
            <button type="button" class="button encypher-brand-logo-select"><?php esc_html_e('Select logo', 'encypher-provenance'); ?></button>
            <button type="button" class="button-link encypher-brand-logo-remove" style="<?php echo $logo_id > 0 ? '' : 'display:none;'; ?>margin-left:8px;"><?php esc_html_e('Remove', 'encypher-provenance'); ?></button>
            <p class="description"><?php esc_html_e('Shown on the reader badge and verification dialog instead of the Encypher mark. PNG, JPG, or WebP only.', 'encypher-provenance'); ?></p>
        </div>
        <?php
    }

    /**
     * Render the "Powered by Encypher" link toggle (enterprise only).
     */
    public function render_powered_by_encypher_field(): void
    {
        $options = get_option('encypher_provenance_settings', []);
        $enabled = isset($options['powered_by_encypher']) ? (bool) $options['powered_by_encypher'] : true;
        ?>
        <label for="encypher-powered-by">
            <input id="encypher-powered-by" type="checkbox" name="encypher_provenance_settings[powered_by_encypher]" value="1" <?php checked($enabled); ?> />
            <?php esc_html_e('Show a small "Powered by Encypher" link in the verification dialog', 'encypher-provenance'); ?>
        </label>
        <?php
    }


    /**
     * Render WordPress/ai integration enabled field.
     */
    public function render_wordpress_ai_enabled_field(): void
    {
        $options = get_option('encypher_provenance_settings', []);
        $enabled = isset($options['wordpress_ai_enabled']) ? (bool) $options['wordpress_ai_enabled'] : false;
        $wpai_active = class_exists('WP_AI\\Plugin') || class_exists('WordPressAI\\Plugin') || class_exists('WP_AI_Experiments\\Plugin')
            || function_exists('wp_register_ability') || function_exists('wp_do_ability');
        ?>
        <label>
            <input type="checkbox" name="encypher_provenance_settings[wordpress_ai_enabled]" value="1" <?php checked($enabled, true); ?> />
            <?php esc_html_e('Sign AI-generated content with Encypher C2PA provenance', 'encypher-provenance'); ?>
        </label>
        <?php if (!$wpai_active): ?>
        <p class="description" style="color:#856404;">
            <img src="<?php echo esc_url(ENCYPHER_PROVENANCE_PLUGIN_URL . 'assets/images/brand/encypher-mark-navy.svg'); ?>" alt="" width="16" height="16" style="vertical-align:middle;" aria-hidden="true" />
            <?php esc_html_e('The WordPress/ai plugin does not appear to be active. This setting will have no effect until WordPress/ai is installed and activated.', 'encypher-provenance'); ?>
        </p>
        <?php else: ?>
        <p class="description">
            <?php esc_html_e('When enabled, content generated by WordPress/ai experiments (titles, excerpts, summaries, alt text, etc.) will be automatically signed via the Encypher API before being returned to the editor.', 'encypher-provenance'); ?>
        </p>
        <?php endif; ?>
        <?php
    }



    /**
     * Add a single "Provenance" column to native Posts/Pages list tables
     * (manage_posts_columns / manage_pages_columns).
     *
     * @param array $columns Existing columns.
     * @return array Columns with the provenance column appended.
     */
    public function add_content_provenance_column(array $columns): array
    {
        $columns['encypher_provenance'] = __('Provenance', 'encypher-provenance');
        return $columns;
    }

    /**
     * Render the provenance cell for a post/page row
     * (manage_posts_custom_column / manage_pages_custom_column).
     *
     * @param string $column  Current column key.
     * @param int    $post_id Row post ID.
     */
    public function render_content_provenance_column(string $column, int $post_id): void
    {
        if ('encypher_provenance' !== $column) {
            return;
        }

        $details = self::canonical_details_for_post($post_id);
        $display_state = (string) $details['display_state'];
        $assurance_state = (string) $details['assurance_state'];
        $registry = class_exists(PublisherPublication::class)
            ? PublisherPublication::projection($post_id)
            : [];
        $registry = is_array($registry) ? $registry : [];
        $registry_state = (string) ($registry['delivery_state'] ?? '');
        $registry_states = ['pending', 'retry', 'failed', 'published'];
        echo '<span class="encypher-provenance-state" data-encypher-display-state="'
            . esc_attr($display_state) . '" data-encypher-assurance-state="'
            . esc_attr($assurance_state) . '"';
        if (in_array($registry_state, $registry_states, true)) {
            echo ' data-encypher-registry-state="' . esc_attr($registry_state) . '"'
                . ' data-encypher-registry-attempts="' . esc_attr((string) max(0, (int) ($registry['attempts'] ?? 0))) . '"'
                . ' data-encypher-registry-max-attempts="' . esc_attr((string) max(0, (int) ($registry['max_attempts'] ?? 0))) . '"';
        }
        echo '>' . esc_html($this->classic_status_label($display_state)) . '</span>';

        $registry_error = self::safe_failure_text((string) ($registry['error'] ?? ''), 160);
        if ('' !== $registry_error && in_array($registry_state, ['retry', 'failed'], true)) {
            echo '<details class="encypher-registry-error"><summary>'
                . esc_html__('Registry delivery details', 'encypher-provenance')
                . '</summary><code>' . esc_html($registry_error) . '</code></details>';
        }

        $registry_actions = isset($registry['actions']) && is_array($registry['actions'])
            ? $registry['actions']
            : [];
        $can_retry_registry = ! empty($registry['retryable'])
            || ! empty($registry_actions['can_retry_registry']);
        if ($can_retry_registry) {
            echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" class="encypher-registry-retry-form">';
            echo '<input type="hidden" name="action" value="encypher_retry_registry" />';
            echo '<input type="hidden" name="post_id" value="' . esc_attr((string) $post_id) . '" />';
            wp_nonce_field('encypher_retry_registry_' . $post_id);
            echo '<button type="submit" class="button button-small encypher-registry-retry" data-post-id="'
                . esc_attr((string) $post_id) . '">' . esc_html__('Retry registry delivery', 'encypher-provenance') . '</button>';
            echo '</form>';
        }
    }

    /**
     * Render the provenance status filter dropdown above native Posts/Pages
     * list tables (restrict_manage_posts).
     *
     * @param string $post_type Current list-table post type.
     */
    public function render_provenance_list_filter(string $post_type): void
    {
        $settings = get_option('encypher_provenance_settings', []);
        $eligible = isset($settings['post_types']) && is_array($settings['post_types'])
            ? $settings['post_types'] : ['post', 'page'];
        if (! in_array($post_type, $eligible, true)) {
            return;
        }

        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only list filter; narrows the query only, changes no state.
        $current = isset($_GET['encypher_provenance']) ? sanitize_key(wp_unslash($_GET['encypher_provenance'])) : '';
        $options = [
            ''                => __('All provenance states', 'encypher-provenance'),
            'not_signed'      => __('Not yet signed', 'encypher-provenance'),
            'signing'         => __('Signing', 'encypher-provenance'),
            'signed'          => __('Signed', 'encypher-provenance'),
            'verified'        => __('Verified', 'encypher-provenance'),
            'edited'          => __('Edited', 'encypher-provenance'),
            'needs_attention' => __('Needs attention', 'encypher-provenance'),
        ];
        echo '<label class="screen-reader-text" for="encypher_provenance_filter">'
            . esc_html__('Filter by provenance', 'encypher-provenance') . '</label>';
        echo '<select name="encypher_provenance" id="encypher_provenance_filter">';
        foreach ($options as $value => $text) {
            echo '<option value="' . esc_attr($value) . '"' . selected($current, $value, false) . '>'
                . esc_html($text) . '</option>';
        }
        echo '</select>';
    }

    /**
     * Constrain the native list query by the selected provenance filter
     * (pre_get_posts). Admin main query only.
     *
     * @param \WP_Query $query Current query.
     */
    public function filter_list_by_provenance($query): void
    {
        if (! is_admin() || ! ($query instanceof \WP_Query) || ! $query->is_main_query()) {
            return;
        }
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only list filter; narrows the query only, changes no state.
        $filter = isset($_GET['encypher_provenance']) ? sanitize_key(wp_unslash($_GET['encypher_provenance'])) : '';
        if ('' === $filter) {
            return;
        }

        $query_post_type = $query->get('post_type');
        if (is_array($query_post_type)) {
            $query_post_types = array_map('sanitize_key', $query_post_type);
        } else {
            $query_post_type = sanitize_key((string) $query_post_type);
            if ('' === $query_post_type || 'any' === $query_post_type) {
                // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only list scope.
                $query_post_type = isset($_GET['post_type']) ? sanitize_key(wp_unslash($_GET['post_type'])) : 'post';
            }
            $query_post_types = [$query_post_type];
        }
        $settings = get_option('encypher_provenance_settings', []);
        $eligible_types = isset($settings['post_types']) && is_array($settings['post_types'])
            ? array_map('sanitize_key', $settings['post_types'])
            : ['post', 'page'];
        if (! array_intersect($query_post_types, $eligible_types)) {
            return;
        }

        if (! in_array($filter, ['not_signed', 'signing', 'signed', 'verified', 'edited', 'needs_attention'], true)) {
            return;
        }
        $meta_query = $this->provenance_meta_query($filter);

        $existing_meta_query = $query->get('meta_query');
        if (! empty($existing_meta_query)) {
            $meta_query = ['relation' => 'AND', $existing_meta_query, $meta_query];
        }
        $query->set('meta_query', $meta_query);
    }

    /**
     * Add the contextual Sign, Verify, or Retry action to native list rows
     * (post_row_actions / page_row_actions).
     *
     * @param array    $actions Existing row actions.
     * @param \WP_Post $post    Row post object.
     * @return array Row actions with the provenance action appended.
     */
    public function add_provenance_row_action(array $actions, $post): array
    {
        if (! ($post instanceof \WP_Post) || ! current_user_can('edit_post', $post->ID)) {
            return $actions;
        }
        $settings = get_option('encypher_provenance_settings', []);
        $eligible = isset($settings['post_types']) && is_array($settings['post_types'])
            ? $settings['post_types'] : ['post', 'page'];
        if (! in_array($post->post_type, $eligible, true)) {
            return $actions;
        }

        $details = self::canonical_details_for_post((int) $post->ID);
        $display_state = (string) $details['display_state'];
        $verification_url = (string) get_post_meta($post->ID, '_encypher_provenance_verification_url', true);
        $is_signed = in_array($display_state, ['signed', 'verified'], true);
        $needs_attention = in_array($display_state, ['edited', 'needs_attention'], true);
        if ($is_signed && class_exists(PublicationAssurance::class)) {
            $receipt_url = PublicationAssurance::receipt_url((int) $post->ID);
            if ('' !== $receipt_url) {
                $actions['encypher_receipt'] = '<a href="' . esc_url($receipt_url) . '">'
                    . esc_html__('Verification receipt', 'encypher-provenance') . '</a>';
            }
        }
        if ('signing' === $display_state) {
            return $actions;
        }

        if (! $needs_attention && $verification_url && $is_signed) {
            $actions['encypher_verify'] = '<a href="' . esc_url($verification_url) . '">'
                . esc_html__('Verify provenance', 'encypher-provenance') . '</a>';
            return $actions;
        }

        $url = wp_nonce_url(
            admin_url('admin-post.php?action=encypher_sign_single&post=' . $post->ID),
            'encypher_sign_single_' . $post->ID
        );
        $key = ($needs_attention || $is_signed) ? 'encypher_retry' : 'encypher_sign';
        $label = ($needs_attention || $is_signed)
            ? __('Retry signing', 'encypher-provenance')
            : __('Sign with Encypher', 'encypher-provenance');
        $actions[$key] = '<a href="' . esc_url($url) . '">' . esc_html($label) . '</a>';
        return $actions;
    }

    /**
     * Register the "Sign with Encypher" native bulk action
     * (bulk_actions-edit-{type}).
     *
     * @param array $actions Existing bulk actions.
     * @return array Bulk actions with the sign action appended.
     */
    public function add_bulk_sign_action(array $actions): array
    {
        $actions['encypher_sign'] = __('Sign with Encypher', 'encypher-provenance');
        return $actions;
    }

    /**
     * Handle the "Sign with Encypher" bulk action
     * (handle_bulk_actions-edit-{type}).
     *
     * Enqueues each capable post onto the existing WP-Cron deferred-sign queue
     * (the same 'encypher_deferred_sign' path used by publish/update/retry), so
     * there is no second signing pipeline.
     *
     * @param string $redirect_to Redirect URL WordPress will send after the action.
     * @param string $doaction    The bulk action key.
     * @param array  $post_ids    Selected post IDs.
     * @return string Redirect URL with the queued count appended.
     */
    public function handle_bulk_sign_action(string $redirect_to, string $doaction, array $post_ids): string
    {
        if ('encypher_sign' !== $doaction) {
            return $redirect_to;
        }

        $count = 0;
        $user_id = get_current_user_id();
        foreach ($post_ids as $raw_id) {
            $post_id = (int) $raw_id;
            if (! $post_id || ! current_user_can('edit_post', $post_id)) {
                continue;
            }
            if ($this->schedule_deferred_sign($post_id, $user_id)) {
                $count++;
            }
        }

        return add_query_arg('encypher_signed', $count, $redirect_to);
    }

    /**
     * Handle the single-row "Sign with Encypher" action from a native list row
     * (admin_post_encypher_sign_single). Nonce + capability protected.
     */
    public function handle_single_sign_action(): void
    {
        $post_id = isset($_GET['post']) ? (int) $_GET['post'] : 0;
        if (! $post_id || ! current_user_can('edit_post', $post_id)) {
            wp_die(esc_html__('You are not allowed to sign this content.', 'encypher-provenance'));
        }
        check_admin_referer('encypher_sign_single_' . $post_id);

        $this->schedule_deferred_sign($post_id, get_current_user_id());

        $referer = wp_get_referer();
        $redirect = $referer ? $referer : admin_url('edit.php');
        wp_safe_redirect(add_query_arg('encypher_signed', 1, $redirect));
        exit;
    }

    /**
     * Queue a post onto the existing WP-Cron deferred-sign path. Returns true
     * when a run is scheduled (or already pending) for the post.
     *
     * @param int $post_id Post to sign.
     * @param int $user_id Acting user impersonated by the deferred run.
     * @return bool
     */
    private function schedule_deferred_sign(int $post_id, int $user_id): bool
    {
        $document_id = get_post_meta($post_id, '_encypher_provenance_document_id', true);
        $status = (string) get_post_meta($post_id, '_encypher_provenance_status', true);
        $is_update = ! empty($document_id)
            || in_array($status, ['c2pa_protected', 'c2pa_verified', 'signed', 'verified', 'modified'], true);
        $args = [$post_id, $is_update, $user_id];
        if (! wp_next_scheduled('encypher_deferred_sign', $args)) {
            wp_schedule_single_event(time(), 'encypher_deferred_sign', $args);
        }
        delete_post_meta($post_id, '_encypher_last_sign_error');
        SigningStatus::acquire($post_id, 'admin-queue');
        return true;
    }

    /**
     * Show a success notice after a "Sign with Encypher" action queued one or
     * more posts for signing (admin_notices).
     */
    public function render_bulk_sign_notice(): void
    {
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only redirect flag written by the queue handlers; displays a count only.
        if (! isset($_GET['encypher_signed'])) {
            return;
        }
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only redirect flag.
        $count = (int) $_GET['encypher_signed'];
        if ($count < 1) {
            return;
        }
        printf(
            '<div class="notice notice-success is-dismissible"><p>%s</p></div>',
            esc_html(sprintf(
                /* translators: %d: number of items queued for signing. */
                _n('%d item queued for signing with Encypher.', '%d items queued for signing with Encypher.', $count, 'encypher-provenance'),
                $count
            ))
        );
    }
}
