<?php
namespace EncypherProvenance;

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

/**
 * Product-scoped forwarding surface for the full C2PA Conformance Program matrix.
 *
 * The caller selects WordPress attachment IDs. WordPress reads those files from
 * its own media store and sends them through the API credential configured for
 * this installation. The API remains the format and signing-policy authority.
 */
final class AllFormatForwarder
{
    private const REST_NAMESPACE = 'encypher-provenance/v1';
    private const MAX_FILES = 513;
    private const MAX_TOTAL_BYTES = 134217728;

    private const ROUTES = [
        'text' => ['sign' => 'sign', 'verify' => 'public/verify-text'],
        'media' => ['sign' => 'sign/media', 'verify' => 'public/verify/media'],
        'declared-media' => ['sign' => 'sign/declared-media', 'verify' => 'verify/declared-media'],
        'model' => ['sign' => 'sign/model', 'verify' => 'verify/model'],
        'model-collection' => ['sign' => 'sign/model/collection', 'verify' => 'verify/model/collection'],
        'stream' => ['sign' => 'sign/stream', 'verify' => 'verify/stream'],
    ];

    public function register_hooks(): void
    {
        add_action('rest_api_init', [$this, 'register_routes']);
    }

    public function register_routes(): void
    {
        register_rest_route(self::REST_NAMESPACE, '/all-formats/forward', [
            'methods' => 'POST',
            'callback' => [$this, 'forward'],
            'permission_callback' => static function (): bool {
                return current_user_can('manage_options');
            },
        ]);
    }

    /**
     * Forward one complete all-format operation to the Encypher API.
     *
     * Request body:
     * - operation: sign|verify
     * - family: text|media|declared-media|model|model-collection|stream
     * - attachment_ids or multipart files[]: ordered source files
     * - manifest_attachment_id or multipart manifest: detached model manifest
     * - format_token: MIME, generic-container, or ML-model token
     * - paths: component paths for a model collection
     */
    public function forward(\WP_REST_Request $request)
    {
        $operation = sanitize_key((string) $request->get_param('operation'));
        $family = sanitize_key((string) $request->get_param('family'));
        if (! in_array($operation, ['sign', 'verify'], true)) {
            return new \WP_Error('invalid_operation', __('Operation must be sign or verify.', 'encypher-provenance'), ['status' => 422]);
        }
        if (! isset(self::ROUTES[$family])) {
            return new \WP_Error('invalid_format_family', __('Unsupported all-format family.', 'encypher-provenance'), ['status' => 422]);
        }

        $files = $this->resolve_source_files($request);
        if (is_wp_error($files)) {
            return $files;
        }

        $manifest = null;
        if ('verify' === $operation && in_array($family, ['model', 'model-collection'], true)) {
            $manifest = $this->resolve_manifest($request);
            if (is_wp_error($manifest)) {
                return $manifest;
            }
        }

        $total_bytes = array_sum(array_column($files, 'size')) + (is_array($manifest) ? (int) $manifest['size'] : 0);
        if ($total_bytes > self::MAX_TOTAL_BYTES) {
            return new \WP_Error('forwarding_payload_too_large', __('The selected files exceed the 128 MiB forwarding limit.', 'encypher-provenance'), ['status' => 413]);
        }

        $settings = get_option('encypher_provenance_settings', []);
        $api_key = trim((string) ($settings['api_key'] ?? ''));
        if ('' === $api_key) {
            return new \WP_Error('missing_api_key', __('Connect this site to Encypher before forwarding files.', 'encypher-provenance'), ['status' => 401]);
        }
        $api_url = $this->api_url((string) ($settings['api_base_url'] ?? ''), self::ROUTES[$family][$operation]);
        if (is_wp_error($api_url)) {
            return $api_url;
        }

        $payload = [
            'operation' => $operation,
            'family' => $family,
            'format_token' => sanitize_text_field((string) $request->get_param('format_token')),
            'spec' => '2.2' === (string) $request->get_param('spec') ? '2.2' : '2.4',
            'compress_manifest' => rest_sanitize_boolean($request->get_param('compress_manifest')),
            'encapsulation' => 'CMAF' === (string) $request->get_param('encapsulation') ? 'CMAF' : 'fMP4',
            'method' => 'per-segment' === (string) $request->get_param('method') ? 'per-segment' : 'verifiable-segment-info',
            'stream_id' => sanitize_text_field((string) $request->get_param('stream_id')),
            'paths' => $request->get_param('paths'),
        ];

        $response = 'text' === $family
            ? $this->forward_text($api_url, $api_key, $files, $payload)
            : $this->forward_multipart($api_url, $api_key, $files, $manifest, $payload);
        if (is_wp_error($response)) {
            return $response;
        }

        $status = (int) wp_remote_retrieve_response_code($response);
        $raw_body = (string) wp_remote_retrieve_body($response);
        $decoded = json_decode($raw_body, true);
        if (! is_array($decoded)) {
            $decoded = ['success' => false, 'message' => $raw_body ?: __('Encypher API returned an empty response.', 'encypher-provenance')];
        }
        return new \WP_REST_Response($decoded, $status > 0 ? $status : 502);
    }

