<?php
/**
 * Shared block-locator + path-mutator helpers for the surgical block-edit
 * tools (swap_image_in_block, update_block_attrs, delete_block,
 * insert_block).
 *
 * Conventions:
 *   - A "path" is an integer array, e.g. [0] or [2, 1, 0], that walks the
 *     blocks tree top-down via innerBlocks.
 *   - A "locator" is a shape the cloud sends:
 *       { type: 'image_src',    value: 'https://…/pic61.png', ordinal?: 1 }
 *       { type: 'text',         value: 'For a Fire Within',   ordinal?: 1 }
 *       { type: 'button_label', value: 'Start Here',          ordinal?: 1 }
 *       { type: 'block_name',   value: 'core/image',          ordinal?: 1 }
 *       { type: 'block_path',   value: [2, 1, 0] }   // exact path
 *     `value` is matched on the relevant block field (image src
 *     contains-match, text contains-match, button label contains-match).
 *     `ordinal` is 1-based; defaults to 1 (first match). Pass higher
 *     ordinals to disambiguate when there are multiple matches.
 */
if (!defined('ABSPATH')) exit;

/**
 * Find a block in the tree matching $locator. Returns null on no match.
 * Otherwise: ['path' => [int,...], 'block' => array].
 *
 * Walking is depth-first / pre-order which matches reading order.
 */
function swcp_find_block_by_locator($blocks, $locator) {
    if (!is_array($locator) || empty($locator['type'])) return null;
    $type    = (string) $locator['type'];
    $value   = $locator['value'] ?? null;
    $ordinal = isset($locator['ordinal']) ? max(1, (int) $locator['ordinal']) : 1;

    if ($type === 'block_path') {
        if (!is_array($value)) return null;
        $found = swcp_get_block_at_path($blocks, $value);
        return $found ? ['path' => $value, 'block' => $found] : null;
    }

    if ($value === null || $value === '') return null;

    $matches = [];
    swcp_locator_walk($blocks, [], function ($block, $path) use (&$matches, $type, $value) {
        if (swcp_locator_matches($block, $type, $value)) {
            $matches[] = ['path' => $path, 'block' => $block];
        }
    });

    if (count($matches) < $ordinal) return null;
    return $matches[$ordinal - 1];
}

/**
 * List candidates without committing — used by tool error paths so the
 * Co-pilot can surface a disambiguation prompt instead of guessing.
 *
 * Returns up to $limit entries, each ['path', 'block_name', 'preview'].
 */
function swcp_list_block_candidates($blocks, $locator, $limit = 8) {
    if (!is_array($locator) || empty($locator['type'])) return [];
    $type  = (string) $locator['type'];
    $value = $locator['value'] ?? null;
    if ($type === 'block_path' || $value === null || $value === '') return [];

    $out = [];
    swcp_locator_walk($blocks, [], function ($block, $path) use (&$out, $type, $value, $limit) {
        if (count($out) >= $limit) return;
        if (swcp_locator_matches($block, $type, $value)) {
            $name = isset($block['blockName']) ? (string) $block['blockName'] : '';
            $preview = swcp_locator_preview_for($block);
            $out[] = ['path' => $path, 'block_name' => $name, 'preview' => $preview];
        }
    });
    return $out;
}

function swcp_locator_walk($blocks, $path_prefix, $cb) {
    foreach ($blocks as $idx => $block) {
        $path = array_merge($path_prefix, [$idx]);
        $cb($block, $path);
        if (!empty($block['innerBlocks']) && is_array($block['innerBlocks'])) {
            swcp_locator_walk($block['innerBlocks'], $path, $cb);
        }
    }
}

