<?php
/**
 * Read-only media-library surfacing of detected source C2PA provenance.
 *
 * When an uploaded asset already carries a C2PA manifest (detected by
 * C2paDetector::detect and recorded by Plugin::detect_c2pa_on_upload), this
 * class shows a small "Encypher Provenance" indicator on
 * the attachment — both as a column in the media list table and as a field in
 * the attachment details panel. Display only: it never mutates provenance.
 *
 * @package EncypherProvenance
 */

namespace EncypherProvenance;

if (! defined('ABSPATH')) {
    exit;
}
require_once __DIR__ . '/class-encypher-signing-status.php';
require_once __DIR__ . '/class-encypher-reader-verdict.php';
require_once __DIR__ . '/class-encypher-capabilities.php';

class MediaLibraryUi
{
    /**
     * Register media-library display hooks.
     */
    public function register_hooks(): void
    {
        // Attachment details panel: provenance indicator + per-asset license.
        add_filter('attachment_fields_to_edit', [$this, 'add_attachment_field'], 10, 2);
        add_filter('attachment_fields_to_save', [$this, 'save_attachment_rights'], 10, 2);

        // Media list table column.
        add_filter('manage_media_columns', [$this, 'add_media_column']);
        add_action('manage_media_custom_column', [$this, 'render_media_column'], 10, 2);

        // Media list provenance filter (upload.php list mode).
        add_action('restrict_manage_posts', [$this, 'render_media_provenance_filter'], 10, 2);
        add_action('pre_get_posts', [$this, 'filter_media_by_provenance']);

        // Standalone media signing: row action + native bulk action.
        add_filter('media_row_actions', [$this, 'add_media_row_action'], 10, 2);
        add_filter('bulk_actions-upload', [$this, 'add_media_bulk_action']);
        add_filter('handle_bulk_actions-upload', [$this, 'handle_media_bulk_action'], 10, 3);
        add_action('admin_post_encypher_sign_attachment', [$this, 'handle_single_attachment_sign']);
        add_action('admin_notices', [$this, 'render_media_sign_notice']);

        // Per-asset license selector assets on media screens.
        add_action('admin_enqueue_scripts', [$this, 'enqueue_media_rights_assets']);
    }

    /**
     * Add an "Encypher Provenance" column to the media list table.
     *
     * @param array $columns Existing columns.
     *
     * @return array Columns with the indicator column appended.
     */
    public function add_media_column(array $columns): array
    {
        $columns['encypher_c2pa'] = __('Encypher Provenance', 'encypher-provenance');

        return $columns;
    }

    /**
     * Render the "Encypher Provenance" column cell for an attachment.
     *
     * @param string $column_name   Current column key.
     * @param int    $attachment_id Attachment post ID.
     */
    public function render_media_column(string $column_name, int $attachment_id): void
    {
        if ('encypher_c2pa' !== $column_name) {
            return;
        }

        $html = $this->column_badge_html($attachment_id);
        if (in_array($this->classify($attachment_id), ['source_c2pa', 'signed_by_you'], true)) {
            $edit_url = get_edit_post_link($attachment_id);
            if (is_string($edit_url) && '' !== $edit_url) {
                $html .= '<br /><a class="encypher-c2pa-source__details-link" href="' . esc_url($edit_url) . '">'
                    . esc_html__('View provenance details', 'encypher-provenance') . '</a>';
            }
        }
        echo wp_kses($html, self::indicator_allowed_html());
    }

    /**
     * Add a read-only indicator field to the attachment details panel.
     *
     * @param array    $form_fields Existing attachment fields.
     * @param \WP_Post $post        Attachment post object.
     *
     * @return array Fields, with the indicator added when provenance exists.
     */
    public function add_attachment_field(array $form_fields, \WP_Post $post): array
    {
        // Always surface a provenance indicator: rich detail for source C2PA /
        // Encypher-signed assets, a quiet "No provenance detected" once scanned
        // clean, and a dash while unscanned.
        $form_fields['encypher_source_c2pa'] = [
            'label' => __('Encypher Provenance', 'encypher-provenance'),
            'input' => 'html',
            'html'  => $this->get_indicator_html($post->ID),
        ];

        // Per-asset policy selection is a paid capability. Source and signed
        // rights remain readable for every account.
        if (Capabilities::allows(Capabilities::SEGMENT_RIGHTS) && class_exists(Rest::class)) {
            $mime = (string) get_post_mime_type($post->ID);
            if (Rest::is_signable_media_mime($mime)) {
                $form_fields['encypher_rights'] = [
                    'label' => __('Encypher License', 'encypher-provenance'),
                    'input' => 'html',
                    'html'  => $this->license_field_html($post->ID),
                ];
            }
        }

        return $form_fields;
    }

