<?php
if (!defined('ABSPATH')) {
    exit;
}

/**
 * Safely read/write Gutenberg block content without changing layout.
 *
 * Uses WordPress native parse_blocks/serialize_blocks functions
 * to preserve block structure and comment boundaries.
 *
 * @since 2.1.0
 * @since 2.1.3 Refactored to use parse_blocks/serialize_blocks instead of regex
 */
class AI_Magic_Gutenberg_Processor
{

    /**
     * Block types that contain editable text content.
     *
     * @var array
     */
    private $text_blocks = array(
        'core/paragraph',
        'core/heading',
        'core/list',
        'core/list-item',
        'core/quote',
        'core/pullquote',
        'core/preformatted',
        'core/verse',
        'core/freeform',    // Classic block
        'core/button',
    );

    /**
     * Extracted texts with their block paths for reconstruction.
     *
     * @var array
     */
    private $extracted_texts = array();

    /**
     * Get raw post_content.
     *
     * @param int $post_id
     * @return string
     */
    public function get_content($post_id)
    {
        return get_post_field('post_content', $post_id);
    }

    /**
     * Save updated content back to post.
     *
     * @param int    $post_id
     * @param string $content
     * @return int|WP_Error
     */
    public function save_content($post_id, $content)
    {
        return wp_update_post(array(
            'ID' => $post_id,
            'post_content' => $content,
        ));
    }

    /**
     * Extract text content from Gutenberg blocks.
     *
     * Uses WordPress parse_blocks for proper block parsing,
     * preserving all block attributes and structure.
     *
     * @param string $content Post content with blocks
     * @return array Array of text items with path, blockName, value, original
     */
    public function extract_paragraphs($content)
    {
        $this->extracted_texts = array();

        if (!is_string($content) || $content === '') {
            return array();
        }

        // Use WordPress native block parser
        $blocks = parse_blocks($content);

        if (empty($blocks)) {
            return array();
        }

        $this->extract_from_blocks($blocks, '');

        return $this->extracted_texts;
    }

    /**
     * Recursively extract text from parsed blocks.
     *
     * @param array  $blocks Array of parsed blocks
     * @param string $path   Current path for nested blocks
     * @return void
     */
    private function extract_from_blocks(array $blocks, $path = '')
    {
        foreach ($blocks as $index => $block) {
            $block_path = $path . '/' . $index;

            // Skip empty or null block names (usually spacer content)
            if (empty($block['blockName'])) {
                continue;
            }

            // Process text-based blocks
            if (in_array($block['blockName'], $this->text_blocks, true)) {
                $html = isset($block['innerHTML']) ? $block['innerHTML'] : '';
                $plain_text = wp_strip_all_tags($html);

                // Only include blocks with meaningful content
                if (strlen(trim($plain_text)) > 5) {
                    $this->extracted_texts[] = array(
                        'path' => $block_path,
                        'blockName' => $block['blockName'],
                        'value' => $plain_text,
                        'original' => $html,
                        'type' => $this->get_block_type($block['blockName']),
                    );
                }
            }

            // Recursively process inner blocks (columns, groups, cover, etc.)
            if (!empty($block['innerBlocks']) && is_array($block['innerBlocks'])) {
                $this->extract_from_blocks($block['innerBlocks'], $block_path);
            }
        }
    }

    /**
     * Map block name to simple type.
     *
     * @param string $block_name Full block name (e.g., 'core/paragraph')
     * @return string Simple type (paragraph, heading, etc.)
     */
    private function get_block_type($block_name)
    {
        $type_map = array(
            'core/paragraph' => 'paragraph',
            'core/heading' => 'heading',
            'core/list' => 'list',
            'core/list-item' => 'list-item',
            'core/quote' => 'quote',
            'core/pullquote' => 'quote',
            'core/preformatted' => 'code',
            'core/verse' => 'verse',
            'core/freeform' => 'classic',
            'core/button' => 'button',
        );

        return isset($type_map[$block_name]) ? $type_map[$block_name] : 'text';
    }

