<?php
/**
 * Tool: update_block_attrs
 *
 * Patch the attrs of a single Gutenberg block via locator. Common uses:
 *   - Button: change `text`, `url`, `linkTarget`, `backgroundColor`,
 *     `textColor`, `align`.
 *   - Heading: `level`, `textColor`, `textAlign`.
 *   - Image: `align`, `width`, `height`, `caption`.
 *   - Cover / group: `backgroundColor`, `gradient`, `dimRatio`.
 *
 * The tool deep-merges the supplied `attrs` into the existing block.attrs
 * (passed values overwrite, missing values left alone). For text-bearing
 * blocks (button label, heading text, paragraph), the optional
 * `inner_text` field rewrites the visible text inside the block's
 * innerHTML — this is the predictable way to change a button's label
 * because Gutenberg keeps the label in the rendered HTML, not in attrs.
 */
if (!defined('ABSPATH')) exit;

/**
 * Translate canonical attribute names (url, text, color, …) into the
 * names the given block type actually expects. Lets the cloud always
 * pass the canonical names and not have to know every plugin's quirks.
 *
 * Returns a NEW attrs array — original keys are preserved when no
 * translation is needed.
 */
function swcp_translate_attrs_for_block($block_name, $attrs) {
    if (!is_array($attrs) || empty($attrs)) return $attrs;
    $name = (string) $block_name;
    $out  = [];

    foreach ($attrs as $k => $v) {
        // Spectra (uagb/buttons-child): canonical url → link, target → linkTarget
        if ($name === 'uagb/buttons-child') {
            if ($k === 'url')         { $out['link']       = $v; continue; }
            if ($k === 'linkTarget')  { $out['target']     = $v; continue; }
            if ($k === 'backgroundColor') { $out['background'] = $v; continue; }
        }
        // GenerateBlocks button: keeps "url" but uses "buttonType" / "linkTo"
        // — leave canonical for now; canonical is already understood by GB.
        $out[$k] = $v;
    }
    return $out;
}