    /**
     * Build the indicator markup for an attachment.
     *
     * Ordering matters. An Encypher-signed asset that ALSO arrived carrying a
     * source manifest used to fall into the source branch, so the badge read
     * "Signed by you" while the detail below it read "Trust: signature could not
     * be verified". Those two sentences are about different manifests, and read
     * together they said the site's own signature had failed. The Encypher state
     * is now resolved first, and every fact about the ORIGINAL file is rendered
     * under its own heading with its own scoped trust line, so neither sentence
     * can be mistaken for the other.
     *
     * @param int $attachment_id Attachment post ID.
     *
     * @return string Indicator HTML.
     */
    private function get_indicator_html(int $attachment_id): string
    {
        $state = $this->classify($attachment_id);
        $badge = $this->column_badge_html($attachment_id);

        if ('signed_by_you' === $state) {
            $signed_at = (string) get_post_meta($attachment_id, '_encypher_signed_at', true);
            $detail = '<br />' . esc_html__('Signed with Encypher. Its signature has not been checked live from this screen.', 'encypher-provenance');
            if ('' !== $signed_at) {
                /* translators: %s: date the asset was signed with Encypher. */
                $detail .= '<br />' . sprintf(esc_html__('Signed: %s', 'encypher-provenance'), esc_html($signed_at));
            }
            $detail .= $this->source_manifest_detail($attachment_id);
            $detail .= '<br />' . $this->inspect_link();

            return $badge . '<span class="encypher-c2pa-source__detail">' . $detail . '</span>';
        }

        $has_source_manifest = '' !== (string) get_post_meta($attachment_id, '_encypher_source_has_c2pa', true);
        if ('source_c2pa' === $state || $has_source_manifest) {
            $detail = $this->source_manifest_detail($attachment_id, false);
            if ('' === $detail) {
                $detail = '<br />' . $this->inspect_link();
            }

            return $badge . '<span class="encypher-c2pa-source__detail">' . $detail . '</span>';
        }

        if ('needs_attention' === $state) {
            $err = self::last_sign_error_message($attachment_id);
            $retry = 'vip_cache_purge_failed' === self::last_sign_error_code($attachment_id)
                ? '<br /><button type="button" class="button-link" data-encypher-purge-retry="1" data-attachment-id="' . esc_attr((string) $attachment_id) . '">'
                    . esc_html__('Retry public delivery', 'encypher-provenance') . '</button>'
                : '';
            return $badge . '<span class="encypher-c2pa-source__detail">'
                . ('' !== $err ? '<br />' . esc_html($err) : '') . $retry . '</span>';
        }

        return $badge;
    }

