<?php
namespace EncypherProvenance;

use WP_Error;

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

/**
 * WordPress VIP original-only delivery and edge-purge adapter.
 *
 * The canonical attachment record owns every URL. No editor or request value
 * can select a purge target. Transformed media remains outside the Verified
 * claim because only the stored original carries the C2PA hard binding.
 */
final class VipMediaDelivery
{
    private const DELIVERY_META = '_encypher_public_delivery_state';
    private const PURGE_ERROR = 'vip_cache_purge_failed';
    private const RETRY_TTL = 30;

    public const PROVENANCE_STATUSES = ['signed', 'verified', 'c2pa_protected', 'c2pa_verified'];

    public static function signed_url(int $attachment_id): string
    {
        $url = wp_get_attachment_url($attachment_id);
        if (! is_string($url) || '' === $url) {
            return '';
        }
        if (! self::carries_provenance($attachment_id) || ! self::is_vip()) {
            return $url;
        }

        $mime_type = function_exists('get_post_mime_type')
            ? (string) get_post_mime_type($attachment_id)
            : '';
        if ('' !== $mime_type && ! str_starts_with(strtolower($mime_type), 'image/')) {
            return $url;
        }

        // VIP documents ?original as the exact bypass for all image transforms,
        // including format negotiation. Canonical attachment URLs do not carry
        // transformation arguments; strip any stale query before adding it.
        $cut = strcspn($url, '?#');
        return substr($url, 0, $cut) . '?original';
    }

    /**
     * Match a stored media URL to its canonical signed delivery URL.
     *
     * WordPress image blocks often store generated names such as
     * photo-1024x768.jpg. The signed-media render filter replaces those with
     * canonical photo.jpg, so that exact core suffix is the only non-canonical
     * form accepted here.
     */
    public static function matches_rendered_url(string $rendered_url, string $expected_url): bool
    {
        $rendered = self::url_identity($rendered_url);
        $expected = self::url_identity($expected_url);
        if ('' === $rendered || '' === $expected) {
            return false;
        }
        if ($rendered === $expected) {
            return true;
        }
        if (dirname($rendered) !== dirname($expected)) {
            return false;
        }

        $rendered_name = basename($rendered);
        $expected_name = basename($expected);
        $extension = pathinfo($expected_name, PATHINFO_EXTENSION);
        $stem = pathinfo($expected_name, PATHINFO_FILENAME);
        if ('' === $extension || '' === $stem) {
            return false;
        }

        return 1 === preg_match(
            '/\A' . preg_quote($stem, '/') . '-[1-9][0-9]*x[1-9][0-9]*\.' . preg_quote($extension, '/') . '\z/i',
            $rendered_name
        );
    }

    private static function url_identity(string $url): string
    {
        $url = html_entity_decode(trim($url), ENT_QUOTES, 'UTF-8');
        $host = wp_parse_url($url, PHP_URL_HOST);
        $path = wp_parse_url($url, PHP_URL_PATH);
        $port = wp_parse_url($url, PHP_URL_PORT);
        if (! is_string($host) || '' === $host || ! is_string($path) || '' === $path) {
            return '';
        }

        return strtolower($host) . (is_int($port) ? ':' . $port : '') . $path;
    }

    /**
     * Preserve query semantics when binding non-image rendered media bytes.
     */
    private static function exact_url_identity(string $url): string
    {
        $identity = self::url_identity($url);
        if ('' === $identity) {
            return '';
        }
        $query = wp_parse_url(html_entity_decode(trim($url), ENT_QUOTES, 'UTF-8'), PHP_URL_QUERY);
        return $identity . (is_string($query) && '' !== $query ? '?' . $query : '');
    }
    public static function carries_provenance(int $attachment_id): bool
    {
        $delivery_state = (string) get_post_meta($attachment_id, self::DELIVERY_META, true);
        if (in_array($delivery_state, ['signed_local_pending_purge', 'purge_failed'], true)) {
            return false;
        }

        $signed_hash = (string) get_post_meta($attachment_id, '_encypher_signed_file_hash', true);
        $source_valid = ! empty(get_post_meta($attachment_id, '_encypher_source_c2pa_valid', true));
        $expected_hash = '' !== $signed_hash
            ? $signed_hash
            : ($source_valid ? (string) get_post_meta($attachment_id, '_encypher_source_file_hash', true) : '');
        if (1 !== preg_match('/\Asha256:[0-9a-f]{64}\z/i', $expected_hash)) {
            return false;
        }

        $file_path = function_exists('get_attached_file') ? get_attached_file($attachment_id) : '';
        if (! is_string($file_path) || '' === $file_path || ! is_file($file_path) || ! is_readable($file_path)) {
            return false;
        }
        $actual_hash = hash_file('sha256', $file_path);
        return is_string($actual_hash) && hash_equals(strtolower($expected_hash), 'sha256:' . strtolower($actual_hash));
    }

