<?php
namespace EncypherProvenance;

if (! defined('ABSPATH')) {
    exit;
}
require_once __DIR__ . '/class-encypher-vip-media-delivery.php';
require_once __DIR__ . '/class-encypher-rsl-export.php';
require_once __DIR__ . '/class-encypher-reader-verdict.php';
require_once __DIR__ . '/class-encypher-capabilities.php';

/**
 * Handles frontend display of C2PA badges and verification links.
 *
 * Provides optional badges on posts to indicate C2PA protection
 * and links for public verification.
 */
class Frontend
{

    /**
     * Register hooks for frontend display.
     */
    public function register_hooks(): void
    {
        // wptexturize asks this core filter once per request before rewriting
        // punctuation. Keep it enabled everywhere except a singular post whose
        // current signed snapshot must remain byte-stable.
        add_filter('run_wptexturize', [$this, 'allow_wptexturize'], 10, 1);

        // WS1: single final-priority render pipeline closes the mutation
        // window within each the_content pass.
        add_filter('the_content', [$this, 'render_provenance_output'], PHP_INT_MAX);
        add_action('wp_enqueue_scripts', [$this, 'enqueue_frontend_assets']);
    }

    /**
     * Control WordPress smart punctuation for byte-bound signed content.
     */
    public function allow_wptexturize(bool $run_texturize): bool
    {
        if (! $run_texturize || ! is_singular()) {
            return $run_texturize;
        }

        $post_id = (int) get_queried_object_id();
        return $post_id <= 0 || ! RslExport::has_current_committed_text_provenance($post_id);
    }


    /**
     * Final-priority render pipeline: inject the signed snapshot, then append
     * the live-delivery marker and the badge to the SAME result.
     *
     * Order inside this method preserves the old 12/20 invariant: replace
     * third-party-mutated content with the signed snapshot first. VIP media
     * URL selection is then constrained to byte-identical originals without
     * changing visible text or its provenance markers. The marker and badge
     * wrappers land outside the signed region. Unsigned or stale posts pass
     * through injection unchanged and still receive the badge per
     * maybe_add_c2pa_badge().
     *
     * @param string $content Content after every other the_content filter.
     * @return string Final provenance-bearing output.
     */
    public function render_provenance_output(string $content): string
    {
        $content = $this->inject_provenance_markers($content);
        $content = VipMediaDelivery::rewrite_signed_image_tags($content);
        $content = $this->append_live_delivery_marker($content);

        return $this->maybe_add_c2pa_badge($content);
    }

    /**
     * Append the invariant live-delivery marker.
     *
     * The delivery check must be able to confirm from the served document that
     * this exact post's current signed generation is what went out. It may NOT
     * depend on the reader badge: the badge is an optional presentation control
     * a publisher can switch off, and switching off a badge is not a delivery
     * failure. So the marker renders on the same predicate that authorizes
     * serving the signed snapshot, regardless of show_badge or media_badges.
     *
     * The marker is evidence, not UI. It carries no verdict, no trust language,
     * and no label; it is hidden, out of the accessibility tree, and not
     * focusable or interactive. It never renders for unsigned content, for a
     * signing run still in flight, or for a stale generation, because the
     * currentness predicate is false in all three cases.
     *
     * @param string $content Rendered content.
     * @return string Content with the marker appended, or unchanged.
     */
    private function append_live_delivery_marker(string $content): string
    {
        if (! is_singular() || ! in_the_loop() || ! is_main_query()) {
            return $content;
        }

        $post_id = (int) get_the_ID();
        if ($post_id <= 0 || ! RslExport::has_current_committed_text_provenance($post_id)) {
            return $content;
        }

        $signing_generation = (string) get_post_meta($post_id, '_encypher_provenance_instance_id', true);
        if ('' === $signing_generation) {
            return $content;
        }

        // Omitted rather than emitted empty: a blank attribute could be read as
        // a matched generation by a checker comparing string equality.
        $publication_generation = class_exists(PublicationAssurance::class)
            && method_exists(PublicationAssurance::class, 'publication_generation')
                ? (string) PublicationAssurance::publication_generation($post_id)
                : '';

        $marker = '<span class="encypher-provenance-live"'
            . ' data-encypher-live-post="' . esc_attr((string) $post_id) . '"'
            . ' data-encypher-live-signing-generation="' . esc_attr($signing_generation) . '"';
        if ('' !== $publication_generation) {
            $marker .= ' data-encypher-live-publication-generation="' . esc_attr($publication_generation) . '"';
        }
        $marker .= ' aria-hidden="true" hidden style="display:none"></span>';

        return $content . $marker;
    }