function swcp_locator_matches($block, $type, $value) {
    $name = isset($block['blockName']) ? (string) $block['blockName'] : '';

    if ($type === 'block_name') {
        if ($value === '') return false;
        // Allow wildcard: "core/*" or "uagb/*"
        if (substr($value, -2) === '/*') {
            $prefix = substr($value, 0, -1); // keep trailing slash
            return strpos($name, $prefix) === 0;
        }
        return strcasecmp($name, $value) === 0;
    }

    if ($type === 'image_src') {
        $src = swcp_locator_image_src($block);
        if (!$src) return false;
        return stripos($src, (string) $value) !== false;
    }

    if ($type === 'text') {
        $haystack = swcp_locator_block_text($block);
        if ($haystack === '') return false;
        return stripos($haystack, (string) $value) !== false;
    }

    if ($type === 'button_label') {
        $is_button = ($name === 'core/button' || $name === 'uagb/buttons-child' || $name === 'core/buttons');
        if (!$is_button) return false;
        $label = swcp_locator_block_text($block);
        return $label !== '' && stripos($label, (string) $value) !== false;
    }

    return false;
}

/**
 * Pull a representative image src out of a block. Handles core/image,
 * core/cover (background), and a few common third-party blocks
 * (uagb/image, generateblocks/image).
 */
function swcp_locator_image_src($block) {
    $name  = isset($block['blockName']) ? (string) $block['blockName'] : '';
    $attrs = isset($block['attrs']) && is_array($block['attrs']) ? $block['attrs'] : [];

    if ($name === 'core/image') {
        if (!empty($attrs['url'])) return (string) $attrs['url'];
    }
    if ($name === 'core/cover') {
        if (!empty($attrs['url'])) return (string) $attrs['url'];
    }
    if (in_array($name, ['uagb/image', 'generateblocks/image'], true)) {
        if (!empty($attrs['url']))   return (string) $attrs['url'];
        if (!empty($attrs['src']))   return (string) $attrs['src'];
    }
    // Fallback: scrape <img src="..."> from innerHTML for blocks that wrap
    // an inline image without exposing it in attrs.
    if (!empty($block['innerHTML']) && is_string($block['innerHTML'])) {
        if (preg_match('/<img\b[^>]*\bsrc="([^"]+)"/i', $block['innerHTML'], $m)) {
            return $m[1];
        }
    }
    return '';
}

/**
 * Stripped + whitespace-collapsed text content of a block. Mirrors the
 * normalization update-page-text uses for matching, so locator hits feel
 * consistent across tools.
 */
function swcp_locator_block_text($block) {
    $html = isset($block['innerHTML']) ? (string) $block['innerHTML'] : '';
    if ($html === '') return '';
    $no_tags = preg_replace('/<[^>]+>/', ' ', $html);
    return trim(preg_replace('/\s+/', ' ', $no_tags));
}

function swcp_locator_preview_for($block) {
    $text = swcp_locator_block_text($block);
    if ($text !== '') return mb_strimwidth($text, 0, 60, '…');
    $src = swcp_locator_image_src($block);
    if ($src !== '') {
        // Tail of the URL is more recognisable than the whole thing.
        $tail = preg_replace('#^.*/([^/]+)$#', '$1', $src);
        return '<image: ' . $tail . '>';
    }
    $name = isset($block['blockName']) ? (string) $block['blockName'] : '';
    return $name ? '<' . $name . '>' : '<unknown block>';
}

/**
 * Resolve a path to a block reference. Returns null if any step misses.
 * Use the &$ref variant when you want to mutate.
 */
function swcp_get_block_at_path($blocks, $path) {
    $cur = null;
    $level = $blocks;
    foreach ($path as $i) {
        if (!isset($level[$i])) return null;
        $cur = $level[$i];
        $level = isset($cur['innerBlocks']) && is_array($cur['innerBlocks']) ? $cur['innerBlocks'] : [];
    }
    return $cur;
}

/**
 * Apply a callback to the block at $path within $blocks (by reference).
 * The callback receives the block by reference and may mutate it; if it
 * returns the literal string '__delete__', the block is removed; if it
 * returns an array, that array replaces the block. Returns true on
 * success, false if the path was invalid.
 */
function swcp_mutate_at_path(array &$blocks, array $path, callable $fn) {
    if (empty($path)) return false;
    return swcp_mutate_at_path_recurse($blocks, $path, 0, $fn);
}