    /**
     * Resolve every provenance-bearing attachment a public post actually
     * renders. This is the single authorization boundary shared by localized
     * reader overlays and the public verify-media route.
     *
     * @return array<int,array{id:int,url:string,type:string}>
     */
    public static function public_signed_media(int $post_id): array
    {
        if (! class_exists(MediaBlockExtractor::class) || ! function_exists('get_post')) {
            return [];
        }
        $post = get_post($post_id);
        if (
            ! $post
            || ! isset($post->post_content)
            || ! empty($post->post_password)
            || (function_exists('is_post_publicly_viewable') && ! is_post_publicly_viewable($post))
        ) {
            return [];
        }

        $extractor = new MediaBlockExtractor();
        $items = $extractor->extract_media_from_content((string) $post->post_content, $post_id, true, false);
        $payload = [];
        foreach ($items as $item) {
            $attachment_id = isset($item['attachment_id']) ? (int) $item['attachment_id'] : 0;
            if ($attachment_id <= 0 || ! self::carries_provenance($attachment_id)) {
                continue;
            }
            $url = self::signed_url($attachment_id);
            if ('' === $url) {
                continue;
            }

            $source_block = (string) ($item['source_block'] ?? '');
            $rendered_url = isset($item['rendered_url']) ? (string) $item['rendered_url'] : '';
            $featured = 'featured_image' === $source_block
                && function_exists('get_post_thumbnail_id')
                && $attachment_id === (int) get_post_thumbnail_id($post_id);
            $legacy_gallery = 'wp:gallery' === $source_block;
            $mime_type = function_exists('get_post_mime_type')
                ? strtolower((string) get_post_mime_type($attachment_id))
                : '';
            $is_image = str_starts_with($mime_type, 'image/')
                || 'image' === strtolower((string) ($item['media_type'] ?? ''))
                || str_contains(strtolower($source_block), 'image');
            $url_matches = $is_image
                ? self::matches_rendered_url($rendered_url, $url)
                : self::exact_url_identity($rendered_url) === self::exact_url_identity($url);
            if (
                ! $featured
                && ! $legacy_gallery
                && ('' === $rendered_url || ! $url_matches)
            ) {
                continue;
            }

            $type = str_starts_with($mime_type, 'audio/')
                ? 'audio'
                : (str_starts_with($mime_type, 'video/') ? 'video' : 'image');
            $payload[$attachment_id] = [
                'id'   => $attachment_id,
                'url'  => $url,
                'type' => $type,
            ];
        }

        return array_values($payload);
    }

    /**
     * Confirm that every media item in a rich-signed post still matches the
     * bytes committed when the compound manifest was created.
     */
    public static function post_media_intact(int $post_id): bool
    {
        if (! class_exists(MediaBlockExtractor::class) || ! function_exists('get_post')) {
            return false;
        }
        $post = get_post($post_id);
        if (! $post || ! isset($post->post_content)) {
            return false;
        }
        $extractor = new MediaBlockExtractor();
        foreach ($extractor->extract_media_from_content((string) $post->post_content, $post_id, true, false) as $item) {
            $attachment_id = isset($item['attachment_id']) ? (int) $item['attachment_id'] : 0;
            if ($attachment_id <= 0 || ! self::carries_provenance($attachment_id)) {
                return false;
            }
        }
        return true;
    }