    /**
     * Enqueue frontend assets.
     */
    public function enqueue_frontend_assets(): void
    {
        if (! is_singular()) {
            return;
        }

        $css_path = dirname(__DIR__) . '/assets/css/frontend.css';
        $js_path = dirname(__DIR__) . '/assets/js/frontend.js';
        $css_version = defined('WP_DEBUG') && WP_DEBUG && is_file($css_path)
            ? (string) filemtime($css_path)
            : ENCYPHER_PROVENANCE_VERSION;
        $js_version = defined('WP_DEBUG') && WP_DEBUG && is_file($js_path)
            ? (string) filemtime($js_path)
            : ENCYPHER_PROVENANCE_VERSION;

        wp_enqueue_style(
            'encypher-frontend',
            ENCYPHER_PROVENANCE_PLUGIN_URL . 'assets/css/frontend.css',
            [],
            $css_version
        );

        wp_enqueue_script(
            'encypher-frontend',
            ENCYPHER_PROVENANCE_PLUGIN_URL . 'assets/js/frontend.js',
            [],
            $js_version,
            true
        );

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

        $config = [
            'restUrl'      => esc_url_raw(rest_url('encypher-provenance/v1/')),
            'nonce'        => wp_create_nonce('wp_rest'),
            'overlayLabel' => __('View content provenance', 'encypher-provenance'),
            'toolIdentity' => 'WordPress/Encypher Plugin v' . ENCYPHER_PROVENANCE_VERSION,
            'brandLogoUrl' => $this->resolve_brand_logo_url($settings),
            'markUrl'      => ENCYPHER_PROVENANCE_PLUGIN_URL . 'assets/images/brand/encypher-mark-navy.svg',
            'loaderUrl'    => ENCYPHER_PROVENANCE_PLUGIN_URL . 'assets/images/brand/encypher-loader-navy.svg',
            // ONE label table for the whole reader surface. The browser renders
            // the same translated words and the same glyph the PHP badge did,
            // from the same source, so a client-side update can never disagree
            // with the server-rendered control it replaces.
            'verdicts'     => ReaderVerdict::localized_table(),
            'reasons'      => ReaderVerdict::reason_copy(),
            'identity'     => ReaderVerdict::localized_identity_table(),
            'labels'       => [
                'retry'          => __('Retry verification', 'encypher-provenance'),
                'viewProvenance' => __('View provenance', 'encypher-provenance'),
                'toolUsed'       => __('Tool used', 'encypher-provenance'),
                'signedAt'       => __('Signed', 'encypher-provenance'),
                'standard'       => __('Technical standard', 'encypher-provenance'),
                'documentId'     => __('C2PA document', 'encypher-provenance'),
                'cached'         => __('Check', 'encypher-provenance'),
                'cachedValue'    => __('Cached verification result', 'encypher-provenance'),
                'rightsHeading'  => __('Rights and permissions', 'encypher-provenance'),
                'chainHeading'   => __('Edit history', 'encypher-provenance'),
                'license'        => __('License', 'encypher-provenance'),
                'usageTerms'     => __('Usage terms', 'encypher-provenance'),
                'licenseTerms'   => __('License terms', 'encypher-provenance'),
                'copyrightHolder' => __('Copyright holder', 'encypher-provenance'),
                'aiTraining'     => __('AI training', 'encypher-provenance'),
                'aiInference'    => __('AI inference', 'encypher-provenance'),
                'dataMining'     => __('Data mining', 'encypher-provenance'),
                'allowed'        => __('Allowed', 'encypher-provenance'),
                'notAllowed'     => __('Not allowed', 'encypher-provenance'),
                'sourceType'     => __('Source type', 'encypher-provenance'),
                'ingredients'    => __('Built from', 'encypher-provenance'),
            ],
            'mediaBadges'  => false,
            'signedMedia'  => [],
        ];

        // Optional reader-facing media verification marks. These are a
        // CLIENT-SIDE overlay only: the served the_content markup is never
        // mutated (the signed region must stay byte-exact). When enabled and
        // the post references provenance-bearing image, audio, or video files
        // that STILL have a resolvable claim, ship their canonical delivery
        // URLs so the script can position clickable marks.
        $post_id = function_exists('get_queried_object_id') ? (int) get_queried_object_id() : 0;
        if (! empty($settings['media_badges']) && $post_id > 0) {
            $signed_media = $this->get_signed_media_payload($post_id);
            if (! empty($signed_media)) {
                $config['mediaBadges'] = true;
                $config['signedMedia'] = $signed_media;
                $config['postId']      = $post_id;
                $this->add_verification_modal($post_id);
            }
        }

        wp_localize_script('encypher-frontend', 'EncypherProvenanceFrontend', $config);
    }

