<?php
namespace EncypherProvenance;

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

require_once __DIR__ . '/class-encypher-editorial-provenance.php';
require_once __DIR__ . '/class-encypher-capabilities.php';

class SignRequestBuilder
{
    private const OPTION_KEY = 'encypher_provenance_settings';

    private const VALID_DOCUMENT_TYPES = ['article', 'legal_brief', 'contract', 'ai_output'];
    private const VALID_SEGMENTATION_LEVELS = ['document', 'sentence', 'paragraph', 'section', 'word'];
    private const VALID_EMBEDDING_STRATEGIES = ['single_point', 'distributed', 'distributed_redundant'];
    private const VALID_MANIFEST_MODES = ['full', 'micro'];
    private const VALID_MERKLE_SEGMENTATION_LEVELS = ['sentence', 'paragraph', 'section'];
    private const VALID_EMBEDDING_FORMATS = ['plain', 'html', 'markdown', 'json'];
    private const VALID_EMBEDDING_METHODS = ['invisible', 'data-attribute', 'span', 'comment'];
    private const VALID_DISTRIBUTION_TARGETS = ['whitespace', 'punctuation', 'all_chars'];

    private const IPTC_AI_SOURCE = 'http://cv.iptc.org/newscodes/digitalsourcetype/trainedAlgorithmicMedia';
    private const IPTC_HUMAN_DIGITAL_SOURCE = 'http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation';

    public static function get_settings(): array
    {
        $settings = get_option(self::OPTION_KEY, []);
        return is_array($settings) ? $settings : [];
    }

    /**
     * Build the server-owned WordPress identity carried by every sign request.
     *
     * The authenticated organization scopes the logical asset on the backend;
     * the persistent installation id and native post id distinguish assets
     * within that organization. Each durable generation receives an immutable
     * document id, while the operation key remains the generation token itself.
     *
     * @return array{logical_asset_id:string,document_id:string,operation_key:string,metadata:array<string,mixed>}
     */
    public static function build_wordpress_inventory_contract(
        array $settings,
        int $post_id,
        string $generation,
        array $metadata = []
    ): array {
        $install_id = trim((string) ($settings['install_id'] ?? ''));
        $generation = trim($generation);
        if ('' === $install_id || $post_id <= 0 || '' === $generation || strlen($generation) > 128) {
            throw new \InvalidArgumentException('WordPress signing requires a persistent install id, post id, and bounded generation token.');
        }

        $logical_asset_id = sprintf('wordpress:%s:post:%d', $install_id, $post_id);
        $document_id = 'wpg_' . substr(hash('sha256', $logical_asset_id . "\0" . $generation), 0, 60);

        // Reserved provenance keys always win over editor/caller metadata.
        $metadata['source'] = 'wordpress';
        $metadata['wordpress_install_id'] = substr($install_id, 0, 128);
        $metadata['wordpress_post_id'] = $post_id;

        return [
            'logical_asset_id' => $logical_asset_id,
            'document_id' => $document_id,
            'operation_key' => $generation,
            'metadata' => $metadata,
        ];
    }

    public static function build_text_sign_payload(
        string $text,
        array $context = [],
        ?array $settings = null
    ): array {
        if ($settings === null) {
            $settings = self::get_settings();
        }

        $options = self::build_options($settings, $context);

        $payload = [
            'text' => $text,
            'options' => $options,
        ];

        if (!empty($context['document_id'])) {
            $payload['document_id'] = $context['document_id'];
        }
        if (!empty($context['document_title'])) {
            $payload['document_title'] = $context['document_title'];
        }
        if (!empty($context['work_id'])) {
            $payload['work_id'] = $context['work_id'];
        }
        if (!empty($context['document_url'])) {
            $payload['document_url'] = $context['document_url'];
        }
        if (!empty($context['metadata'])) {
            $payload['metadata'] = $context['metadata'];
        }

        // publisher_org_id (strategic_partner proxy signing)
        if (!empty($context['publisher_org_id'])) {
            $payload['publisher_org_id'] = $context['publisher_org_id'];
        } elseif (!empty($settings['publisher_org_id'])) {
            $payload['publisher_org_id'] = $settings['publisher_org_id'];
        }

        return $payload;
    }

