<?php

namespace Extendify\QuickEdit\Schemas;

defined('ABSPATH') || die('No direct access.');

// Mirrors the media-* attrs and the inner <figure>/<img> markup so the
// parsed-block tree stays internally consistent across re-renders. The
// editable target is the image on the media side; the synthetic client
// blockType `core/media-text:image` resolves to the real `core/media-text`
// before it reaches SaveController, which is why this is keyed on the real
// block name in the Registry.
class MediaText implements Schema
{
    public function fields(): array
    {
        return [
            [
                'key'     => 'media',
                'control' => 'image',
                'label'   => __('Image', 'extendify-local'),
            ],
        ];
    }

    public function apply(array $block, string $fieldKey, $value): array
    {
        if ($fieldKey !== 'media' || !is_array($value)) {
            return $block;
        }

        $id  = isset($value['id']) ? (int) $value['id'] : null;
        $alt = isset($value['alt']) ? wp_strip_all_tags((string) $value['alt']) : null;

        // A positive id must resolve to a real image attachment; trust the
        // server's URL when it does. The raw client URL is only honored for
        // the id-less external-image case (mirror of Image::apply).
        if ($id !== null && $id > 0) {
            if (!wp_attachment_is_image($id)) {
                return $block;
            }
            $url = wp_get_attachment_image_url($id, 'full') ?: null;
        } else {
            $url = isset($value['url']) ? esc_url_raw((string) $value['url']) : null;
        }

        if (!$url) {
            return $block;
        }

        $attrs = $block['attrs'] ?? [];
        $attrs['mediaUrl']  = $url;
        $attrs['mediaLink'] = $url;
        $attrs['mediaType'] = 'image';
        if ($id !== null && $id > 0) {
            $attrs['mediaId'] = $id;
        } else {
            unset($attrs['mediaId']);
        }
        if ($alt !== null) {
            $attrs['mediaAlt'] = $alt;
        }
        $attrs = $this->dropImportClass($attrs);
        $block['attrs'] = $attrs;

        // Static block: serialize_blocks emits the stored innerContent verbatim,
        // so the rendered <img>/background must be patched here — there's no
        // save() re-run from attrs. The media <figure> + <img> live in the
        // leading string chunk (the content side is inner blocks → nulls), so
        // find that chunk and patch in place. NOT innerHTML, whose nulls would
        // collapse and drop the content-side inner blocks (see Cover::apply).
        $innerContent = $block['innerContent'] ?? [];
        foreach ($innerContent as $i => $chunk) {
            if (!is_string($chunk) || strpos($chunk, 'wp-block-media-text__media') === false) {
                continue;
            }
            $tp = new \WP_HTML_Tag_Processor($chunk);
            $inMedia = false;
            $patched = false;
            while ($tp->next_tag()) {
                $tag = $tp->get_tag();
                if ($tag === 'FIGURE') {
                    $inMedia = $tp->has_class('wp-block-media-text__media');
                    if ($inMedia) {
                        // imageFill renders the media as the figure's CSS
                        // background; keep that url in sync with the <img>.
                        $style = $tp->get_attribute('style');
                        if (is_string($style) && stripos($style, 'background-image') !== false) {
                            $tp->set_attribute('style', preg_replace_callback(
                                '/background-image\s*:\s*url\([^)]*\)/i',
                                static fn () => 'background-image:url(' . $url . ')',
                                $style
                            ));
                            $patched = true;
                        }
                    }
                    continue;
                }
                if ($tag === 'IMG' && $inMedia) {
                    $tp->set_attribute('src', $url);
                    if ($alt !== null) {
                        $tp->set_attribute('alt', $alt);
                    }

                    // Gutenberg's media-text save() derives wp-image-{id} from
                    // attrs.mediaId; a mismatch trips the validator. Strip any
                    // pre-existing first so re-edits don't stack.
                    $cls = (string) ($tp->get_attribute('class') ?? '');
                    $cls = trim((string) preg_replace('/\bwp-image-\d+\b/', '', $cls));
                    if ($id !== null && $id > 0) {
                        $cls = trim($cls . ' wp-image-' . $id);
                    }
                    $cls = (string) preg_replace('/\s{2,}/', ' ', $cls);
                    if ($cls === '') {
                        $tp->remove_attribute('class');
                    } else {
                        $tp->set_attribute('class', $cls);
                    }

                    if ($id === null || $id <= 0) {
                        $tp->remove_attribute('srcset');
                        $tp->remove_attribute('sizes');
                    }
                    $patched = true;
                    break;
                }
            }
            if ($patched) {
                $patchedChunk = $this->stripImportMarker($tp->get_updated_html(), 'div');
                $innerContent[$i] = $patchedChunk;
                $block['innerContent'] = $innerContent;
                // Inspection-only; the serializer rebuilds innerHTML from innerContent.
                $block['innerHTML'] = implode('', array_map(
                    static fn ($c) => is_string($c) ? $c : '',
                    $innerContent
                ));
                break;
            }
        }

        return $block;
    }

    // The Launch-import marker lives in attrs.className for media-text (it
    // renders into the wrapper div); strip it so a Quick Edit replace claims
    // the image the same way the agent's change-image flow does.
    private function dropImportClass(array $attrs): array
    {
        if (!isset($attrs['className']) || !is_string($attrs['className'])) {
            return $attrs;
        }
        $classes = array_filter(
            preg_split('/\s+/', $attrs['className']) ?: [],
            static fn ($c) => $c !== '' && $c !== 'extendify-image-import'
        );
        if ($classes) {
            $attrs['className'] = implode(' ', $classes);
        } else {
            unset($attrs['className']);
        }
        return $attrs;
    }

    // Mirror of Image::stripImportMarker — drop the marker class that survives
    // in the rendered wrapper but not in attrs after the className edit above.
    private function stripImportMarker(string $html, string $tagName): string
    {
        $tp = new \WP_HTML_Tag_Processor($html);
        if (!$tp->next_tag($tagName)) {
            return $html;
        }
        if (!$tp->has_class('extendify-image-import')) {
            return $html;
        }
        $tp->remove_class('extendify-image-import');
        return $tp->get_updated_html();
    }
}