    private function resolve_source_files(\WP_REST_Request $request)
    {
        $attachment_ids = $request->get_param('attachment_ids');
        $uploads = method_exists($request, 'get_file_params') ? $request->get_file_params() : [];
        $uploaded = $this->normalize_upload_field($uploads['files'] ?? $uploads['file'] ?? []);
        $has_attachments = is_array($attachment_ids) && [] !== $attachment_ids;

        if ($has_attachments && [] !== $uploaded) {
            return new \WP_Error('ambiguous_files', __('Choose attachment IDs or direct uploads, not both.', 'encypher-provenance'), ['status' => 422]);
        }

        if ($has_attachments) {
            if (count($attachment_ids) > self::MAX_FILES) {
                return new \WP_Error('invalid_attachment_count', __('Select between 1 and 513 attachments.', 'encypher-provenance'), ['status' => 422]);
            }
            $files = [];
            foreach ($attachment_ids as $attachment_id) {
                $resolved = $this->resolve_attachment((int) $attachment_id);
                if (is_wp_error($resolved)) {
                    return $resolved;
                }
                $files[] = $resolved;
            }
            return $files;
        }

        if ([] === $uploaded || count($uploaded) > self::MAX_FILES) {
            return new \WP_Error('invalid_file_count', __('Upload between 1 and 513 source files.', 'encypher-provenance'), ['status' => 422]);
        }
        $files = [];
        foreach ($uploaded as $upload) {
            $resolved = $this->resolve_upload($upload);
            if (is_wp_error($resolved)) {
                return $resolved;
            }
            $files[] = $resolved;
        }
        return $files;
    }

    private function resolve_manifest(\WP_REST_Request $request)
    {
        $manifest_id = (int) $request->get_param('manifest_attachment_id');
        $uploads = method_exists($request, 'get_file_params') ? $request->get_file_params() : [];
        $uploaded = $this->normalize_upload_field($uploads['manifest'] ?? []);
        if ($manifest_id > 0 && [] !== $uploaded) {
            return new \WP_Error('ambiguous_manifest', __('Choose a manifest attachment or a direct manifest upload, not both.', 'encypher-provenance'), ['status' => 422]);
        }
        if ($manifest_id > 0) {
            return $this->resolve_attachment($manifest_id);
        }
        if (1 !== count($uploaded)) {
            return new \WP_Error('missing_manifest', __('Model verification requires one detached manifest.', 'encypher-provenance'), ['status' => 422]);
        }
        return $this->resolve_upload($uploaded[0]);
    }

    private function normalize_upload_field($field): array
    {
        if (! is_array($field) || [] === $field) {
            return [];
        }
        if (isset($field[0]) && is_array($field[0])) {
            return array_values($field);
        }
        if (isset($field['tmp_name']) && is_array($field['tmp_name'])) {
            $uploads = [];
            foreach (array_keys($field['tmp_name']) as $index) {
                $uploads[] = [
                    'name' => $field['name'][$index] ?? '',
                    'type' => $field['type'][$index] ?? '',
                    'tmp_name' => $field['tmp_name'][$index] ?? '',
                    'error' => $field['error'][$index] ?? UPLOAD_ERR_NO_FILE,
                ];
            }
            return $uploads;
        }
        return isset($field['tmp_name']) ? [$field] : [];
    }