    public static function build_batch_sign_payload(
        array $documents,
        ?array $settings = null
    ): array {
        if ($settings === null) {
            $settings = self::get_settings();
        }

        $batch_limit = Capabilities::allows(Capabilities::BULK_OPERATIONS) ? 100 : 10;

        $docs = array_slice($documents, 0, $batch_limit);
        $formatted = [];
        foreach ($docs as $doc) {
            $entry = ['text' => $doc['text']];
            if (!empty($doc['document_id'])) {
                $entry['document_id'] = $doc['document_id'];
            }
            if (!empty($doc['document_title'])) {
                $entry['document_title'] = $doc['document_title'];
            }
            if (!empty($doc['document_url'])) {
                $entry['document_url'] = $doc['document_url'];
            }
            if (!empty($doc['metadata'])) {
                $entry['metadata'] = $doc['metadata'];
            }
            $formatted[] = $entry;
        }

        $options = self::build_options($settings, []);

        return [
            'documents' => $formatted,
            'options' => $options,
        ];
    }

    public static function build_media_sign_payload(
        string $file_path,
        array $context = [],
        ?array $settings = null
    ): array {
        if ($settings === null) {
            $settings = self::get_settings();
        }

        $trustmark_entitled = Capabilities::allows(Capabilities::TRUSTMARK_WATERMARK);

        $payload = [
            'file_path' => $file_path,
            'action' => 'c2pa.published',
        ];
        if (! empty($context['digital_source_type'])) {
            $payload['digital_source_type'] = (string) $context['digital_source_type'];
        } elseif (! empty($settings['digital_source_type'])) {
            $payload['digital_source_type'] = (string) $settings['digital_source_type'];
        } else {
            $payload['digital_source_type'] = self::IPTC_HUMAN_DIGITAL_SOURCE;
        }

        if (!empty($context['title'])) {
            $payload['title'] = $context['title'];
        }
        if (!empty($context['original_url'])) {
            $payload['original_url'] = $context['original_url'];
        }

        if (!empty($context['asset_id'])) {
            $payload['asset_id'] = (string) $context['asset_id'];
        }
        if (!empty($context['source'])) {
            $payload['source'] = (string) $context['source'];
        }
        if (!empty($context['source_metadata']) && is_array($context['source_metadata'])) {
            $payload['source_metadata'] = $context['source_metadata'];
        }

        if ($trustmark_entitled) {
            $payload['enable_image_watermark'] = !empty($settings['enable_image_watermark']);
        }

        // Standalone media has no JSON options object. Keep the three rights
        // inputs separate: a selected dashboard schema, deliberate custom
        // fields, or the site profile fallback when neither is present.
        $preserve_source_rights = ! empty($context['preserve_source_rights']);
        $asset_rights_entitled = Capabilities::allows(Capabilities::SEGMENT_RIGHTS);
        $has_schema = $asset_rights_entitled
            && ! $preserve_source_rights
            && ! empty($context['rights_template_id']);
        $has_custom_rights = $asset_rights_entitled && ! $preserve_source_rights && (
            ! empty($context['rights_custom_override'])
            || (! empty($context['rights']) && is_array($context['rights']))
            || ! empty($context['license_type'])
        );
        $payload['use_rights_profile'] = ! $preserve_source_rights
            && ! $has_schema
            && ! $has_custom_rights
            && ! empty($settings['use_rights_profile']);
        if ($has_schema) {
            $payload['rights_template_id'] = (string) $context['rights_template_id'];
        } elseif ($has_custom_rights) {
            if (! empty($context['rights']) && is_array($context['rights'])) {
                $payload['rights'] = $context['rights'];
            }
            if (! empty($context['license_type'])) {
                $payload['license_type'] = (string) $context['license_type'];
            }
            if (! empty($context['license_url'])) {
                $payload['license_url'] = (string) $context['license_url'];
            }
        }
        if (!empty($context['preserve_source_rights'])) {
            $payload['preserve_source_rights'] = true;
        }
        if (! empty($context['ingredients']) && is_array($context['ingredients'])) {
            $payload['ingredients'] = $context['ingredients'];
        }

        return $payload;
    }

    /**
     * Maximum total raw media size (before base64 encoding) for a /sign/rich request.
     */
    private const MAX_RICH_PAYLOAD_BYTES = 52_428_800; // 50 MB