function swcp_mutate_at_path_recurse(array &$level, array $path, $depth, callable $fn) {
    $idx = $path[$depth] ?? null;
    if ($idx === null || !isset($level[$idx])) return false;

    if ($depth === count($path) - 1) {
        $block = $level[$idx];
        $result = $fn($block);
        if ($result === '__delete__') {
            array_splice($level, $idx, 1);
            return true;
        }
        if (is_array($result)) {
            $level[$idx] = $result;
            return true;
        }
        // Callback mutated by reference (PHP arrays are values here, but
        // we wrote back through $block — meaning the cb may have done
        // nothing; play safe and re-assign).
        $level[$idx] = $block;
        return true;
    }

    if (!isset($level[$idx]['innerBlocks']) || !is_array($level[$idx]['innerBlocks'])) {
        return false;
    }
    return swcp_mutate_at_path_recurse($level[$idx]['innerBlocks'], $path, $depth + 1, $fn);
}

/**
 * Insert a new block before/after/inside the block at $anchor_path.
 * `position`: 'before' | 'after' | 'inside_start' | 'inside_end'.
 * Returns true on success.
 */
function swcp_insert_block_at(array &$blocks, array $anchor_path, $position, array $new_block) {
    if (empty($anchor_path)) return false;
    $position = (string) $position;

    if ($position === 'inside_start' || $position === 'inside_end') {
        // The anchor itself becomes the parent. Walk to it, then push to
        // its innerBlocks at the given end.
        return swcp_mutate_at_path($blocks, $anchor_path, function (&$block) use ($position, $new_block) {
            if (!isset($block['innerBlocks']) || !is_array($block['innerBlocks'])) {
                $block['innerBlocks'] = [];
            }
            if ($position === 'inside_start') {
                array_unshift($block['innerBlocks'], $new_block);
            } else {
                $block['innerBlocks'][] = $new_block;
            }
            return $block;
        });
    }

    // before/after: insert into the parent level next to the anchor.
    $parent_path  = $anchor_path;
    $anchor_index = (int) array_pop($parent_path);

    return swcp_walk_to_level($blocks, $parent_path, function (&$level) use ($anchor_index, $position, $new_block) {
        if (!is_array($level) || $anchor_index < 0 || $anchor_index >= count($level)) return false;
        $insert_at = $position === 'before' ? $anchor_index : ($anchor_index + 1);
        array_splice($level, $insert_at, 0, [$new_block]);
        return true;
    });
}

/**
 * Walk to the level (innerBlocks of the block at $path) and run $fn on
 * the level array by reference. With an empty $path, the level is the
 * top-level $blocks itself.
 */
function swcp_walk_to_level(array &$blocks, array $path, callable $fn) {
    if (empty($path)) {
        return $fn($blocks);
    }
    return swcp_walk_to_level_recurse($blocks, $path, 0, $fn);
}

function swcp_walk_to_level_recurse(array &$level, array $path, $depth, callable $fn) {
    $idx = $path[$depth] ?? null;
    if ($idx === null || !isset($level[$idx])) return false;

    if ($depth === count($path) - 1) {
        if (!isset($level[$idx]['innerBlocks']) || !is_array($level[$idx]['innerBlocks'])) {
            $level[$idx]['innerBlocks'] = [];
        }
        return $fn($level[$idx]['innerBlocks']);
    }

    if (!isset($level[$idx]['innerBlocks']) || !is_array($level[$idx]['innerBlocks'])) {
        return false;
    }
    return swcp_walk_to_level_recurse($level[$idx]['innerBlocks'], $path, $depth + 1, $fn);
}

/**
 * Wrap the candidate-list emit so the four tools share the same error
 * shape when a locator hits 0 or many blocks.
 */
function swcp_locator_ambiguous_or_missing($blocks, $locator) {
    $candidates = swcp_list_block_candidates($blocks, $locator, 6);
    if (empty($candidates)) {
        return [
            'ok'      => false,
            'message' => 'I could not find a block matching that. Try a more specific phrase, or click the block in the page (the rail picker) and try again.',
        ];
    }
    $previews = array_map(function ($c) { return $c['preview']; }, $candidates);
    return [
        'ok'         => false,
        'ambiguous'  => true,
        'message'    => 'I see a few blocks that could match: "' . implode('", "', $previews) . '". Which one did you mean? Tell me a phrase that\'s only in that one, or click it in the page.',
        'candidates' => $candidates,
    ];
}