    private function resolve_upload(array $upload)
    {
        $path = $upload['tmp_name'] ?? '';
        if (
            UPLOAD_ERR_OK !== (int) ($upload['error'] ?? UPLOAD_ERR_NO_FILE)
            || ! is_string($path)
            || '' === $path
            || ! is_uploaded_file($path)
            || ! is_readable($path)
        ) {
            return new \WP_Error('invalid_upload', __('A direct upload is missing or invalid.', 'encypher-provenance'), ['status' => 422]);
        }
        $size = filesize($path);
        if (false === $size) {
            return new \WP_Error('upload_file_unreadable', __('An uploaded file size could not be read.', 'encypher-provenance'), ['status' => 500]);
        }
        $name = sanitize_file_name((string) ($upload['name'] ?? 'asset.bin'));
        return [
            'path' => $path,
            'name' => '' !== $name ? $name : 'asset.bin',
            'mime' => sanitize_mime_type((string) ($upload['type'] ?? 'application/octet-stream')),
            'size' => (int) $size,
        ];
    }

    private function resolve_attachment(int $attachment_id)
    {
        if ($attachment_id <= 0 || 'attachment' !== get_post_type($attachment_id) || ! current_user_can('edit_post', $attachment_id)) {
            return new \WP_Error('invalid_attachment', __('An attachment is missing or not accessible.', 'encypher-provenance'), ['status' => 404]);
        }
        $path = get_attached_file($attachment_id);
        if (! is_string($path) || '' === $path || ! is_file($path) || ! is_readable($path)) {
            return new \WP_Error('attachment_file_missing', __('An attachment file is unavailable.', 'encypher-provenance'), ['status' => 404]);
        }
        $size = filesize($path);
        if (false === $size) {
            return new \WP_Error('attachment_file_unreadable', __('An attachment size could not be read.', 'encypher-provenance'), ['status' => 500]);
        }
        return [
            'path' => $path,
            'name' => basename($path),
            'mime' => (string) (get_post_mime_type($attachment_id) ?: 'application/octet-stream'),
            'size' => (int) $size,
        ];
    }

    private function api_url(string $base, string $route)
    {
        $base = rtrim(trim($base), '/');
        if ('' === $base) {
            $base = 'https://api.encypher.com/api/v1';
        }
        if (! preg_match('#/api/v1$#', $base)) {
            $base .= '/api/v1';
        }
        $url = $base . '/' . ltrim($route, '/');
        if (! ApiUrlGuard::is_safe($url)) {
            return new \WP_Error('unsafe_api_base_url', __('The configured Encypher API URL is not allowed.', 'encypher-provenance'), ['status' => 400]);
        }
        return $url;
    }

    private function forward_text(string $url, string $api_key, array $files, array $payload)
    {
        if (1 !== count($files)) {
            return new \WP_Error('invalid_text_file_count', __('Text forwarding accepts exactly one attachment.', 'encypher-provenance'), ['status' => 422]);
        }
        $text = file_get_contents($files[0]['path']);
        if (false === $text || '' === $text) {
            return new \WP_Error('empty_text_attachment', __('The selected text attachment is empty or unreadable.', 'encypher-provenance'), ['status' => 422]);
        }
        $body = 'verify' === $payload['operation']
            ? ['text' => $text]
            : [
                'text' => $text,
                'document_title' => $files[0]['name'],
                'options' => [
                    'action' => 'c2pa.created',
                    'content_mime_type' => $payload['format_token'] ?: $files[0]['mime'],
                ],
            ];
        return ApiUrlGuard::guarded_remote_post($url, [
            'headers' => [
                'Accept' => 'application/json',
                'Authorization' => 'Bearer ' . sanitize_text_field($api_key),
                'Content-Type' => 'application/json',
                'Idempotency-Key' => wp_generate_uuid4(),
            ],
            'body' => wp_json_encode($body),
            'timeout' => 120, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout -- authenticated admin-initiated all-format request; large model and media uploads need the API's bounded 120-second ceiling.
        ]);
    }