    /**
     * Build a payload for POST /sign/rich (composite document signing).
     *
     * @param string $text        Post content (text/HTML).
     * @param array  $media_items Array of media descriptors from MediaBlockExtractor.
     * @param array  $context     Document context (document_id, document_title, etc.).
     * @param ?array $settings    Plugin settings (null = read from DB).
     *
     * @return array JSON-serializable payload matching RichArticleSignRequest schema.
     */
    public static function build_rich_sign_payload(
        string $text,
        array $media_items,
        array $context = [],
        ?array $settings = null
    ): array {
        if ($settings === null) {
            $settings = self::get_settings();
        }

        // Every commercial option is controlled by Encypher's versioned
        // capability projection, never by a local tier value.
        $base_options = self::build_options($settings, $context);
        $rich_options = self::build_rich_options($base_options, $settings);
        // CMS media already exists before Encypher signs the compound asset.
        // The first rich sign therefore opens the document rather than claiming
        // that Encypher created it. Editorial re-sign actions pass through.
        if (($rich_options['action'] ?? '') === 'c2pa.created') {
            $rich_options['action'] = 'c2pa.opened';
        }

        $content_format = 'html';
        if (isset($context['content_format']) && in_array($context['content_format'], ['html', 'markdown', 'plain'], true)) {
            $content_format = $context['content_format'];
        }

        $payload = [
            'content'        => $text,
            'content_format' => $content_format,
            'options'        => $rich_options,
        ];

        if (!empty($context['document_id'])) {
            $payload['document_id'] = $context['document_id'];
        }
        if (!empty($context['work_id'])) {
            $payload['work_id'] = $context['work_id'];
        }
        if (!empty($context['document_title'])) {
            $payload['document_title'] = $context['document_title'];
        }
        if (!empty($context['document_url'])) {
            $payload['document_url'] = $context['document_url'];
        }
        if (!empty($context['metadata'])) {
            $payload['metadata'] = $context['metadata'];
        }
        if (!empty($context['publisher_org_id'])) {
            $payload['publisher_org_id'] = $context['publisher_org_id'];
        } elseif (!empty($settings['publisher_org_id'])) {
            $payload['publisher_org_id'] = $settings['publisher_org_id'];
        }

        // Partition media items by type and encode file data as base64.
        $images = [];
        $audios = [];
        $videos = [];
        $total_bytes = 0;

        foreach ($media_items as $item) {
            // Enforce cumulative payload size limit.
            if ($total_bytes + $item['file_size'] > self::MAX_RICH_PAYLOAD_BYTES) {
                break;
            }

            $file_data = array_key_exists('_encypher_original_bytes', $item)
                ? $item['_encypher_original_bytes']
                : @file_get_contents($item['file_path']);
            if ($file_data === false) {
                continue;
            }

            $b64 = base64_encode($file_data);
            $total_bytes += $item['file_size'];

            $entry = [
                'data'      => $b64,
                'filename'  => $item['filename'],
                'mime_type' => $item['mime_type'],
                'position'  => $item['position'],
            ];

            // Include source_block for C2PA compound content role mapping
            if (!empty($item['source_block'])) {
                $entry['source_block'] = $item['source_block'];
            }

            // A verified source rights block remains authoritative for an
            // already-signed asset. The server keeps it in the signed parent
            // ingredient and must not layer block, attachment, document, or
            // site rights onto the new media manifest.
            if (!empty($item['preserve_source_rights'])) {
                $entry['preserve_source_rights'] = true;
            } elseif (Capabilities::allows(Capabilities::SEGMENT_RIGHTS)) {
                if (!empty($item['rights'])) {
                    $entry['rights'] = $item['rights'];
                }
                if (!empty($item['rights_template_id'])) {
                    $entry['rights_template_id'] = (string) $item['rights_template_id'];
                }
            }

            // Flag assets with existing C2PA provenance for ingredient chain.
            if (!empty($item['has_existing_c2pa'])) {
                $entry['has_existing_c2pa'] = true;
            }

            switch ($item['media_type']) {
                case 'image':
                    if (!empty($item['alt_text'])) {
                        $entry['alt_text'] = $item['alt_text'];
                    }
                    $images[] = $entry;
                    break;
                case 'audio':
                    $audios[] = $entry;
                    break;
                case 'video':
                    $videos[] = $entry;
                    break;
            }
        }

        if (!empty($images)) {
            $payload['images'] = $images;
        }
        if (!empty($audios)) {
            $payload['audios'] = $audios;
        }
        if (!empty($videos)) {
            $payload['videos'] = $videos;
        }

        return $payload;
    }