    /**
     * Apply new texts to blocks and return updated content.
     *
     * Uses parse_blocks and serialize_blocks to safely modify content
     * while preserving block structure and comment boundaries.
     *
     * @param string $content    Original post content
     * @param array  $texts      Items from extract_paragraphs()
     * @param array  $new_values New text values in same order
     * @return string Updated post content
     */
    public function apply_new_paragraphs($content, array $texts, array $new_values)
    {
        if (empty($texts) || empty($new_values)) {
            return $content;
        }

        // Parse the blocks
        $blocks = parse_blocks($content);

        if (empty($blocks)) {
            return $content;
        }

        // Build path-to-value map
        $updates = array();
        foreach ($texts as $index => $item) {
            if (isset($new_values[$index])) {
                $updates[$item['path']] = array(
                    'new_value' => $new_values[$index],
                    'old_value' => $item['value'],
                    'original' => $item['original'],
                    'block_name' => $item['blockName'],
                );
            }
        }

        // Apply updates to blocks
        $this->apply_updates_to_blocks($blocks, $updates, '');

        // Serialize blocks back to content
        return serialize_blocks($blocks);
    }

    /**
     * Recursively apply updates to blocks.
     *
     * @param array  &$blocks Blocks array (by reference)
     * @param array  $updates Path-to-update map
     * @param string $path    Current path
     * @return void
     */
    private function apply_updates_to_blocks(array &$blocks, array $updates, $path = '')
    {
        foreach ($blocks as $index => &$block) {
            $block_path = $path . '/' . $index;

            // Check if this block has an update
            if (isset($updates[$block_path])) {
                $update = $updates[$block_path];
                $block = $this->update_block_content($block, $update);
            }

            // Recursively process inner blocks
            if (!empty($block['innerBlocks']) && is_array($block['innerBlocks'])) {
                $this->apply_updates_to_blocks($block['innerBlocks'], $updates, $block_path);
            }
        }
    }

    /**
     * Update a single block's content.
     *
     * @param array $block  The block to update
     * @param array $update Update data with new_value, old_value, original
     * @return array Updated block
     */
    private function update_block_content(array $block, array $update)
    {
        $old_text = $update['old_value'];
        $new_text = $update['new_value'];
        $original = $update['original'];

        // Update innerHTML by replacing the text content
        if (isset($block['innerHTML'])) {
            // Try to preserve HTML structure by replacing text within tags
            $new_html = $this->replace_text_in_html($original, $old_text, $new_text);
            $block['innerHTML'] = $new_html;
        }

        // Update innerContent array (maintains block structure)
        if (!empty($block['innerContent']) && is_array($block['innerContent'])) {
            foreach ($block['innerContent'] as $i => $content_piece) {
                if (is_string($content_piece) && !empty($content_piece)) {
                    $block['innerContent'][$i] = $this->replace_text_in_html(
                        $content_piece,
                        $old_text,
                        $new_text
                    );
                }
            }
        }

        return $block;
    }

    /**
     * Replace text content in HTML while preserving tags.
     *
     * @param string $html     Original HTML
     * @param string $old_text Old plain text
     * @param string $new_text New plain text
     * @return string Updated HTML
     */
    private function replace_text_in_html($html, $old_text, $new_text)
    {
        // For simple single-element HTML, do direct replacement
        // This handles <p>text</p>, <h1>text</h1>, etc.

        // Extract the tag wrapper
        if (preg_match('/^(\s*<[^>]+>)(.*?)(<\/[^>]+>\s*)$/s', $html, $matches)) {
            $open_tag = $matches[1];
            $content = $matches[2];
            $close_tag = $matches[3];

            // Replace the text content
            $stripped_content = wp_strip_all_tags($content);
            if (trim($stripped_content) === trim($old_text)) {
                // Direct match - replace entirely
                return $open_tag . esc_html($new_text) . $close_tag;
            }
        }

        // Fallback: do str_replace (may not preserve inline formatting)
        return str_replace($old_text, $new_text, $html);
    }

    /**
     * Get supported text block types.
     *
     * @return array
     */
    public function get_text_blocks()
    {
        return $this->text_blocks;
    }

    /**
     * Add custom block type(s) to process.
     *
     * @param string|array $blocks Block name(s) to add
     * @return void
     */
    public function add_text_blocks($blocks)
    {
        if (is_string($blocks)) {
            $blocks = array($blocks);
        }

        $this->text_blocks = array_unique(array_merge($this->text_blocks, $blocks));
    }

}