    /**
     * Force signed images onto the exact original and remove browser-selectable
     * derivative candidates.
     *
     * @param array<string,mixed> $attributes
     * @param object              $attachment Object exposing ID.
     * @return array<string,mixed>
     */
    public static function filter_image_attributes(array $attributes, object $attachment): array
    {
        $attachment_id = isset($attachment->ID) ? (int) $attachment->ID : 0;
        if ($attachment_id <= 0 || ! self::carries_provenance($attachment_id)) {
            return $attributes;
        }

        $url = self::signed_url($attachment_id);
        if ('' !== $url) {
            $attributes['src'] = $url;
        }
        unset($attributes['srcset'], $attributes['sizes']);
        return $attributes;
    }
    /**
     * Rewrite saved Gutenberg image markup after the signed text snapshot is
     * selected. Only URL-selection attributes change; visible signed text and
     * its provenance markers remain byte-for-byte intact.
     */
    public static function rewrite_signed_image_tags(string $html): string
    {
        if ('' === $html
            || ! preg_match_all('/\bwp-image-(\d+)\b/', $html, $matches)) {
            return $html;
        }

        $attachment_ids = array_unique(array_map('intval', $matches[1]));
        foreach ($attachment_ids as $attachment_id) {
            if ($attachment_id <= 0 || ! self::carries_provenance($attachment_id)) {
                continue;
            }
            $url = self::signed_url($attachment_id);
            if ('' === $url) {
                continue;
            }
            $escaped_url = htmlspecialchars($url, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
            $pattern = '/<img\b(?=[^>]*\bwp-image-' . $attachment_id . '\b)[^>]*>/i';
            $html = (string) preg_replace_callback(
                $pattern,
                static function (array $match) use ($escaped_url): string {
                    $tag = (string) preg_replace(
                        "/\\s+(?:srcset|sizes)\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s>]+)/i",
                        '',
                        $match[0]
                    );
                    if (preg_match('/\ssrc\s*=/i', $tag)) {
                        return (string) preg_replace_callback(
                            "/(\\ssrc\\s*=\\s*)([\"'])(.*?)\\2/is",
                            static fn (array $src): string => $src[1] . $src[2] . $escaped_url . $src[2],
                            $tag,
                            1
                        );
                    }
                    return (string) preg_replace('/\s*\/?>$/', ' src="' . $escaped_url . '">', $tag, 1);
                },
                $html
            );
        }
        return $html;
    }


    /**
     * Purge the canonical VIP attachment URL after a verified byte write.
     * Non-VIP hosts need no edge purge and retain their existing behavior.
     *
     * @return true|WP_Error
     */
    public static function purge_after_verified_write(int $attachment_id)
    {
        if (! self::is_vip()) {
            delete_post_meta($attachment_id, self::DELIVERY_META);
            return true;
        }

        $url = wp_get_attachment_url($attachment_id);
        if (! is_string($url) || '' === $url || ! function_exists('wpcom_vip_purge_edge_cache_for_url')) {
            return self::record_purge_failure($attachment_id);
        }

        update_post_meta($attachment_id, self::DELIVERY_META, 'signed_local_pending_purge');
        $result = wpcom_vip_purge_edge_cache_for_url($url);
        if (false === $result) {
            return self::record_purge_failure($attachment_id);
        }

        update_post_meta($attachment_id, self::DELIVERY_META, 'purge_accepted');
        $last_error = get_post_meta($attachment_id, '_encypher_last_sign_error', true);
        if (is_array($last_error) && self::PURGE_ERROR === ($last_error['code'] ?? '')) {
            delete_post_meta($attachment_id, '_encypher_last_sign_error');
        }
        return true;
    }

    /**
     * Retry one attachment purge. The caller owns nonce and capability checks;
     * this adapter supplies per-attachment throttling and the same target rules.
     *
     * @return true|WP_Error
     */
    public static function retry_purge(int $attachment_id)
    {
        $last_error = get_post_meta($attachment_id, '_encypher_last_sign_error', true);
        $delivery_state = (string) get_post_meta($attachment_id, self::DELIVERY_META, true);
        if ('purge_failed' !== $delivery_state || ! is_array($last_error) || self::PURGE_ERROR !== ($last_error['code'] ?? '')) {
            return new WP_Error(
                'purge_retry_not_failed',
                __('No failed media purge is pending for this attachment.', 'encypher-provenance')
            );
        }

        $transient = 'encypher_vip_purge_retry_' . $attachment_id;
        if (get_transient($transient)) {
            return new WP_Error(
                'purge_retry_limited',
                __('Public delivery was checked recently. Wait a moment, then try again.', 'encypher-provenance')
            );
        }
        set_transient($transient, 1, self::RETRY_TTL);
        return self::purge_after_verified_write($attachment_id);
    }

    private static function is_vip(): bool
    {
        $detected = defined('WPCOM_IS_VIP_ENV') || function_exists('wpcom_vip_purge_edge_cache_for_url');
        return function_exists('apply_filters')
            ? (bool) apply_filters('encypher_wordpress_vip_media', $detected)
            : $detected;
    }

    private static function record_purge_failure(int $attachment_id): WP_Error
    {
        $message = __('Signed bytes were saved, but WordPress VIP did not accept the edge-cache purge. Retry public delivery.', 'encypher-provenance');
        update_post_meta($attachment_id, self::DELIVERY_META, 'purge_failed');
        update_post_meta($attachment_id, '_encypher_last_sign_error', [
            'code' => self::PURGE_ERROR,
            'message' => $message,
            'time' => time(),
        ]);
        return new WP_Error(self::PURGE_ERROR, $message);
    }
}