    /**
     * Build the RichSignOptions sub-object from base text options + rich-specific settings.
     */
    private static function build_rich_options(array $base_options, array $settings): array
    {
        $rich = [
            'segmentation_level'    => $base_options['segmentation_level'] ?? 'sentence',
            // Keep the immutable C2PA manifest server-side and return only
            // sentence leaf pointers in the rendered text. Interleaving the
            // document wrapper with those pointers prevents a copied sentence
            // from carrying one resolvable pointer window.
            'manifest_mode'         => $base_options['manifest_mode'] ?? 'micro',
            'embed_c2pa'            => !empty($base_options['embed_c2pa']),
            'fragment_pointers_only'=> !empty($base_options['fragment_pointers_only']),
            'ecc'                   => array_key_exists('ecc', $base_options) ? (bool) $base_options['ecc'] : true,
            'legacy_safe'           => !empty($base_options['legacy_safe']),
            'action'                => $base_options['action'] ?? 'c2pa.created',
            'document_type'         => $base_options['document_type'] ?? 'article',
            'use_rights_profile'    => $base_options['use_rights_profile'] ?? true,
            'index_for_attribution' => $base_options['index_for_attribution'] ?? true,
            // WordPress signs the final visible text after the_content filters.
            // Dual leaf pointers make each rendered sentence independently
            // verifiable when readers select or copy a fragment.
            'fragment_pointers'      => !empty($base_options['fragment_pointers']),
            'generation_pointer'     => !array_key_exists('generation_pointer', $settings)
                || (bool) $settings['generation_pointer'],
        ];

        // Provenance chain: forward the prior instance id so a c2pa.edited
        // rich re-sign produces the same created -> edited ingredient chain
        // as the text-only /sign path.
        if (!empty($base_options['previous_instance_id'])) {
            $rich['previous_instance_id'] = $base_options['previous_instance_id'];
        }
        if (!empty($base_options['previous_signed_content'])) {
            $rich['previous_signed_content'] = $base_options['previous_signed_content'];
        }
        if (!empty($base_options['editorial_provenance'])) {
            $rich['editorial_provenance'] = $base_options['editorial_provenance'];
        }
        if (!empty($base_options['article_metadata'])) {
            $rich['article_metadata'] = $base_options['article_metadata'];
        }

        // AI-content labeling: the IPTC digital source type survives the
        // rich path when the base options carry one.
        if (!empty($base_options['digital_source_type'])) {
            $rich['digital_source_type'] = $base_options['digital_source_type'];
        }

        // Rich-specific paid features use independent server capabilities so
        // an Encypher-admin add-on can grant one without granting the rest.
        if (Capabilities::allows(Capabilities::TRUSTMARK_WATERMARK)) {
            $rich['enable_trustmark'] = !empty($settings['enable_trustmark']);
        }
        if (Capabilities::allows(Capabilities::AUDIO_WATERMARK)) {
            $rich['enable_audio_watermark'] = !empty($settings['enable_audio_watermark']);
        }
        if (Capabilities::allows(Capabilities::VIDEO_WATERMARK)) {
            $rich['enable_video_watermark'] = !empty($settings['enable_video_watermark']);
        }
        $quality = isset($settings['image_quality']) ? (int) $settings['image_quality'] : 95;
        $rich['image_quality'] = ($quality >= 1 && $quality <= 100) ? $quality : 95;
        $rich['use_rights_profile'] = (bool) ($base_options['use_rights_profile'] ?? false);
        if (!empty($base_options['rights_template_id'])) {
            $rich['rights_template_id'] = $base_options['rights_template_id'];
        }
        if (!empty($base_options['rights'])) {
            $rich['rights'] = $base_options['rights'];
        }
        if (!empty($base_options['license'])) {
            $rich['license'] = $base_options['license'];
        }
        if (
            !empty($base_options['segment_rights'])
            && Capabilities::allows(Capabilities::SEGMENT_RIGHTS)
        ) {
            $rich['segment_rights'] = $base_options['segment_rights'];
        }

        return $rich;
    }