    private function forward_multipart(string $url, string $api_key, array $files, ?array $manifest, array $payload)
    {
        $parts = [];
        $family = $payload['family'];
        $operation = $payload['operation'];

        if (in_array($family, ['media', 'declared-media', 'model'], true) && 1 !== count($files)) {
            return new \WP_Error('invalid_file_count', __('This format family accepts exactly one source attachment.', 'encypher-provenance'), ['status' => 422]);
        }
        if (null !== $manifest) {
            $parts[] = ['file_field' => 'manifest', 'file' => $manifest];
        }

        if ('stream' === $family) {
            $parts[] = ['file_field' => 'init_segment', 'file' => $files[0]];
            foreach (array_slice($files, 1) as $file) {
                $parts[] = ['file_field' => 'segments', 'file' => $file];
            }
        } elseif ('model-collection' === $family) {
            $paths = is_array($payload['paths']) ? array_values($payload['paths']) : [];
            if ([] === $paths) {
                $paths = array_column($files, 'name');
            }
            if (count($paths) !== count($files)) {
                return new \WP_Error('invalid_model_paths', __('Model component paths must match the selected attachments.', 'encypher-provenance'), ['status' => 422]);
            }
            foreach ($files as $index => $file) {
                $parts[] = ['file_field' => 'files', 'file' => $file];
                $parts[] = ['field' => 'paths', 'value' => (string) $paths[$index]];
            }
        } else {
            $parts[] = ['file_field' => 'file', 'file' => $files[0]];
        }

        if ('media' === $family && 'sign' === $operation) {
            $parts[] = ['field' => 'title', 'value' => $files[0]['name']];
            $parts[] = ['field' => 'action', 'value' => 'c2pa.created'];
        } elseif ('declared-media' === $family) {
            if ('' === $payload['format_token']) {
                return new \WP_Error('missing_format_token', __('A generic container media type is required.', 'encypher-provenance'), ['status' => 422]);
            }
            $parts[] = ['field' => 'declared_media_type', 'value' => $payload['format_token']];
            $parts[] = ['field' => 'spec', 'value' => $payload['spec']];
            if ('sign' === $operation) {
                $parts[] = ['field' => 'compress_manifest', 'value' => $payload['compress_manifest'] ? 'true' : 'false'];
            }
        } elseif (in_array($family, ['model', 'model-collection'], true)) {
            if ('' === $payload['format_token']) {
                return new \WP_Error('missing_format_token', __('An ML model format is required.', 'encypher-provenance'), ['status' => 422]);
            }
            $parts[] = ['field' => 'model_format', 'value' => $payload['format_token']];
            $parts[] = ['field' => 'spec', 'value' => $payload['spec']];
            if ('sign' === $operation) {
                $parts[] = ['field' => 'compress_manifest', 'value' => $payload['compress_manifest'] ? 'true' : 'false'];
            }
        } elseif ('stream' === $family) {
            $parts[] = ['field' => 'encapsulation', 'value' => $payload['encapsulation']];
            $parts[] = ['field' => 'method', 'value' => $payload['method']];
            $parts[] = ['field' => 'mime', 'value' => $payload['format_token'] ?: 'video/mp4'];
            $parts[] = ['field' => 'spec', 'value' => $payload['spec']];
            if ('' !== $payload['stream_id']) {
                $parts[] = ['field' => 'stream_id', 'value' => $payload['stream_id']];
            }
        }

        $boundary = 'encypher-' . str_replace('-', '', wp_generate_uuid4());
        $body = $this->encode_multipart($boundary, $parts);
        if (is_wp_error($body)) {
            return $body;
        }
        return ApiUrlGuard::guarded_remote_post($url, [
            'headers' => [
                'Accept' => 'application/json',
                'Authorization' => 'Bearer ' . sanitize_text_field($api_key),
                'Content-Type' => 'multipart/form-data; boundary=' . $boundary,
                'Idempotency-Key' => wp_generate_uuid4(),
            ],
            'body' => $body,
            'timeout' => 120, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout -- authenticated admin-initiated all-format request; large model and media uploads need the API's bounded 120-second ceiling.
        ]);
    }

    private function encode_multipart(string $boundary, array $parts)
    {
        $body = '';
        foreach ($parts as $part) {
            $body .= '--' . $boundary . "\r\n";
            if (isset($part['file_field'])) {
                $file = $part['file'];
                $contents = file_get_contents($file['path']);
                if (false === $contents) {
                    return new \WP_Error('attachment_file_unreadable', __('An attachment could not be read.', 'encypher-provenance'), ['status' => 500]);
                }
                $name = str_replace(["\r", "\n", '"'], ['', '', '_'], (string) $file['name']);
                $field = str_replace(["\r", "\n", '"'], '', (string) $part['file_field']);
                $body .= 'Content-Disposition: form-data; name="' . $field . '"; filename="' . $name . "\"\r\n";
                $body .= 'Content-Type: ' . sanitize_mime_type((string) $file['mime']) . "\r\n\r\n";
                $body .= $contents . "\r\n";
            } else {
                $field = str_replace(["\r", "\n", '"'], '', (string) $part['field']);
                $body .= 'Content-Disposition: form-data; name="' . $field . "\"\r\n\r\n";
                $body .= (string) $part['value'] . "\r\n";
            }
        }
        return $body . '--' . $boundary . "--\r\n";
    }
}