    /**
     * WS1: Render-time injection of provenance markers.
     *
     * Invoked from render_provenance_output() (single the_content filter at
     * PHP_INT_MAX), never registered as its own filter.
     *
     * The editor stores CLEAN (marker-free) content. On the public page we
     * replace the rendered body with the marker-bearing signed text from the
     * side store (`_encypher_signed_text`), so the served HTML carries the
     * single C2PA wrapper + VS256-RS granular markers and copy-paste from the
     * live site preserves provenance — without ever putting invisible bytes in
     * the editor. The visible text is identical (markers are invisible), so the
     * canonical text the C2PA manifest hard-binds is unchanged.
     *
     * @param string $content Clean post content.
     * @return string Marker-bearing content for the public render, or the
     *                clean content unchanged when no signed text is stored.
     */
    public function inject_provenance_markers(string $content): string
    {
        if (! is_singular() || ! in_the_loop() || ! is_main_query()) {
            return $content;
        }

        $post_id = get_the_ID();
        if (! $post_id) {
            return $content;
        }

        if (! RslExport::has_current_committed_text_provenance((int) $post_id)) {
            return $content;
        }

        $signed_text = get_post_meta($post_id, '_encypher_signed_text', true);
        if (! is_string($signed_text) || '' === $signed_text) {
            // No side-stored signed text (e.g. signed before WS1, or text
            // embedding failed under WS0.3 fail-closed) — render clean content.
            return $content;
        }

        return $signed_text;
    }