    private static function build_options(array $settings, array $context): array
    {
        $advanced_signing = Capabilities::allows(Capabilities::ADVANCED_SIGNING);
        $is_ai_content = !empty($context['is_ai_content']);

        $options = [];

        // document_type
        if ($is_ai_content) {
            $options['document_type'] = 'ai_output';
        } else {
            $doc_type = $context['document_type'] ?? $settings['document_type'] ?? 'article';
            if (in_array($doc_type, self::VALID_DOCUMENT_TYPES, true)) {
                $options['document_type'] = $doc_type;
            } else {
                $options['document_type'] = 'article';
            }
        }

        // claim_generator
        $options['claim_generator'] = $context['claim_generator']
            ?? 'WordPress/Encypher Plugin v' . (defined('ENCYPHER_PROVENANCE_VERSION') ? ENCYPHER_PROVENANCE_VERSION : '0.0.0');

        // action (c2pa.created or c2pa.edited)
        $options['action'] = $context['action'] ?? 'c2pa.created';

        // previous_instance_id for edit provenance chain
        if (!empty($context['previous_instance_id'])) {
            $options['previous_instance_id'] = $context['previous_instance_id'];
        }
        if (!empty($context['previous_signed_content'])) {
            $options['previous_signed_content'] = $context['previous_signed_content'];
        }
        if (!empty($context['editorial_provenance']) && is_array($context['editorial_provenance'])) {
            $editorial = EditorialProvenance::sanitize_api_object($context['editorial_provenance']);
            if (!empty($editorial)) {
                $options['editorial_provenance'] = $editorial;
            }
        }
        if (!empty($context['article_metadata']) && is_array($context['article_metadata'])) {
            $options['article_metadata'] = $context['article_metadata'];
        }

        // Every c2pa.created action requires a digitalSourceType under the
        // C2PA 2.4 text-embedding contract. WordPress posts are human digital
        // creations unless the editor or site explicitly declares otherwise.
        if ($is_ai_content) {
            $options['digital_source_type'] = self::IPTC_AI_SOURCE;
        } elseif (!empty($settings['digital_source_type'])) {
            $options['digital_source_type'] = $settings['digital_source_type'];
        } elseif ('c2pa.created' === $options['action']) {
            $options['digital_source_type'] = self::IPTC_HUMAN_DIGITAL_SOURCE;
        }

        // The Rust dual-pointer signer cannot yet carry an edit ingredient.
        // Preserve the created -> edited chain by using the standard full
        // manifest path for edits; fresh publications retain leaf pointers.
        $has_edit_parent = 'c2pa.edited' === $options['action']
            && !empty($options['previous_instance_id']);
        $fragment_pointers_enabled = $advanced_signing
            && ! $has_edit_parent
            && (!array_key_exists('fragment_pointers', $settings) || (bool) $settings['fragment_pointers']);

        // Pointer-only output preserves one independently verifiable pointer
        // window per sentence. Its signed C2PA manifest remains server-side.
        if ($fragment_pointers_enabled) {
            $options['segmentation_level'] = 'document';
        } else {
            $seg_level = $settings['segmentation_level'] ?? 'sentence';
            if (!in_array($seg_level, self::VALID_SEGMENTATION_LEVELS, true)) {
                $seg_level = 'sentence';
            }
            if ($seg_level === 'word' && !$advanced_signing) {
                $seg_level = 'sentence';
            }
            $options['segmentation_level'] = $seg_level;
        }

        // segmentation_levels (multi-level Merkle: build multiple trees in one pass)
        if (!empty($context['segmentation_levels']) && is_array($context['segmentation_levels'])) {
            $valid_levels = array_filter($context['segmentation_levels'], function ($l) {
                return in_array($l, self::VALID_MERKLE_SEGMENTATION_LEVELS, true);
            });
            if (!empty($valid_levels)) {
                $options['segmentation_levels'] = array_values($valid_levels);
            }
        } elseif (!empty($settings['segmentation_levels']) && is_array($settings['segmentation_levels'])) {
            $valid_levels = array_filter($settings['segmentation_levels'], function ($l) {
                return in_array($l, self::VALID_MERKLE_SEGMENTATION_LEVELS, true);
            });
            if (!empty($valid_levels)) {
                $options['segmentation_levels'] = array_values($valid_levels);
            }
        }

        // manifest_mode
        $manifest_mode = $settings['manifest_mode'] ?? 'micro';
        if (in_array($manifest_mode, self::VALID_MANIFEST_MODES, true)) {
            $options['manifest_mode'] = $manifest_mode;
        } else {
            $options['manifest_mode'] = 'micro';
        }
        if ($fragment_pointers_enabled) {
            $options['manifest_mode'] = 'full';
        }

        // ecc (Reed-Solomon error correction for micro mode)
        $options['ecc'] = (bool) ($settings['ecc'] ?? true);

        // A C2PA wrapper interleaved into the text obscures sentence pointer
        // windows. Pointer-only output retains that manifest server-side.
        $options['embed_c2pa'] = $fragment_pointers_enabled
            ? false
            : (bool) ($settings['embed_c2pa'] ?? true);

        // legacy_safe
        if (!empty($settings['legacy_safe'])) {
            $options['legacy_safe'] = true;
        }

        // embedding_strategy
        $embed_strategy = $settings['embedding_strategy'] ?? 'single_point';
        if (in_array($embed_strategy, self::VALID_EMBEDDING_STRATEGIES, true)) {
            $options['embedding_strategy'] = $embed_strategy;
        }
        if ($embed_strategy !== 'single_point') {
            $dist_target = $settings['distribution_target'] ?? '';
            if (in_array($dist_target, self::VALID_DISTRIBUTION_TARGETS, true)) {
                $options['distribution_target'] = $dist_target;
            }
        }

        // index_for_attribution
        $options['index_for_attribution'] = (bool) ($settings['index_for_attribution'] ?? true);

        // return_embedding_plan (always true for WordPress - needed for HTML reconstruction)
        $options['return_embedding_plan'] = true;

        // Rights inputs are mutually exclusive. A selected opaque dashboard
        // schema is authoritative. Deliberate document custom fields are next.
        // Site settings apply only when the document made no selection.
        $has_schema = ! empty($context['rights_template_id']);
        $has_custom_rights = ! empty($context['rights_custom_override'])
            || (! empty($context['rights']) && is_array($context['rights']))
            || ! empty($context['license_type']);
        if ($has_schema) {
            $options['rights_template_id'] = (string) $context['rights_template_id'];
        } elseif ($has_custom_rights) {
            if (! empty($context['rights']) && is_array($context['rights'])) {
                $options['rights'] = $context['rights'];
            }
            $license = self::build_license([], $context);
            if (! empty($license)) {
                $options['license'] = $license;
            }
        } else {
            $rights = self::build_rights_metadata($settings);
            if (! empty($rights)) {
                $options['rights'] = $rights;
            }
            $license = self::build_license($settings, []);
            if (! empty($license)) {
                $options['license'] = $license;
            }
        }

        // Never let the server profile fallback overlay a selected schema or
        // deliberate custom document policy.
        $options['use_rights_profile'] = ! $has_schema
            && ! $has_custom_rights
            && ! empty($settings['use_rights_profile']);

        // custom_assertions (passed through from context for programmatic use)
        if (!empty($context['custom_assertions']) && is_array($context['custom_assertions'])) {
            $options['custom_assertions'] = $context['custom_assertions'];
        }

        // validate_assertions (defaults to true on API side; only send when explicitly false)
        if (isset($context['validate_assertions']) && $context['validate_assertions'] === false) {
            $options['validate_assertions'] = false;
        } elseif (isset($settings['validate_assertions']) && $settings['validate_assertions'] === false) {
            $options['validate_assertions'] = false;
        }

        // template_id (passed through from context for programmatic use)
        if (!empty($context['template_id'])) {
            $options['template_id'] = $context['template_id'];
        }

        // actions (C2PA action assertions list, context-driven)
        if (!empty($context['actions']) && is_array($context['actions'])) {
            $options['actions'] = $context['actions'];
        }

        // embedding_options (output format control)
        $embedding_opts = [];
        $embed_format = $context['embedding_format'] ?? $settings['embedding_format'] ?? '';
        if ($embed_format && in_array($embed_format, self::VALID_EMBEDDING_FORMATS, true)) {
            $embedding_opts['format'] = $embed_format;
        }
        $embed_method = $context['embedding_method'] ?? $settings['embedding_method'] ?? '';
        if ($embed_method && in_array($embed_method, self::VALID_EMBEDDING_METHODS, true)) {
            $embedding_opts['method'] = $embed_method;
        }
        if (isset($context['embedding_include_text'])) {
            $embedding_opts['include_text'] = (bool) $context['embedding_include_text'];
        } elseif (isset($settings['embedding_include_text'])) {
            $embedding_opts['include_text'] = (bool) $settings['embedding_include_text'];
        }
        if (!empty($embedding_opts)) {
            $options['embedding_options'] = $embedding_opts;
        }

        // expires_at
        if (!empty($settings['expires_at'])) {
            $options['expires_at'] = $settings['expires_at'];
        }

        // Advanced signing is one server capability; independently sellable
        // segment rights remains a separate grant.
        if ($advanced_signing) {
            if (!empty($settings['add_dual_binding'])) {
                $options['add_dual_binding'] = true;
            }
            if (!empty($settings['include_fingerprint'])) {
                $options['include_fingerprint'] = true;
            }
            if (!empty($settings['enable_print_fingerprint'])) {
                $options['enable_print_fingerprint'] = true;
            }
            if (!empty($settings['enable_print_micro_ecc'])) {
                $options['enable_print_micro_ecc'] = true;
            }
            if (isset($settings['store_c2pa_manifest']) && $settings['store_c2pa_manifest'] === false) {
                $options['store_c2pa_manifest'] = false;
            }
            if (!empty($settings['disable_c2pa'])) {
                $options['disable_c2pa'] = true;
            }
            $options['fragment_pointers'] = $fragment_pointers_enabled;
            $options['fragment_pointers_only'] = $fragment_pointers_enabled;
            $options['generation_pointer'] = !array_key_exists('generation_pointer', $settings)
                || (bool) $settings['generation_pointer'];
        }
        if (
            Capabilities::allows(Capabilities::SEGMENT_RIGHTS)
            && !empty($context['segment_rights'])
            && is_array($context['segment_rights'])
        ) {
            $options['segment_rights'] = $context['segment_rights'];
        }

        return $options;
    }