function swcp_tool_update_block_attrs($input, $ctx) {
    $page_arg   = isset($input['page']) ? $input['page'] : 'current';
    $locator    = isset($input['locator']) && is_array($input['locator']) ? $input['locator'] : null;
    $attrs      = isset($input['attrs']) && is_array($input['attrs']) ? $input['attrs'] : [];
    $inner_text = array_key_exists('inner_text', $input) ? (string) $input['inner_text'] : null;

    if (!$locator) {
        return ['ok' => false, 'message' => 'I need to know which block to change. Click the block in the page or describe it.'];
    }
    if (empty($attrs) && $inner_text === null) {
        return ['ok' => false, 'message' => 'Tell me what to change about this block (color, link, alignment, label, etc.).'];
    }

    $resolved = swcp_resolve_or_error($page_arg, $ctx['post_id'] ?? 0);
    if (is_array($resolved) && isset($resolved['ok']) && !$resolved['ok']) return $resolved;
    $post = $resolved;

    $before_html = (string) $post->post_content;
    $blocks      = swcp_get_blocks($post);

    $found = swcp_find_block_by_locator($blocks, $locator);
    if (!$found) {
        return swcp_locator_ambiguous_or_missing($blocks, $locator) + ['page_id' => (int) $post->ID];
    }

    $old_attrs = isset($found['block']['attrs']) ? (array) $found['block']['attrs'] : [];
    $old_text  = swcp_locator_block_text($found['block']);

    $ok = swcp_mutate_at_path($blocks, $found['path'], function ($block) use ($attrs, $inner_text) {
        $name = isset($block['blockName']) ? (string) $block['blockName'] : '';
        if (!isset($block['attrs']) || !is_array($block['attrs'])) $block['attrs'] = [];

        // Translate canonical attr names to whatever this block-type uses.
        // We do not require the cloud to know every plugin's quirks.
        $attrs = swcp_translate_attrs_for_block($name, $attrs);

        // If the user changed a link/url, also patch href in the rendered
        // innerHTML so the saved markup matches the new attrs without
        // waiting for a re-render. We do this for the most common keys
        // we just translated above.
        $new_href = null;
        if (isset($attrs['url']))   $new_href = (string) $attrs['url'];
        if (isset($attrs['link']))  $new_href = (string) $attrs['link'];
        if (isset($attrs['linkURL'])) $new_href = (string) $attrs['linkURL'];
        if ($new_href !== null) {
            $href_patch = function ($html) use ($new_href) {
                if (!is_string($html) || $html === '') return $html;
                return preg_replace(
                    '#(<a\b[^>]*\bhref=")[^"]*(")#i',
                    '$1' . esc_url($new_href) . '$2',
                    $html,
                    1
                );
            };
            if (isset($block['innerHTML'])) {
                $block['innerHTML'] = $href_patch((string) $block['innerHTML']);
            }
            if (isset($block['innerContent']) && is_array($block['innerContent'])) {
                foreach ($block['innerContent'] as &$ic) {
                    if (is_string($ic)) $ic = $href_patch($ic);
                }
                unset($ic);
            }
        }

        // Deep-merge: nested arrays merged; scalars overwritten; null clears.
        foreach ($attrs as $k => $v) {
            if ($v === null) {
                unset($block['attrs'][$k]);
                continue;
            }
            if (is_array($v) && isset($block['attrs'][$k]) && is_array($block['attrs'][$k])) {
                $block['attrs'][$k] = array_replace_recursive($block['attrs'][$k], $v);
            } else {
                $block['attrs'][$k] = $v;
            }
        }

        // Rewrite the visible text inside innerHTML if requested.
        if ($inner_text !== null) {
            $patch = function ($html) use ($inner_text) {
                if (!is_string($html) || $html === '') return $html;
                $new_text = esc_html($inner_text);

                // Strategy 1 — leaf text. Find the *deepest* element whose
                // content is just text (no further tags) and replace that.
                // This catches Spectra/uagb buttons where the visible text
                // sits inside `<a>...<div class="uagb-button__link">Label</div>...</a>`.
                if (preg_match_all('#<([a-z][a-z0-9]*)\b[^>]*>([^<]+)</\1>#is', $html, $m, PREG_OFFSET_CAPTURE)) {
                    // Pick the LAST leaf (likely the deepest occurrence).
                    $last = end($m[0]);
                    $tag  = end($m[1]);
                    $inner = end($m[2]);
                    if (trim($inner[0]) !== '') {
                        $offset = $last[1];
                        $length = strlen($last[0]);
                        $opening_close = strpos($last[0], '>') + 1;
                        $closing_start = strrpos($last[0], '</');
                        $head = substr($last[0], 0, $opening_close);
                        $tail = substr($last[0], $closing_start);
                        return substr($html, 0, $offset) . $head . $new_text . $tail . substr($html, $offset + $length);
                    }
                }

                // Strategy 2 — common containers with simple text content.
                $patterns = [
                    '#(<a\b[^>]*>)([^<]*)(</a>)#is',
                    '#(<button\b[^>]*>)([^<]*)(</button>)#is',
                    '#(<h[1-6]\b[^>]*>)([^<]*)(</h[1-6]>)#is',
                    '#(<p\b[^>]*>)([^<]*)(</p>)#is',
                    '#(<span\b[^>]*class="[^"]*"[^>]*>)([^<]*)(</span>)#is',
                ];
                foreach ($patterns as $rx) {
                    if (preg_match($rx, $html)) {
                        return preg_replace($rx, '$1' . $new_text . '$3', $html, 1);
                    }
                }
                return $html;
            };
            if (isset($block['innerHTML'])) {
                $block['innerHTML'] = $patch((string) $block['innerHTML']);
            }
            if (isset($block['innerContent']) && is_array($block['innerContent'])) {
                foreach ($block['innerContent'] as &$ic) {
                    if (is_string($ic)) $ic = $patch($ic);
                }
                unset($ic);
            }
            // Some blocks store their visible text in attrs and re-render
            // from attrs (Spectra, GenerateBlocks). Mirror inner_text into
            // the right attr so the next render uses our value.
            $name = isset($block['blockName']) ? (string) $block['blockName'] : '';
            if ($name === 'core/button')        $block['attrs']['text']  = $inner_text;
            if ($name === 'uagb/buttons-child') $block['attrs']['label'] = $inner_text;
            if ($name === 'generateblocks/button') $block['attrs']['text'] = $inner_text;
        }

        return $block;
    });

    if (!$ok) {
        return ['ok' => false, 'message' => 'Found the block but failed to apply the change.'];
    }

    $saved = swcp_save_blocks($post->ID, $blocks);
    if (!$saved) {
        return ['ok' => false, 'message' => 'Found the block but failed to save the page.'];
    }

    $name = isset($found['block']['blockName']) ? (string) $found['block']['blockName'] : 'block';
    $what = [];
    foreach ($attrs as $k => $v) $what[] = $k;
    if ($inner_text !== null) $what[] = 'text';
    $what_str = $what ? implode(', ', $what) : 'attrs';

    return [
        'ok'               => true,
        'page_id'          => (int) $post->ID,
        'message'          => 'Updated ' . $what_str . ' on the ' . $name . ' block on "' . esc_html(get_the_title($post)) . '".',
        'view_url'         => swcp_view_url_for($post->ID),
        'edit_url'         => swcp_edit_url_for($post->ID),
        'block_name'       => $name,
        'changed_keys'     => $what,
        'requires_refresh' => true,
        'undo' => [
            'tool'  => 'restore_page_version',
            'input' => ['post_id' => (int) $post->ID, 'snapshot_html' => $before_html],
            'label' => 'Undo',
        ],
    ];
}