    /**
     * Maybe add the C2PA badge to post content.
     *
     * Invoked from render_provenance_output() (single the_content filter at
     * PHP_INT_MAX) AFTER injection, never registered as its own filter: the
     * badge wrapper must land outside the byte-exact signed region.
     *
     * The gate is "does this post carry a provenance record", NOT "is the
     * served snapshot current". Those are different questions and conflating
     * them suppressed the badge on exactly the posts a reader most needs it for:
     * an edited post still carries a record, and hiding its control hid the
     * `changed` verdict instead of reporting it. Currentness still decides which
     * bytes are served and which pre-check state the badge shows.
     *
     * @param string $content Post content.
     * @return string Content with the badge wrapper added.
     */
    public function maybe_add_c2pa_badge(string $content): string
    {
        if (! is_singular() || ! in_the_loop() || ! is_main_query()) {
            return $content;
        }

        $post_id = get_the_ID();
        if (! $post_id) {
            return $content;
        }

        if (! ReaderVerdict::post_carries_text_provenance((int) $post_id)) {
            return $content;
        }

        // Get settings
        $settings = get_option('encypher_provenance_settings', []);
        $show_badge = isset($settings['show_badge']) ? (bool) $settings['show_badge'] : true; // Default ON

        // A server entitlement is required to suppress or replace Encypher's
        // standard reader branding. Local tier strings never grant this.
        if (! Capabilities::allows(Capabilities::WHITE_LABEL)) {
            $show_badge = true;
        }

        if (! $show_badge) {
            return $content;
        }

        $badge_position = ! empty($settings['badge_position']) ? $settings['badge_position'] : 'bottom';
        $badge_html = $this->get_badge_html($post_id, 'large');

        $this->add_verification_modal($post_id);

        if ('bottom-right' === $badge_position) {
            add_action('wp_footer', function () use ($badge_html) {
                echo $this->get_badge_wrapper($badge_html, 'floating'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- trusted plugin-generated badge markup; dynamic values escaped inside get_badge_html().
            });

            return $content;
        }

        $placement = 'top' === $badge_position ? 'top' : 'bottom';
        $badge_wrapper = $this->get_badge_wrapper($badge_html, $placement);

        if ('top' === $badge_position) {
            return $badge_wrapper . $content;
        }

        return $content . $badge_wrapper;
    }

    /**
     * Register the shared verification dialog for a post, once per request.
     */
    private function add_verification_modal(int $post_id): void
    {
        static $modal_post_ids = [];

        if (isset($modal_post_ids[$post_id])) {
            return;
        }

        add_action('wp_footer', function () {
            echo $this->get_modal_html(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- trusted plugin-generated modal markup; dynamic values escaped inside get_modal_html().
        });

        $modal_post_ids[$post_id] = true;
    }

    private function get_badge_wrapper(string $badge_html, string $placement): string
    {
        $classes = 'encypher-c2pa-badge-wrapper';

        if ('floating' === $placement) {
            $classes .= ' encypher-c2pa-badge-floating';
        } elseif ('top' === $placement) {
            $classes .= ' encypher-c2pa-badge-top';
        } else {
            $classes .= ' encypher-c2pa-badge-bottom';
        }

        return '<div class="' . esc_attr($classes) . '">' . $badge_html . '</div>';
    }


    /**
     * Resolve the entitled brand-logo URL for the reader surfaces, or '' for
     * Encypher branding. The stored attachment is local presentation input;
     * Encypher's current server capability projection is the authority.
     *
     * @param array<string,mixed> $settings The provenance settings option.
     */
    private function resolve_brand_logo_url(array $settings): string
    {
        if (! Capabilities::allows(Capabilities::WHITE_LABEL)) {
            return '';
        }

        $logo_id = isset($settings['brand_logo_id']) ? (int) $settings['brand_logo_id'] : 0;
        if ($logo_id <= 0) {
            return '';
        }

        $mime = (string) get_post_mime_type($logo_id);
        if (! in_array($mime, ['image/png', 'image/jpeg', 'image/webp'], true)) {
            return '';
        }

        $url = (string) wp_get_attachment_image_url($logo_id, 'full');
        if ('' === $url) {
            return '';
        }

        $modified = (int) get_post_modified_time('U', true, $logo_id);
        if ($modified > 0) {
            $url = add_query_arg('v', $modified, $url);
        }

        return $url;
    }

    /**
     * Build the small map of a post's provenance-bearing media for the optional
     * client-side info marks.
     *
     * Two independent gates must both hold before a mark may be offered, because
     * a mark is a promise that clicking it produces an answer:
     *
     * 1. EXACT LIVE BINDING. VipMediaDelivery::public_signed_media() returns only
     *    attachments whose bytes on disk still hash to the value that was signed,
     *    served from the canonical delivery URL. The browser then re-checks that
     *    the element it is about to mark actually loaded that exact URL, so a
     *    resized, cropped, re-encoded, or content-negotiated rendition is never
     *    marked: those renditions do not carry the original's hard binding.
     *
     * 2. RESOLVABLE CLAIM. The attachment must still have a claim the verify
     *    route can resolve. An asset whose signing run left an error, or which
     *    carries no claim identifier and no valid detected source manifest, has
     *    nothing to show; marking it produced a dialog that could only ever say
     *    verification was unavailable.
     *
     * @return array<int,array{id:int,url:string,type:string,claim:string}>
     */
    private function get_signed_media_payload(int $post_id): array
    {
        $payload = [];
        foreach (VipMediaDelivery::public_signed_media($post_id) as $item) {
            $attachment_id = isset($item['id']) ? (int) $item['id'] : 0;
            $claim = self::resolvable_media_claim($attachment_id);
            if ('' === $claim) {
                continue;
            }
            $item['claim'] = $claim;
            $payload[] = $item;
        }

        return $payload;
    }

    /**
     * The claim identifier a reader mark for this attachment would resolve to,
     * or '' when there is nothing resolvable.
     *
     * A pending signing failure disqualifies the asset outright: the stored
     * claim, if any, describes a run that did not complete, so offering the
     * reader a check against it is offering a check that cannot succeed.
     *
     * An Encypher-signed asset resolves through its C2PA instance id. An asset
     * that arrived carrying provenance resolves through its detected source
     * manifest, but only when that manifest actually validated: an unverifiable
     * source manifest is a claim we cannot stand behind.
     */
    private static function resolvable_media_claim(int $attachment_id): string
    {
        if ($attachment_id <= 0) {
            return '';
        }
        $sign_error = get_post_meta($attachment_id, '_encypher_last_sign_error', true);
        if (! empty($sign_error)) {
            return '';
        }

        $instance_id = (string) get_post_meta($attachment_id, '_encypher_c2pa_instance_id', true);
        if ('' !== $instance_id) {
            return $instance_id;
        }

        $source_manifest = (string) get_post_meta($attachment_id, '_encypher_source_has_c2pa', true);
        $source_valid = ! empty(get_post_meta($attachment_id, '_encypher_source_c2pa_valid', true));
        if ('' !== $source_manifest && $source_valid) {
            $source_hash = (string) get_post_meta($attachment_id, '_encypher_source_file_hash', true);

            return '' !== $source_hash ? $source_hash : $source_manifest;
        }

        return '';
    }

    /**
     * Get badge HTML for a post.
     *
     * The server NEVER renders a trusted verdict. Trust is the result of a live
     * check against a trust anchor; a stored `c2pa_verified` status with a
     * recent timestamp records that some past check succeeded, which is not a
     * statement about the bytes this request is serving. Rendering one as the
     * other is how a ten-minute-old timestamp became a green check on content
     * that had since been edited. The reachable pre-check states are therefore
     * `signed_unchecked`, `changed`, and `unavailable`, all conservative, and
     * the dialog's live check supplies every terminal verdict.
     *
     * @param int    $post_id Post ID.
     * @param string $size    Badge size: 'small' or 'large'.
     * @return string Badge HTML, or '' when no control may render.
     */
    private function get_badge_html(int $post_id, string $size = 'large'): string
    {
        $status = (string) get_post_meta($post_id, '_encypher_provenance_status', true);
        $sign_errors = get_post_meta($post_id, '_encypher_sign_errors', true);
        $signing_incomplete = in_array($status, ['embedding_failed', 'media_signing_failed'], true)
            || (is_array($sign_errors) && ! empty($sign_errors));

        $state = ReaderVerdict::pre_check_state(
            ReaderVerdict::post_carries_text_provenance($post_id),
            RslExport::has_current_committed_text_provenance($post_id),
            $signing_incomplete
        );
        if (null === $state) {
            // Unsigned content, or a signing run still in flight: there is no
            // record to offer a check against.
            return '';
        }

        $presentation = ReaderVerdict::presentation($state);
        $detail = 'unavailable' === $state && $signing_incomplete
            ? ReaderVerdict::detail_copy(ReaderVerdict::verdict_from_error('signing_incomplete'))
            : $presentation['detailLabel'];

        $settings = get_option('encypher_provenance_settings', []);

        // A server-entitled, resolvable raster logo replaces the Encypher mark.
        // Otherwise the standard Encypher branding remains.
        $brand_logo_url = $this->resolve_brand_logo_url($settings);
        $icon_url = ENCYPHER_PROVENANCE_PLUGIN_URL . 'assets/images/brand/encypher-mark-navy.svg';
        $icon_size = 'small' === $size ? '18' : '24';
        $delivery_generation = (string) get_post_meta($post_id, '_encypher_provenance_instance_id', true);

        ob_start();
        ?>
        <button type="button"
                class="encypher-c2pa-badge encypher-c2pa-badge-<?php echo esc_attr($presentation['legacyClass']); ?> encypher-c2pa-badge--<?php echo esc_attr($size); ?>"
                data-post-id="<?php echo esc_attr($post_id); ?>"
                data-provenance-generation="<?php echo esc_attr($delivery_generation); ?>"
                data-encypher-verdict="<?php echo esc_attr($presentation['state']); ?>"
                data-encypher-tone="<?php echo esc_attr($presentation['tone']); ?>"
                <?php if ($presentation['trusted']) : ?>data-encypher-trusted="1"<?php endif; ?>
                aria-label="<?php echo esc_attr($presentation['shortLabel'] . '. ' . $detail); ?>"
                title="<?php echo esc_attr($detail); ?>">
            <?php if ('' !== $brand_logo_url) : ?>
                <img src="<?php echo esc_url($brand_logo_url); ?>"
                     alt=""
                     class="encypher-c2pa-badge__icon encypher-c2pa-badge__icon--brand" width="<?php echo esc_attr($icon_size); ?>" height="<?php echo esc_attr($icon_size); ?>" />
            <?php else : ?>
                <img src="<?php echo esc_url($icon_url); ?>"
                     alt=""
                     class="encypher-c2pa-badge__icon" width="<?php echo esc_attr($icon_size); ?>" height="<?php echo esc_attr($icon_size); ?>" aria-hidden="true" />
            <?php endif; ?>

            <span class="encypher-c2pa-badge__label"><?php echo esc_html($presentation['shortLabel']); ?></span>
            <?php // One status glyph, distinct per verdict, drawn by CSS. The
                  // check variant is reachable only from `trusted`. ?>
            <span class="encypher-c2pa-badge__glyph encypher-c2pa-badge__glyph--<?php echo esc_attr($presentation['glyph']); ?>"
                  data-encypher-glyph="<?php echo esc_attr($presentation['glyph']); ?>"
                  aria-hidden="true"></span>
            <?php if ($presentation['showCheck']) : ?>
                <span class="encypher-c2pa-badge__check" aria-hidden="true"></span>
            <?php endif; ?>
        </button>
        <?php
        return ob_get_clean();
    }

    /**
     * Get modal HTML for verification display.
     *
     * @return string Modal HTML
     */
    private function get_modal_html(): string
    {
        $logo_url = ENCYPHER_PROVENANCE_PLUGIN_URL . 'assets/images/encypher-logo.png';

        $settings      = get_option('encypher_provenance_settings', []);
        $entitled      = Capabilities::allows(Capabilities::WHITE_LABEL);
        $show_branding = $entitled
            ? (isset($settings['show_branding']) ? (bool) $settings['show_branding'] : true)
            : true;

        $brand_logo_url  = $this->resolve_brand_logo_url($settings);
        $powered_by      = isset($settings['powered_by_encypher']) ? (bool) $settings['powered_by_encypher'] : true;
        $show_powered_by = $entitled ? $powered_by : true;

        ob_start();
        ?>
        <div
            id="encypher-c2pa-modal"
            class="encypher-c2pa-modal"
            role="dialog"
            aria-modal="true"
            aria-hidden="true"
            aria-labelledby="encypher-modal-title"
            aria-describedby="encypher-modal-description"
            hidden>
            <div class="encypher-c2pa-modal-overlay" aria-hidden="true"></div>
            <div class="encypher-c2pa-modal-content" tabindex="-1">
                <div class="encypher-c2pa-modal-header">
                    <?php if ('' !== $brand_logo_url) : ?>
                        <img src="<?php echo esc_url($brand_logo_url); ?>" alt="" class="encypher-c2pa-modal-logo encypher-c2pa-modal-logo--brand" />
                    <?php else : ?>
                        <img src="<?php echo esc_url($logo_url); ?>" alt="Encypher" class="encypher-c2pa-modal-logo" />
                    <?php endif; ?>
                    <button type="button" class="encypher-c2pa-modal-close" aria-label="<?php esc_attr_e('Close verification dialog', 'encypher-provenance'); ?>">
                        <span aria-hidden="true">&times;</span>
                    </button>
                </div>
                <div class="encypher-c2pa-modal-body">
                    <h2 id="encypher-modal-title"><?php esc_html_e('About this content', 'encypher-provenance'); ?></h2>
                    <p id="encypher-modal-description" class="encypher-c2pa-modal-description">
                        <?php esc_html_e('Check who published it and whether it has changed.', 'encypher-provenance'); ?>
                    </p>
                    <div class="encypher-c2pa-modal-loading" role="status" aria-live="polite">
                        <img src="<?php echo esc_url(ENCYPHER_PROVENANCE_PLUGIN_URL . 'assets/images/brand/encypher-loader-navy.svg'); ?>" alt="" class="encypher-c2pa-loader" width="38" height="38" aria-hidden="true" />
                        <p><?php esc_html_e('Verifying content...', 'encypher-provenance'); ?></p>
                    </div>
                    <div class="encypher-c2pa-modal-result" role="status" aria-live="polite" hidden></div>
                    <?php if ($show_powered_by) : ?>
                        <p class="encypher-c2pa-modal-powered-by">
                            <a href="<?php echo esc_url('https://encypher.com'); ?>" target="_blank" rel="noopener noreferrer"><?php esc_html_e('Powered by Encypher', 'encypher-provenance'); ?></a>
                        </p>
                    <?php endif; ?>
                </div>
            </div>
        </div>
        <?php
        return ob_get_clean();
    }

}