    private static function build_rights_metadata(array $settings): array
    {
        $rights = [];

        if (!empty($settings['rights_copyright_holder'])) {
            $rights['copyright_holder'] = $settings['rights_copyright_holder'];
        }
        if (!empty($settings['rights_license_url'])) {
            $rights['license_url'] = $settings['rights_license_url'];
        }
        if (!empty($settings['rights_usage_terms'])) {
            $rights['usage_terms'] = $settings['rights_usage_terms'];
        }
        if (isset($settings['rights_syndication_allowed'])) {
            $rights['syndication_allowed'] = (bool) $settings['rights_syndication_allowed'];
        }
        if (!empty($settings['rights_embargo_until'])) {
            $rights['embargo_until'] = $settings['rights_embargo_until'];
        }
        if (!empty($settings['rights_contact_email'])) {
            $rights['contact_email'] = $settings['rights_contact_email'];
        }
        if (isset($settings['rights_ai_training'])) {
            $rights['ai_training'] = (bool) $settings['rights_ai_training'];
        }
        if (isset($settings['rights_ai_inference'])) {
            $rights['ai_inference'] = (bool) $settings['rights_ai_inference'];
        }
        if (isset($settings['rights_data_mining'])) {
            $rights['data_mining'] = (bool) $settings['rights_data_mining'];
        }

        return $rights;
    }

    private static function build_license(array $settings, array $context): array
    {
        $license = [];

        if (!empty($context['license_type'])) {
            $license['type'] = $context['license_type'];
        } elseif (!empty($settings['license_type'])) {
            $license['type'] = $settings['license_type'];
        }

        if (!empty($context['license_url'])) {
            $license['url'] = $context['license_url'];
        } elseif (!empty($settings['license_url'])) {
            $license['url'] = $settings['license_url'];
        }

        if (!empty($context['license_contact_email'])) {
            $license['contact_email'] = $context['license_contact_email'];
        } elseif (!empty($settings['rights_contact_email'])) {
            $license['contact_email'] = $settings['rights_contact_email'];
        }

        if (empty($license['type'])) {
            return [];
        }

        return $license;
    }
}