    /**
     * Facts about the manifest the file arrived with.
     *
     * Every trust sentence here is scoped to the ORIGINAL file. A bare "Trust:"
     * line beside an Encypher-signed asset reads as a verdict on the site's own
     * signature, which is a different manifest signed by a different key.
     *
     * @param bool $scoped Whether to render the "Original file" heading. False
     *                     for an asset the site never signed, where the source
     *                     manifest is the only manifest and needs no scoping.
     */
    private function source_manifest_detail(int $attachment_id, bool $scoped = true): string
    {
        if ('' === (string) get_post_meta($attachment_id, '_encypher_source_has_c2pa', true)) {
            return '';
        }

        $format  = (string) get_post_meta($attachment_id, '_encypher_source_c2pa_format', true);
        $valid   = (bool) get_post_meta($attachment_id, '_encypher_source_c2pa_valid', true);
        $signer  = (string) get_post_meta($attachment_id, '_encypher_source_c2pa_signer', true);
        $summary = $this->decode_meta_array($attachment_id, '_encypher_source_c2pa_summary');
        $rights  = $this->decode_meta_array($attachment_id, '_encypher_source_c2pa_rights');
        $details = $this->decode_meta_array($attachment_id, '_encypher_source_c2pa_details');

        $lines = [];
        if (! empty($summary['statement'])) {
            $lines[] = '<strong>' . esc_html((string) $summary['statement']) . '</strong>';
        }
        if (! empty($summary['software_agent'])) {
            /* translators: %s: software or device that created the source asset. */
            $lines[] = sprintf(esc_html__('Created with: %s', 'encypher-provenance'), esc_html((string) $summary['software_agent']));
        }
        if ('' !== $signer) {
            /* translators: %s: signer organization or tool name. */
            $lines[] = sprintf(esc_html__('Signed by: %s', 'encypher-provenance'), esc_html($signer));
        }
        if (! empty($summary['source_type'])) {
            /* translators: %s: plain-language digital source type. */
            $lines[] = sprintf(esc_html__('Source: %s', 'encypher-provenance'), esc_html((string) $summary['source_type']));
        }
        $lines[] = $valid
            ? esc_html__('Original file trust: its signature and asset binding verified', 'encypher-provenance')
            : esc_html__('Original file trust: its signature could not be verified', 'encypher-provenance');
        if (! empty($summary['signed_at'])) {
            /* translators: %s: C2PA signing date. */
            $lines[] = sprintf(esc_html__('Signed: %s', 'encypher-provenance'), esc_html((string) $summary['signed_at']));
        }
        if ('' !== $format) {
            /* translators: %s: media format or type. */
            $lines[] = sprintf(esc_html__('Format: %s', 'encypher-provenance'), esc_html($format));
        }
        foreach ($this->source_rights_lines($rights) as $rights_line) {
            $lines[] = esc_html($rights_line);
        }
        if (! empty($rights)) {
            $lines[] = '<strong>' . esc_html__('These source rights are preserved when Encypher signs the post.', 'encypher-provenance') . '</strong>';
        }

        $technical = '';
        if (Capabilities::allows(Capabilities::MANIFEST_VIEWER)) {
            $full = ! empty($details) ? $details : $summary;
            $full_json = wp_json_encode($full, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
            $technical = (! empty($full) && is_string($full_json) && '' !== $full_json)
                ? '<details class="encypher-c2pa-source__technical"><summary>'
                    . esc_html__('View full C2PA info', 'encypher-provenance')
                    . '</summary><pre>' . esc_html($full_json) . '</pre></details>'
                : '';
        }

        $heading = $scoped
            ? '<br /><strong>' . esc_html__('Provenance the original file arrived with', 'encypher-provenance') . '</strong>'
            : '';

        return $heading . '<br />' . implode('<br />', $lines) . $technical;
    }

    /**
     * Classify an attachment into a single display state. Source-provenance
     * buckets come from MediaScan so the column, the attachment field, and the
     * list filter never disagree; Encypher-signed and needs-attention states
     * layer on top.
     *
     * @return string needs_attention|signing|signed_by_you|source_c2pa|scanned_clean|unscanned
     */
    private function classify(int $attachment_id): string
    {
        $raw_signing_flag = get_post_meta($attachment_id, '_encypher_signing_in_progress', true);
        if (! empty($raw_signing_flag) && class_exists(SigningStatus::class)) {
            if (SigningStatus::clear_if_stale($attachment_id, $raw_signing_flag, time())) {
                update_post_meta($attachment_id, '_encypher_last_sign_error', [
                    'code'    => 'signing_stalled',
                    'message' => __('Media signing stopped reporting progress. Retry signing this attachment.', 'encypher-provenance'),
                    'time'    => time(),
                ]);
                $raw_signing_flag = '';
            }
        }
        if ('' !== self::last_sign_error_message($attachment_id)) {
            return 'needs_attention';
        }
        if (! empty($raw_signing_flag)) {
            return 'signing';
        }
        $status = (string) get_post_meta($attachment_id, '_encypher_provenance_status', true);
        if (in_array($status, ['signed', 'verified', 'c2pa_protected', 'c2pa_verified'], true)) {
            return 'signed_by_you';
        }

        $bucket = class_exists(MediaScan::class)
            ? MediaScan::source_bucket_for_attachment($attachment_id)
            : ('' !== (string) get_post_meta($attachment_id, '_encypher_source_has_c2pa', true) ? 'carries' : 'unscanned');
        if ('carries' === $bucket) {
            return 'source_c2pa';
        }
        if ('none' === $bucket) {
            return 'scanned_clean';
        }
        return 'unscanned';
    }

    /**
     * Compact single-badge state for the media list column.
     *
     * The badge carries a TRUST state alongside its status. "Signed by you"
     * alone answered "did we sign this" and was read as "is this verified",
     * which is a different question this screen cannot answer: nothing here
     * performs a live check. Every badge now states its trust explicitly, and
     * the only trust word available without a live check is "not yet verified".
     *
     * A detected source manifest is the one case where a real trust fact is
     * already recorded, because the scan checked the signature and the asset
     * binding. Even then the badge says the SIGNATURE verified, not that the
     * signer is trusted: nothing on this screen checked a trust list.
     */
    private function column_badge_html(int $attachment_id): string
    {
        $state = $this->classify($attachment_id);
        $verdict = 'unavailable';
        $trust = 'unknown';
        $chip = '';

        switch ($state) {
            case 'needs_attention':
                $err = self::last_sign_error_message($attachment_id);
                $badge = '<span class="encypher-c2pa-source encypher-c2pa-source--attention" title="' . esc_attr($err) . '">'
                    . self::status_icon() . ' ' . esc_html__('Needs attention', 'encypher-provenance') . '</span>';
                $verdict = 'unavailable';
                break;
            case 'signing':
                $badge = '<span class="encypher-c2pa-source encypher-c2pa-source--signing">'
                    . self::status_icon(true) . ' ' . esc_html__('Signing', 'encypher-provenance') . '</span>';
                $verdict = 'checking';
                break;
            case 'signed_by_you':
                $badge = '<span class="encypher-c2pa-source encypher-c2pa-source--signed" title="'
                    . esc_attr__('Signed with Encypher. Nothing on this screen has checked the signature; open the attachment to check it.', 'encypher-provenance') . '">'
                    . self::status_icon() . ' ' . esc_html__('Signed by you', 'encypher-provenance') . '</span>';
                $verdict = 'signed_unchecked';
                $trust = 'unknown';
                $chip = esc_html__('not yet verified', 'encypher-provenance');
                break;
            case 'source_c2pa':
                $valid  = (bool) get_post_meta($attachment_id, '_encypher_source_c2pa_valid', true);
                $signer = (string) get_post_meta($attachment_id, '_encypher_source_c2pa_signer', true);
                if ($valid) {
                    $tooltip = '' !== $signer
                        ? sprintf(
                            /* translators: %s: signer / claim generator name. */
                            __('The signature and asset binding of this file verified. It claims to be signed by %s, who was not checked against a trust list.', 'encypher-provenance'),
                            $signer
                        )
                        : __('The signature and asset binding of this file verified. Its signer was not checked against a trust list.', 'encypher-provenance');
                    $verdict = 'valid_untrusted';
                    $trust = 'unknown';
                    $chip = esc_html__('signature verified', 'encypher-provenance');
                    $cls = 'encypher-c2pa-source--verified';
                } else {
                    $tooltip = __('A C2PA record was found in this file, but its signature could not be verified.', 'encypher-provenance');
                    $verdict = 'changed';
                    $trust = 'untrusted';
                    $chip = esc_html__('signature could not be verified', 'encypher-provenance');
                    $cls = 'encypher-c2pa-source--detected';
                }
                $badge = '<span class="encypher-c2pa-source ' . esc_attr($cls) . '" title="' . esc_attr($tooltip) . '">'
                    . self::status_icon() . ' ' . esc_html__('Carries provenance', 'encypher-provenance') . '</span>';
                break;
            case 'scanned_clean':
                $badge = '<span class="encypher-c2pa-source encypher-c2pa-source--clean">'
                    . esc_html__('No provenance detected', 'encypher-provenance') . '</span>';
                $verdict = 'not_present';
                break;
            default:
                $badge = '<span class="encypher-c2pa-source encypher-c2pa-source--none">'
                    . esc_html__('—', 'encypher-provenance') . '</span>';
                $verdict = 'unavailable';
                break;
        }

        if ('' !== $chip) {
            $badge .= ' <span class="encypher-c2pa-source__trust encypher-c2pa-source__trust--' . esc_attr($trust) . '"'
                . ' data-encypher-media-trust="' . esc_attr($trust) . '">(' . $chip . ')</span>';
        }

        $poll_status = 'signed_by_you' === $state ? 'signed' : ('needs_attention' === $state ? 'needs_attention' : ('signing' === $state ? 'signing' : 'not_signed'));
        return '<span data-encypher-media-status="1" data-attachment-id="' . esc_attr((string) $attachment_id)
            . '" data-status="' . esc_attr($poll_status) . '"'
            . ' data-encypher-verdict="' . esc_attr(ReaderVerdict::is_known_state($verdict) ? $verdict : 'unavailable') . '"'
            . ' data-encypher-media-trust="' . esc_attr($trust) . '">' . $badge . '</span>';
    }
    private static function last_sign_error_message(int $attachment_id): string
    {
        $error = get_post_meta($attachment_id, '_encypher_last_sign_error', true);
        if (is_array($error)) {
            return isset($error['message']) ? (string) $error['message'] : '';
        }
        return is_string($error) ? $error : '';
    }
    private static function last_sign_error_code(int $attachment_id): string
    {
        $error = get_post_meta($attachment_id, '_encypher_last_sign_error', true);
        return is_array($error) && isset($error['code']) ? (string) $error['code'] : '';
    }



    /**
     * "Inspect in Encypher dashboard" link (new tab, no referrer leak).
     */
    private function inspect_link(): string
    {
        return '<a class="encypher-c2pa-inspect" href="' . esc_url('https://dashboard.encypher.com/inspect')
            . '" target="_blank" rel="noopener noreferrer">'
            . esc_html__('Inspect in Encypher dashboard', 'encypher-provenance') . '</a>';
    }

    /**
     * Decode one JSON-backed attachment meta value.
     *
     * @return array<string,mixed>
     */
    private function decode_meta_array(int $attachment_id, string $meta_key): array
    {
        $raw = get_post_meta($attachment_id, $meta_key, true);
        if (is_array($raw)) {
            return $raw;
        }
        if (! is_string($raw) || '' === $raw) {
            return [];
        }
        $decoded = json_decode($raw, true);
        return is_array($decoded) ? $decoded : [];
    }

    /**
     * Build compact, plain-language source-rights facts.
     *
     * @param array<string,mixed> $rights Normalized public verify rights block.
     * @return list<string>
     */
    private function source_rights_lines(array $rights): array
    {
        if (empty($rights)) {
            return [];
        }
        $embedded = isset($rights['embedded']) && is_array($rights['embedded']) ? $rights['embedded'] : [];
        $embedded_rights = isset($embedded['rights']) && is_array($embedded['rights']) ? $embedded['rights'] : [];
        $display = isset($embedded_rights['display']) && is_array($embedded_rights['display']) ? $embedded_rights['display'] : [];
        $training = isset($embedded['training_mining']['use']) && is_array($embedded['training_mining']['use'])
            ? $embedded['training_mining']['use']
            : [];

        $license = (string) ($rights['license_type'] ?? $embedded_rights['license_type'] ?? '');
        $usage = (string) ($rights['usage_terms'] ?? $display['usage_terms'] ?? $embedded_rights['usage_terms'] ?? '');
        $license_url = (string) ($rights['license_url'] ?? $display['license_url'] ?? $embedded_rights['license_url'] ?? '');
        $lines = [__('Rights: preserved from the source asset', 'encypher-provenance')];
        if ('' !== $license) {
            $lines[] = sprintf(
                /* translators: %s: source asset license name. */
                __('License: %s', 'encypher-provenance'),
                $license
            );
        }
        if ('' !== $usage) {
            $lines[] = sprintf(
                /* translators: %s: source asset usage terms. */
                __('Usage: %s', 'encypher-provenance'),
                $usage
            );
        } elseif ('' !== $license_url) {
            $lines[] = sprintf(
                /* translators: %s: source asset license URL. */
                __('License terms: %s', 'encypher-provenance'),
                $license_url
            );
        }
        $labels = [
            'ai_training' => __('AI training', 'encypher-provenance'),
            'ai_inference' => __('AI inference', 'encypher-provenance'),
            'data_mining' => __('Data mining', 'encypher-provenance'),
        ];
        foreach ($labels as $key => $label) {
            if (array_key_exists($key, $training)) {
                $lines[] = sprintf(
                    /* translators: 1: use category, 2: allowed or not allowed. */
                    __('%1$s: %2$s', 'encypher-provenance'),
                    $label,
                    $training[$key] ? __('allowed', 'encypher-provenance') : __('not allowed', 'encypher-provenance')
                );
            }
        }
        return $lines;
    }

    private static function canonical_evidence_url(array $evidence): string
    {
        $evidence_id = trim((string) ($evidence['rights_evidence_id'] ?? ''));
        $url = trim((string) ($evidence['rights_resolution_url'] ?? ''));
        if (
            ! preg_match('/\A[A-Za-z0-9][A-Za-z0-9._~-]{15,254}\z/D', $evidence_id)
            || '' === $url
            || preg_match('/[\x00-\x20\x7f]/', $url)
        ) {
            return '';
        }
        $parts = wp_parse_url($url);
        if (
            ! is_array($parts)
            || ! isset($parts['scheme'], $parts['host'], $parts['path'])
            || 'https' !== strtolower((string) $parts['scheme'])
            || isset($parts['user'])
            || isset($parts['pass'])
            || isset($parts['query'])
            || isset($parts['fragment'])
        ) {
            return '';
        }
        $suffix = '/api/v1/public/rights/evidence/' . $evidence_id;
        $path = (string) $parts['path'];
        return strlen($path) >= strlen($suffix) && substr($path, -strlen($suffix)) === $suffix ? $url : '';
    }

    /**
     * Per-asset license selector markup. New choices are populated from the
     * dashboard catalog by media-rights.js. A legacy custom selection remains
     * identifiable but disabled and read-only; no local freeform fields render.
     */
    private function license_field_html(int $attachment_id): string
    {
        $source_rights = $this->decode_meta_array($attachment_id, '_encypher_source_c2pa_rights');
        $source_valid = (bool) get_post_meta($attachment_id, '_encypher_source_c2pa_valid', true);
        if ($source_valid && ! empty($source_rights)) {
            $lines = $this->source_rights_lines($source_rights);
            return '<div class="encypher-attachment-rights encypher-attachment-rights--source">'
                . '<strong>' . esc_html__('Rights preserved from source', 'encypher-provenance') . '</strong><br />'
                . implode('<br />', array_map('esc_html', $lines))
                . '<p class="description">'
                . esc_html__('The verified source manifest signed these terms. Post, block, attachment, and site defaults cannot replace them.', 'encypher-provenance')
                . '</p></div>';
        }

        // An Encypher-signed asset: show the applied license read-only.
        $status = (string) get_post_meta($attachment_id, '_encypher_provenance_status', true);
        if (in_array($status, ['signed', 'verified', 'c2pa_protected', 'c2pa_verified'], true)) {
            $selection = $this->decode_meta_array($attachment_id, '_encypher_rights');
            $template = isset($selection['template']) ? (string) $selection['template'] : '';
            $label = '' !== $template ? $template : __('Site default', 'encypher-provenance');
            $custom = 'custom' === $template && is_array($selection['custom'] ?? null)
                ? $selection['custom']
                : [];
            $lines = [];
            $display_fields = [
                'usage_terms'      => __('Usage', 'encypher-provenance'),
                'license_url'      => __('License terms', 'encypher-provenance'),
                'copyright_holder' => __('Copyright holder', 'encypher-provenance'),
            ];
            foreach ($display_fields as $key => $field_label) {
                if (! empty($custom[$key])) {
                    $lines[] = sprintf(
                        /* translators: 1: rights field label, 2: selected custom value. */
                        __('%1$s: %2$s', 'encypher-provenance'),
                        $field_label,
                        (string) $custom[$key]
                    );
                }
            }
            $permission_fields = [
                'ai_training' => __('AI training', 'encypher-provenance'),
                'ai_inference' => __('AI inference', 'encypher-provenance'),
                'data_mining' => __('Data mining', 'encypher-provenance'),
            ];
            foreach ($permission_fields as $key => $field_label) {
                if (array_key_exists($key, $custom)) {
                    $lines[] = sprintf(
                        /* translators: 1: use category, 2: allowed or not allowed. */
                        __('%1$s: %2$s', 'encypher-provenance'),
                        $field_label,
                        $custom[$key] ? __('allowed', 'encypher-provenance') : __('not allowed', 'encypher-provenance')
                    );
                }
            }
            $details = empty($lines) ? '' : '<br />' . implode('<br />', array_map('esc_html', $lines));
            $trusted_evidence = $this->decode_meta_array($attachment_id, '_encypher_signed_rights_evidence');
            $rights_url = self::canonical_evidence_url($trusted_evidence);
            if ('' !== $rights_url) {
                $details .= '<br /><a href="' . esc_url($rights_url) . '" target="_blank" rel="noopener noreferrer">'
                    . esc_html__('View effective Encypher rights', 'encypher-provenance') . '</a>';
            }
            return '<div class="encypher-attachment-rights encypher-attachment-rights--signed" data-encypher-attachment-rights="1" data-encypher-signed-template="' . esc_attr($template) . '">'
                . '<strong>' . esc_html__('Signed license', 'encypher-provenance') . '</strong><br />'
                . '<span data-encypher-signed-rights-summary="1">' . esc_html($label) . '</span>'
                . $details
                . '<p class="description">'
                . esc_html__('These are the rights associated with this signed asset. Change them in your Encypher dashboard and re-sign to update.', 'encypher-provenance')
                . '</p></div>';
        }

        $rights = get_post_meta($attachment_id, '_encypher_rights', true);
        $rights = is_string($rights) ? json_decode($rights, true) : $rights;
        if (! is_array($rights)) {
            $rights = [];
        }
        $template  = isset($rights['template']) ? (string) $rights['template'] : '';
        $is_custom = 'custom' === $template;
        $name      = 'attachments[' . $attachment_id . ']';

        $html  = '<div class="encypher-attachment-rights" data-encypher-attachment-rights="1">';
        $html .= '<select class="encypher-attachment-rights__template" name="' . esc_attr($name . '[encypher_rights_template]') . '">';
        $html .= '<option value="">' . esc_html__('Use site default', 'encypher-provenance') . '</option>';
        if ('' !== $template && ! $is_custom) {
            // A saved id absent from the active catalog remains visible without
            // becoming a writable choice before the catalog enhancer loads.
            $html .= '<option value="' . esc_attr($template) . '" selected="selected" disabled="disabled">' . esc_html($template . ' (saved, read-only)') . '</option>';
        } elseif ($is_custom) {
            $html .= '<option value="custom" selected="selected" disabled="disabled">'
                . esc_html__('Saved custom terms (read-only)', 'encypher-provenance') . '</option>';
        }
        $html .= '</select>';

        $html .= '<p class="description">' . esc_html__('Applied when Encypher signs this asset. Manage license templates in your Encypher dashboard.', 'encypher-provenance') . '</p>';
        $html .= '</div>';

        return $html;
    }


    /**
     * Persist the per-asset license selection to the _encypher_rights attachment
     * meta in the same shape as the per-block encypherRights attribute.
     *
     * @param array<string,mixed> $post       Attachment post data (has ID).
     * @param array<string,mixed> $attachment Submitted attachment fields.
     * @return array<string,mixed> The unchanged post data.
     */
    public function save_attachment_rights(array $post, array $attachment): array
    {
        if (! Capabilities::allows(Capabilities::SEGMENT_RIGHTS)) {
            return $post;
        }
        if (! array_key_exists('encypher_rights_template', $attachment)) {
            return $post;
        }
        // Per-asset rights are an org-policy control: only editors and up
        // (edit_others_posts) may set or clear a license. Lower roles keep the
        // stored value untouched. Display of the field stays available to all
        // who can open the attachment (add_attachment_field is unchanged).
        if (! current_user_can('edit_others_posts')) {
            return $post;
        }
        $attachment_id = (int) ($post['ID'] ?? 0);
        if ($attachment_id <= 0) {
            return $post;
        }

        // Never overwrite rights on a signed asset or a verified source
        // manifest. Changing the license requires re-signing from the dashboard.
        $provenance_status = (string) get_post_meta($attachment_id, '_encypher_provenance_status', true);
        if (in_array($provenance_status, ['signed', 'verified', 'c2pa_protected', 'c2pa_verified'], true)) {
            return $post;
        }
        $source_rights = get_post_meta($attachment_id, '_encypher_source_c2pa_rights', true);
        $source_valid = (bool) get_post_meta($attachment_id, '_encypher_source_c2pa_valid', true);
        if ($source_valid && ! empty($source_rights)) {
            return $post;
        }

        $template = sanitize_text_field((string) $attachment['encypher_rights_template']);
        if ('' === $template) {
            delete_post_meta($attachment_id, '_encypher_rights');
            return $post;
        }

        // `custom` was a legacy local-authoring sentinel. It is now rendered as
        // a disabled read-only selection, so a crafted submission must not
        // create or replace custom rights. Existing saved custom data remains
        // untouched when attachment details are opened or saved.
        if ('custom' === $template) {
            return $post;
        }

        $rights = ['template' => $template];
        update_post_meta($attachment_id, '_encypher_rights', wp_json_encode($rights));
        return $post;
    }

    /**
     * Render the provenance filter dropdown above the media list table
     * (restrict_manage_posts, $which === 'bar' in list mode).
     *
     * @param string $post_type Current list-table post type (unused for media).
     * @param string $which     Table nav location: 'bar' for the media list.
     */
    public function render_media_provenance_filter($post_type = '', $which = ''): void
    {
        if ('bar' !== $which) {
            return;
        }
        $screen = function_exists('get_current_screen') ? get_current_screen() : null;
        if (! $screen || 'upload' !== $screen->id) {
            return;
        }

        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only list filter; narrows the query only, changes no state.
        $current = isset($_REQUEST['encypher_media_provenance']) ? sanitize_key(wp_unslash($_REQUEST['encypher_media_provenance'])) : '';
        $options = [
            ''          => __('All provenance', 'encypher-provenance'),
            'carries'   => __('Carries provenance', 'encypher-provenance'),
            'none'      => __('No provenance', 'encypher-provenance'),
            'unscanned' => __('Not yet scanned', 'encypher-provenance'),
        ];
        echo '<label class="screen-reader-text" for="encypher_media_provenance_filter">'
            . esc_html__('Filter by provenance', 'encypher-provenance') . '</label>';
        echo '<select name="encypher_media_provenance" id="encypher_media_provenance_filter">';
        foreach ($options as $value => $label) {
            echo '<option value="' . esc_attr($value) . '"' . selected($current, $value, false) . '>'
                . esc_html($label) . '</option>';
        }
        echo '</select>';
    }

    /**
     * Constrain the media list query by the selected provenance filter, using
     * the same MediaScan predicates as the coverage card (pre_get_posts).
     *
     * @param \WP_Query $query Current query.
     */
    public function filter_media_by_provenance($query): void
    {
        if (! is_admin() || ! ($query instanceof \WP_Query) || ! $query->is_main_query()) {
            return;
        }
        if ('attachment' !== $query->get('post_type')) {
            return;
        }
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only list filter; narrows the query only, changes no state.
        $filter = isset($_REQUEST['encypher_media_provenance']) ? sanitize_key(wp_unslash($_REQUEST['encypher_media_provenance'])) : '';
        if (! in_array($filter, ['carries', 'none', 'unscanned'], true) || ! class_exists(MediaScan::class)) {
            return;
        }

        $meta_query = MediaScan::media_meta_query($filter);
        $existing   = $query->get('meta_query');
        if (! empty($existing)) {
            $meta_query = ['relation' => 'AND', $existing, $meta_query];
        }
        $query->set('meta_query', $meta_query);
    }

    /**
     * Add the contextual "Sign with Encypher" row action to media list rows
     * (media_row_actions).
     *
     * @param array    $actions Existing row actions.
     * @param \WP_Post $post    Attachment post object.
     * @return array Row actions with the sign action appended.
     */
    public function add_media_row_action($actions, $post): array
    {
        if (! ($post instanceof \WP_Post) || 'attachment' !== $post->post_type) {
            return $actions;
        }
        if (! current_user_can('edit_post', $post->ID) || ! $this->attachment_is_signable($post->ID)) {
            return $actions;
        }

        $state = $this->classify($post->ID);
        if ('signing' === $state) {
            return $actions;
        }
        if ('vip_cache_purge_failed' === self::last_sign_error_code($post->ID)) {
            $actions['encypher_retry_public_delivery'] = '<a href="#" data-encypher-purge-retry="1" data-attachment-id="'
                . esc_attr((string) $post->ID) . '">' . esc_html__('Retry public delivery', 'encypher-provenance') . '</a>';
            return $actions;
        }
        $label = 'needs_attention' === $state
            ? __('Retry Encypher signing', 'encypher-provenance')
            : ('signed_by_you' === $state
                ? __('Re-sign with Encypher', 'encypher-provenance')
                : __('Sign with Encypher', 'encypher-provenance'));

        $url = wp_nonce_url(
            admin_url('admin-post.php?action=encypher_sign_attachment&attachment=' . $post->ID),
            'encypher_sign_attachment_' . $post->ID
        );
        $actions['encypher_sign_media'] = '<a href="' . esc_url($url) . '">' . esc_html($label) . '</a>';
        return $actions;
    }

    /**
     * Register the "Sign with Encypher" native bulk action on the media list
     * (bulk_actions-upload).
     *
     * @param array $actions Existing bulk actions.
     * @return array
     */
    public function add_media_bulk_action(array $actions): array
    {
        $actions['encypher_sign_media'] = __('Sign with Encypher', 'encypher-provenance');
        return $actions;
    }

    /**
     * Queue selected attachments onto the deferred attachment-sign path
     * (handle_bulk_actions-upload). Skips offloaded/missing and unsupported
     * files up front.
     *
     * @param string $redirect_to Redirect URL WordPress will send after the action.
     * @param string $doaction    Bulk action key.
     * @param array  $ids         Selected attachment IDs.
     * @return string Redirect URL with queued/skipped counts appended.
     */
    public function handle_media_bulk_action(string $redirect_to, string $doaction, array $ids): string
    {
        if ('encypher_sign_media' !== $doaction) {
            return $redirect_to;
        }
        $count   = 0;
        $skipped = 0;
        $user_id = get_current_user_id();
        foreach ($ids as $raw_id) {
            $attachment_id = (int) $raw_id;
            if (! $attachment_id || ! current_user_can('edit_post', $attachment_id)) {
                continue;
            }
            if (! $this->attachment_is_signable($attachment_id)) {
                $skipped++;
                continue;
            }
            if ($this->schedule_attachment_sign($attachment_id, $user_id)) {
                $count++;
            }
        }
        return add_query_arg(['encypher_media_signed' => $count, 'encypher_media_skipped' => $skipped], $redirect_to);
    }

    /**
     * Handle the single-row "Sign with Encypher" action (admin_post). Nonce +
     * capability protected.
     */
    public function handle_single_attachment_sign(): void
    {
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- nonce verified below via check_admin_referer.
        $attachment_id = isset($_GET['attachment']) ? (int) $_GET['attachment'] : 0;
        if (! $attachment_id || ! current_user_can('edit_post', $attachment_id)) {
            wp_die(esc_html__('You are not allowed to sign this attachment.', 'encypher-provenance'));
        }
        check_admin_referer('encypher_sign_attachment_' . $attachment_id);

        $signed  = 0;
        $skipped = 0;
        if ($this->attachment_is_signable($attachment_id)) {
            $this->schedule_attachment_sign($attachment_id, get_current_user_id());
            $signed = 1;
        } else {
            $skipped = 1;
        }

        $referer  = wp_get_referer();
        $redirect = $referer ? $referer : admin_url('upload.php');
        wp_safe_redirect(add_query_arg(['encypher_media_signed' => $signed, 'encypher_media_skipped' => $skipped], $redirect));
        exit;
    }

    /**
     * Notice after a media sign action queued (or skipped) attachments.
     */
    public function render_media_sign_notice(): void
    {
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only redirect flags written by the queue handlers; display a count only.
        if (! isset($_GET['encypher_media_signed']) && ! isset($_GET['encypher_media_skipped'])) {
            return;
        }
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only redirect flags.
        $count = isset($_GET['encypher_media_signed']) ? (int) $_GET['encypher_media_signed'] : 0;
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only redirect flags.
        $skipped = isset($_GET['encypher_media_skipped']) ? (int) $_GET['encypher_media_skipped'] : 0;
        if ($count > 0) {
            printf(
                '<div class="notice notice-success is-dismissible"><p>%s</p></div>',
                esc_html(sprintf(
                    /* translators: %d: number of attachments queued for signing. */
                    _n('%d attachment queued for signing with Encypher.', '%d attachments queued for signing with Encypher.', $count, 'encypher-provenance'),
                    $count
                ))
            );
        }
        if ($skipped > 0) {
            printf(
                '<div class="notice notice-warning is-dismissible"><p>%s</p></div>',
                esc_html(sprintf(
                    /* translators: %d: number of attachments skipped. */
                    _n('%d attachment was skipped (unsupported type or not available on this server).', '%d attachments were skipped (unsupported type or not available on this server).', $skipped, 'encypher-provenance'),
                    $skipped
                ))
            );
        }
    }

    /**
     * Whether an attachment can be signed: an attachment, of a supported MIME
     * type, whose original file exists locally (not offloaded).
     */
    private function attachment_is_signable(int $attachment_id): bool
    {
        if ('attachment' !== get_post_type($attachment_id)) {
            return false;
        }
        $mime = (string) get_post_mime_type($attachment_id);
        if (class_exists(Rest::class) && ! Rest::is_signable_media_mime($mime)) {
            return false;
        }
        $file = get_attached_file($attachment_id);
        return (bool) $file && file_exists($file);
    }

    /**
     * Queue an attachment onto the shared deferred-sign cron path with the
     * attachment routing marker. Returns true when scheduled (or already pending).
     */
    private function schedule_attachment_sign(int $attachment_id, int $user_id): bool
    {
        $args = [$attachment_id, false, $user_id, 'attachment'];
        if (! wp_next_scheduled('encypher_deferred_sign', $args)) {
            wp_schedule_single_event(time(), 'encypher_deferred_sign', $args);
        }
        delete_post_meta($attachment_id, '_encypher_last_sign_error');
        SigningStatus::acquire($attachment_id, 'media-queue');

        if (! (defined('DISABLE_WP_CRON') && DISABLE_WP_CRON) && function_exists('spawn_cron')) {
            spawn_cron();
        }
        return true;
    }

    /**
     * Enqueue the shared license-catalog client + the media-rights enhancer on
     * media screens so the per-asset license selector populates and toggles.
     *
     * @param string $hook Current admin page hook.
     */
    public function enqueue_media_rights_assets(string $hook): void
    {
        if (! in_array($hook, ['upload.php', 'post.php', 'post-new.php'], true)) {
            return;
        }
        if (Capabilities::allows(Capabilities::SEGMENT_RIGHTS)) {
            wp_enqueue_script(
                'encypher-license-catalog',
                ENCYPHER_PROVENANCE_PLUGIN_URL . 'assets/js/license-catalog.js',
                ['wp-api-fetch'],
                ENCYPHER_PROVENANCE_VERSION,
                true
            );
            wp_enqueue_script(
                'encypher-media-rights',
                ENCYPHER_PROVENANCE_PLUGIN_URL . 'assets/js/media-rights.js',
                ['jquery', 'wp-api-fetch', 'encypher-license-catalog'],
                ENCYPHER_PROVENANCE_VERSION,
                true
            );
        }
        wp_enqueue_script(
            'encypher-media-signing-status',
            ENCYPHER_PROVENANCE_PLUGIN_URL . 'assets/js/media-signing-status.js',
            ['wp-api-fetch'],
            ENCYPHER_PROVENANCE_VERSION,
            true
        );
        if (function_exists('wp_localize_script')) {
            wp_localize_script(
                'encypher-media-signing-status',
                'EncypherProvenanceConfig',
                [
                    'markUrl'   => ENCYPHER_PROVENANCE_PLUGIN_URL . 'assets/images/brand/encypher-mark-navy.svg',
                    'loaderUrl' => ENCYPHER_PROVENANCE_PLUGIN_URL . 'assets/images/brand/encypher-loader-navy.svg',
                ]
            );
        }
    }

    /**
     * Canonical packaged status artwork for Media Library provenance states.
     *
     * @param bool $loading Whether the status represents active signing.
     * @return string Image markup.
     */
    private static function status_icon(bool $loading = false): string
    {
        $asset = $loading
            ? 'assets/images/brand/encypher-loader-navy.svg'
            : 'assets/images/brand/encypher-mark-navy.svg';

        return '<img class="encypher-status-icon" src="' . esc_url(ENCYPHER_PROVENANCE_PLUGIN_URL . $asset) . '"'
            . ' alt="" width="16" height="16" style="vertical-align:text-bottom;" aria-hidden="true" />';
    }

    /**
     * Allowed HTML for the provenance indicator.
     *
     * `wp_kses_allowed_html('post')` permits no `data-*` attribute on any
     * element, so the column's own state hooks were being stripped on the way
     * out: the polling script's data-encypher-media-status/data-status pair
     * never reached the list table, and the verdict and trust attributes would
     * have gone the same way. They are declared explicitly here. `details`,
     * `summary`, and `pre` are declared too rather than assumed, so the source
     * manifest disclosure does not depend on which WordPress version's post
     * allowlist happens to include them.
     *
     * @return array Kses allowlist.
     */
    private static function indicator_allowed_html(): array
    {
        return array_merge(
            wp_kses_allowed_html('post'),
            [
                'a'   => [
                    'href'   => true,
                    'title'  => true,
                    'class'  => true,
                    'target' => true,
                    'rel'    => true,
                ],
                'button' => [
                    'type' => true,
                    'class' => true,
                    'data-encypher-purge-retry' => true,
                    'data-attachment-id' => true,
                ],
                'img' => [
                    'src'         => true,
                    'alt'         => true,
                    'class'       => true,
                    'width'       => true,
                    'height'      => true,
                    'style'       => true,
                    'aria-hidden' => true,
                ],
                'span' => [
                    'class' => true,
                    'title' => true,
                    'style' => true,
                    'data-encypher-media-status' => true,
                    'data-attachment-id'         => true,
                    'data-status'                => true,
                    'data-encypher-verdict'      => true,
                    'data-encypher-media-trust'  => true,
                ],
                'details' => ['class' => true],
                'summary' => ['class' => true],
                'pre'     => ['class' => true],
                'strong'  => ['class' => true],
                'br'      => [],
            ]
        );
    }
}
