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

class IDVE_Importer {

    private $blocks;

    public function __construct( $blocks = null ) {
        $this->blocks = $blocks;
    }

    public function import( $html, $old_tree = null ) {
        if ( empty( $html ) ) {
            return array( 'sections' => array() );
        }

        libxml_use_internal_errors( true );
        $doc = new DOMDocument();
        $doc->loadHTML( '<?xml encoding="utf-8"?><div id="idve-import-root">' . $html . '</div>', LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD );
        libxml_clear_errors();

        $xpath    = new DOMXPath( $doc );
        $sections = $xpath->query( "//*[contains(concat(' ',normalize-space(@class),' '),' idve-section ')]" );

        if ( $sections->length > 0 ) {
            $tree = $this->parse_idve_structure( $doc, $xpath );
        } else {
            $tree = array(
                'sections' => array( $this->wrap_in_html_block( $html ) ),
            );
        }

        // Merge settings from old tree when available.
        if ( $old_tree && isset( $old_tree['sections'] ) ) {
            $map = $this->build_settings_map( $old_tree );
            $this->apply_old_settings( $tree, $map );
        }

        return $tree;
    }

    private function parse_idve_structure( $doc, $xpath ) {
        $tree     = array( 'sections' => array() );
        $sections = $xpath->query( "//*[contains(concat(' ',normalize-space(@class),' '),' idve-section ')]" );

        foreach ( $sections as $sec_el ) {
            $section = array(
                'id'       => $this->preserve_or_generate_id( $sec_el, 'idve_sec' ),
                'settings' => $this->parse_section_settings( $sec_el, $xpath ),
                'rows'     => array(),
            );

            $rows = $xpath->query( ".//*[contains(concat(' ',normalize-space(@class),' '),' idve-row ')]", $sec_el );
            foreach ( $rows as $row_el ) {
                $row = array(
                    'id'       => $this->preserve_or_generate_id( $row_el, 'idve_row' ),
                    'settings' => $this->parse_row_settings( $row_el, $xpath ),
                    'columns'  => array(),
                );

                $cols = $xpath->query( ".//*[contains(concat(' ',normalize-space(@class),' '),' idve-col ')]", $row_el );
                foreach ( $cols as $col_el ) {
                    $width  = $this->parse_column_width( $col_el->getAttribute( 'class' ) );
                    $column = array(
                        'id'       => $this->preserve_or_generate_id( $col_el, 'idve_col' ),
                        'width'    => $width,
                        'settings' => $this->parse_column_settings( $col_el ),
                        'blocks'   => array(),
                    );

                    // Only query direct child blocks of the column, not descendants inside groups.
                    $blocks = $this->query_direct_child_blocks( $col_el, $xpath );
                    foreach ( $blocks as $blk_el ) {
                        $column['blocks'][] = $this->parse_block_from_element( $blk_el, $doc, $xpath );
                    }

                    $row['columns'][] = $column;
                }

                $section['rows'][] = $row;
            }

            $tree['sections'][] = $section;
        }

        return $tree;
    }

    /**
     * Query only direct-child block elements of a container, not nested descendants inside group blocks.
     */
    private function query_direct_child_blocks( $container, $xpath ) {
        $results = array();
        foreach ( $container->childNodes as $child ) {
            if ( $child->nodeType !== XML_ELEMENT_NODE ) {
                continue;
            }
            $cls = $child->getAttribute( 'class' );
            if ( preg_match( '/\bidve-block\b/', $cls ) ) {
                $results[] = $child;
            }
        }
        return $results;
    }

    /**
     * Parse a single block element into the IDVE block structure. Supports recursion for group blocks.
     */
    private function parse_block_from_element( $blk_el, $doc, $xpath ) {
        $type     = $this->parse_block_type( $blk_el->getAttribute( 'class' ) );
        $data     = $this->parse_block_data( $blk_el, $type, $doc, $xpath );
        $settings = $this->parse_block_settings( $blk_el, $type );

        $block = array(
            'id'       => $this->preserve_or_generate_id( $blk_el, 'idve_blk' ),
            'type'     => $type,
            'data'     => $data,
            'settings' => $settings,
        );

        // Group blocks have child blocks.
        if ( 'group' === $type ) {
            $block['blocks'] = array();
            $children = $this->query_direct_child_blocks( $blk_el, $xpath );
            foreach ( $children as $child_el ) {
                $block['blocks'][] = $this->parse_block_from_element( $child_el, $doc, $xpath );
            }
        }

        return $block;
    }

    public function wrap_in_html_block( $html ) {
        return array(
            'id'       => $this->generate_id( 'idve_sec' ),
            'settings' => $this->default_section_settings(),
            'rows'     => array(
                array(
                    'id'      => $this->generate_id( 'idve_row' ),
                    'columns' => array(
                        array(
                            'id'       => $this->generate_id( 'idve_col' ),
                            'width'    => '1/1',
                            'settings' => $this->default_column_settings(),
                            'blocks'   => array(
                                array(
                                    'id'       => $this->generate_id( 'idve_blk' ),
                                    'type'     => 'html',
                                    'data'     => array( 'content' => $html ),
                                    'settings' => $this->default_block_settings(),
                                ),
                            ),
                        ),
                    ),
                ),
            ),
        );
    }

    // ── Type Detection ──

    private function parse_column_width( $class_string ) {
        $widths = array(
            'idve-col-1-1' => '1/1', 'idve-col-1-2' => '1/2',
            'idve-col-1-3' => '1/3', 'idve-col-2-3' => '2/3',
            'idve-col-1-4' => '1/4', 'idve-col-3-4' => '3/4',
            'idve-col-1-5' => '1/5', 'idve-col-2-5' => '2/5',
            'idve-col-3-5' => '3/5', 'idve-col-4-5' => '4/5',
        );
        foreach ( $widths as $cls => $frac ) {
            if ( strpos( $class_string, $cls ) !== false ) {
                return $frac;
            }
        }
        return '1/1';
    }

    private function parse_block_type( $class_string ) {
        // Check group BEFORE other types since 'idve-block-group' could false-match a prefix check.
        $types = array( 'group', 'text', 'heading', 'image', 'button', 'spacer', 'video', 'divider', 'audio', 'html' );
        foreach ( $types as $type ) {
            if ( strpos( $class_string, 'idve-block-' . $type ) !== false ) {
                return $type;
            }
        }

        // Check registered external block types.
        if ( $this->blocks ) {
            foreach ( array_keys( $this->blocks->get_all() ) as $registered_type ) {
                // Skip core types already checked above.
                if ( in_array( $registered_type, $types, true ) ) {
                    continue;
                }
                if ( strpos( $class_string, 'idve-block-' . $registered_type ) !== false ) {
                    return $registered_type;
                }
            }
        }

        return 'html';
    }

    // ── Style Parsing Helpers ──

    /**
     * Parse an inline style attribute into an associative array.
     * 'text-align:center; color:#fff' → ['text-align' => 'center', 'color' => '#fff']
     */
    private function parse_inline_style( $element ) {
        $style_str = '';
        if ( is_object( $element ) ) {
            $style_str = $element->getAttribute( 'style' );
        } elseif ( is_string( $element ) ) {
            $style_str = $element;
        }

        $styles = array();
        if ( empty( $style_str ) ) {
            return $styles;
        }

        $parts = explode( ';', $style_str );
        foreach ( $parts as $part ) {
            $part = trim( $part );
            if ( empty( $part ) ) {
                continue;
            }
            $colon = strpos( $part, ':' );
            if ( false === $colon ) {
                continue;
            }
            $prop = strtolower( trim( substr( $part, 0, $colon ) ) );
            $val  = trim( substr( $part, $colon + 1 ) );
            $styles[ $prop ] = $val;
        }

        return $styles;
    }

    /**
     * Parse a CSS shorthand spacing value like '5px 10px 5px 10px' into IDVE spacing array.
     */
    private function parse_spacing_style( $value ) {
        $result = array( 'top' => 0, 'right' => 0, 'bottom' => 0, 'left' => 0 );
        if ( empty( $value ) ) {
            return $result;
        }

        // Extract numeric values (strip 'px').
        $parts = preg_split( '/\s+/', trim( $value ) );
        $nums  = array();
        foreach ( $parts as $p ) {
            $nums[] = intval( $p );
        }

        $count = count( $nums );
        if ( 4 === $count ) {
            $result = array( 'top' => $nums[0], 'right' => $nums[1], 'bottom' => $nums[2], 'left' => $nums[3] );
        } elseif ( 3 === $count ) {
            $result = array( 'top' => $nums[0], 'right' => $nums[1], 'bottom' => $nums[2], 'left' => $nums[1] );
        } elseif ( 2 === $count ) {
            $result = array( 'top' => $nums[0], 'right' => $nums[1], 'bottom' => $nums[0], 'left' => $nums[1] );
        } elseif ( 1 === $count ) {
            $result = array( 'top' => $nums[0], 'right' => $nums[0], 'bottom' => $nums[0], 'left' => $nums[0] );
        }

        return $result;
    }

    /**
     * Parse border-related CSS properties into the IDVE border settings structure.
     */
    private function parse_border_styles( $styles ) {
        $border = array(
            'width'  => array( 'top' => 0, 'right' => 0, 'bottom' => 0, 'left' => 0 ),
            'style'  => 'solid',
            'color'  => '#000000',
            'radius' => array( 'topLeft' => 0, 'topRight' => 0, 'bottomRight' => 0, 'bottomLeft' => 0 ),
        );

        $has_border = false;

        foreach ( array( 'top', 'right', 'bottom', 'left' ) as $side ) {
            if ( isset( $styles[ 'border-' . $side ] ) ) {
                $has_border = true;
                // Parse "2px solid #000000" format.
                if ( preg_match( '/(\d+)px\s+(\w+)\s+(.+)/', trim( $styles[ 'border-' . $side ] ), $m ) ) {
                    $border['width'][ $side ] = intval( $m[1] );
                    $border['style']          = $m[2];
                    $border['color']          = trim( $m[3] );
                }
            }
        }

        if ( isset( $styles['border-radius'] ) ) {
            $has_border = true;
            $parts = preg_split( '/\s+/', trim( $styles['border-radius'] ) );
            $nums  = array();
            foreach ( $parts as $p ) {
                $nums[] = intval( $p );
            }
            if ( count( $nums ) >= 4 ) {
                $border['radius'] = array(
                    'topLeft'     => $nums[0],
                    'topRight'    => $nums[1],
                    'bottomRight' => $nums[2],
                    'bottomLeft'  => $nums[3],
                );
            } elseif ( count( $nums ) === 1 && $nums[0] > 0 ) {
                $border['radius'] = array(
                    'topLeft'     => $nums[0],
                    'topRight'    => $nums[0],
                    'bottomRight' => $nums[0],
                    'bottomLeft'  => $nums[0],
                );
            }
        }

        return $has_border ? $border : null;
    }

    /**
     * Get inner HTML of an element.
     */
    private function get_inner_html( $element, $doc ) {
        $inner = '';
        foreach ( $element->childNodes as $child ) {
            $inner .= $doc->saveHTML( $child );
        }
        return $inner;
    }

    /**
     * Extract extra CSS classes from a class string, excluding IDVE system classes.
     */
    private function extract_extra_classes( $class_string, $system_prefixes = array() ) {
        $classes = preg_split( '/\s+/', trim( $class_string ) );
        $extra   = array();
        $default_prefixes = array( 'idve-block', 'idve-group' );
        $prefixes = array_merge( $default_prefixes, $system_prefixes );

        foreach ( $classes as $cls ) {
            if ( empty( $cls ) ) {
                continue;
            }
            $is_system = false;
            foreach ( $prefixes as $prefix ) {
                if ( $cls === $prefix || strpos( $cls, $prefix . '-' ) === 0 ) {
                    $is_system = true;
                    break;
                }
            }
            if ( ! $is_system ) {
                $extra[] = $cls;
            }
        }

        return implode( ' ', $extra );
    }

    // ── Block Data Extraction ──

    private function parse_block_data( $element, $type, $doc, $xpath ) {
        $inner        = $this->get_inner_html( $element, $doc );
        $wrapper_styles = $this->parse_inline_style( $element );

        switch ( $type ) {
            case 'text':
                return $this->parse_text_data( $element, $inner, $wrapper_styles, $doc );

            case 'heading':
                return $this->parse_heading_data( $element, $inner, $wrapper_styles, $doc );

            case 'image':
                return $this->parse_image_data( $element, $inner, $wrapper_styles, $doc );

            case 'button':
                return $this->parse_button_data( $element, $inner, $wrapper_styles, $doc );

            case 'spacer':
                return $this->parse_spacer_data( $element, $inner, $wrapper_styles, $doc );

            case 'video':
                return $this->parse_video_data( $element, $inner, $wrapper_styles, $doc );

            case 'divider':
                return $this->parse_divider_data( $element, $inner, $wrapper_styles, $doc );

            case 'audio':
                return $this->parse_audio_data( $element, $inner, $wrapper_styles, $doc );

            case 'group':
                return $this->parse_group_data( $element, $inner, $wrapper_styles, $doc );

            case 'html':
                return array( 'content' => $inner );

            default:
                return $this->parse_external_block_data( $element, $type, $inner, $doc );
        }
    }

    /**
     * Parse data for non-core (external/plugin) block types.
     *
     * Priority: custom import callback → filter → generic shortcode parser → HTML fallback.
     */
    private function parse_external_block_data( $element, $type, $inner, $doc ) {
        // 1. Custom import callback from block registration.
        if ( $this->blocks ) {
            $config = $this->blocks->get( $type );
            if ( $config && ! empty( $config['import'] ) && is_callable( $config['import'] ) ) {
                $result = call_user_func( $config['import'], $element, $inner, $doc );
                if ( is_array( $result ) ) {
                    return $result;
                }
            }
        }

        // 2. Filter: let any plugin intercept.
        $filtered = apply_filters( 'idve_import_block_data', null, $type, $inner );
        if ( is_array( $filtered ) ) {
            return $filtered;
        }

        // 3. Generic shortcode parser.
        $shortcode_data = $this->parse_shortcode_data( $type, $inner );
        if ( $shortcode_data ) {
            return $shortcode_data;
        }

        // 4. Fall back to HTML block content.
        return array( 'content' => $inner );
    }

    /**
     * Generic shortcode-to-data reverse-engineering.
     *
     * Detects [$type attr1="val1" attr2="val2"] or [$type ...]content[/$type] in innerHTML,
     * parses attributes, and maps them to registered field names with defaults.
     *
     * @param string $type      The block type (e.g. 'id_post_grid').
     * @param string $inner     The inner HTML of the block element.
     * @return array|null        Parsed data array, or null if no shortcode found.
     */
    private function parse_shortcode_data( $type, $inner ) {
        // Build regex to find [$type ...] — self-closing or opening tag.
        $tag = preg_quote( $type, '/' );

        // Match self-closing: [$type attrs]  or  [$type attrs /]
        // Match enclosing:    [$type attrs]content[/$type]
        $pattern = '/\[' . $tag . '(\s[^\]]*?)?\](.*?)(?:\[\/' . $tag . '\])?/s';

        if ( ! preg_match( $pattern, $inner, $m ) ) {
            return null;
        }

        $attr_string = isset( $m[1] ) ? trim( $m[1] ) : '';
        $content     = isset( $m[2] ) ? trim( $m[2] ) : '';

        // Parse attributes from the shortcode tag.
        $attrs = $this->parse_shortcode_attrs( $attr_string );

        // Build data array from registered field defaults, then overlay parsed attributes.
        $data = array();
        if ( $this->blocks ) {
            $config = $this->blocks->get( $type );
            if ( $config && ! empty( $config['fields'] ) ) {
                // Start with field defaults.
                foreach ( $config['fields'] as $field_name => $field_def ) {
                    $data[ $field_name ] = isset( $field_def['default'] ) ? $field_def['default'] : '';
                }

                // Overlay parsed shortcode attributes onto matching field names.
                foreach ( $attrs as $attr_name => $attr_val ) {
                    if ( array_key_exists( $attr_name, $config['fields'] ) ) {
                        $data[ $attr_name ] = $attr_val;
                    }
                }

                // Map enclosing content to the appropriate field.
                if ( '' !== $content ) {
                    $content_field = $this->find_content_field( $config['fields'] );
                    if ( $content_field ) {
                        $data[ $content_field ] = $content;
                    }
                }

                return $data;
            }
        }

        // No field definitions available — return raw attributes.
        if ( ! empty( $attrs ) || '' !== $content ) {
            if ( '' !== $content ) {
                $attrs['content'] = $content;
            }
            return $attrs;
        }

        return null;
    }

    /**
     * Parse shortcode attribute string into an associative array.
     * Handles: attr="val", attr='val', attr=val, and bare attributes.
     */
    private function parse_shortcode_attrs( $attr_string ) {
        $attrs = array();
        if ( empty( $attr_string ) ) {
            return $attrs;
        }

        // Match key="value", key='value', key=value patterns.
        $pattern = '/(\w+)\s*=\s*(?:"([^"]*?)"|\'([^\']*?)\'|(\S+))/';
        if ( preg_match_all( $pattern, $attr_string, $matches, PREG_SET_ORDER ) ) {
            foreach ( $matches as $match ) {
                $key = $match[1];
                if ( '' !== $match[2] ) {
                    $val = $match[2];
                } elseif ( isset( $match[3] ) && '' !== $match[3] ) {
                    $val = $match[3];
                } else {
                    $val = isset( $match[4] ) ? $match[4] : '';
                }
                $attrs[ $key ] = $val;
            }
        }

        return $attrs;
    }

    /**
     * Find the best field name to map enclosing shortcode content to.
     * Checks 'content' first, then 'message', then the first textarea/richtext field.
     */
    private function find_content_field( $fields ) {
        // Priority 1: field named 'content'.
        if ( isset( $fields['content'] ) ) {
            return 'content';
        }

        // Priority 2: field named 'message'.
        if ( isset( $fields['message'] ) ) {
            return 'message';
        }

        // Priority 3: first textarea or richtext field.
        foreach ( $fields as $name => $def ) {
            $field_type = isset( $def['type'] ) ? $def['type'] : 'text';
            if ( 'textarea' === $field_type || 'richtext' === $field_type ) {
                return $name;
            }
        }

        return null;
    }

    private function parse_text_data( $element, $inner, $wrapper_styles, $doc ) {
        $data = array(
            'content'   => '',
            'alignment' => 'left',
            'textColor' => '',
            'fontSize'  => '',
        );

        // Wrapper styles carry alignment, color, font-size.
        if ( ! empty( $wrapper_styles['text-align'] ) ) {
            $data['alignment'] = $wrapper_styles['text-align'];
        }
        if ( ! empty( $wrapper_styles['color'] ) ) {
            $data['textColor'] = $wrapper_styles['color'];
        }
        if ( ! empty( $wrapper_styles['font-size'] ) ) {
            $data['fontSize'] = $wrapper_styles['font-size'];
        }

        // The text block renders as <div [class]>content</div> inside the wrapper.
        // Find the first child <div> and use its innerHTML as content.
        $found_div = false;
        foreach ( $element->childNodes as $child ) {
            if ( $child->nodeType === XML_ELEMENT_NODE && strtolower( $child->nodeName ) === 'div' ) {
                $data['content'] = $this->get_inner_html( $child, $doc );
                $found_div = true;
                break;
            }
        }

        // Fallback: if no inner div found, use full inner HTML.
        if ( ! $found_div ) {
            $data['content'] = $inner;
        }

        return $data;
    }

    private function parse_heading_data( $element, $inner, $wrapper_styles, $doc ) {
        $data = array(
            'content'   => '',
            'level'     => 'h2',
            'alignment' => 'left',
            'color'     => '',
            'fontSize'  => '',
        );

        // Wrapper carries text-align for alignment.
        if ( ! empty( $wrapper_styles['text-align'] ) ) {
            $data['alignment'] = $wrapper_styles['text-align'];
        }

        // Find the <h1>-<h6> element inside.
        foreach ( $element->childNodes as $child ) {
            if ( $child->nodeType === XML_ELEMENT_NODE && preg_match( '/^h[1-6]$/i', $child->nodeName ) ) {
                $data['level']   = strtolower( $child->nodeName );
                $data['content'] = $this->get_inner_html( $child, $doc );

                // Heading styles are on the <h*> element.
                $h_styles = $this->parse_inline_style( $child );
                if ( ! empty( $h_styles['color'] ) ) {
                    $data['color'] = $h_styles['color'];
                }
                if ( ! empty( $h_styles['font-size'] ) ) {
                    $data['fontSize'] = $h_styles['font-size'];
                }
                break;
            }
        }

        // Fallback: regex if DOM traversal didn't find it.
        if ( empty( $data['content'] ) && preg_match( '/<(h[1-6])[^>]*>(.*?)<\/\1>/is', $inner, $m ) ) {
            $data['level']   = $m[1];
            $data['content'] = $m[2];
        }

        return $data;
    }

    private function parse_image_data( $element, $inner, $wrapper_styles, $doc ) {
        $data = array(
            'imageId'    => 0,
            'imageUrl'   => '',
            'alt'        => '',
            'alignment'  => 'center',
            'linkUrl'    => '',
            'linkTarget' => '_self',
            'maxWidth'   => '',
            'imageSize'  => 'full',
        );

        // Wrapper carries text-align for alignment.
        if ( ! empty( $wrapper_styles['text-align'] ) ) {
            $data['alignment'] = $wrapper_styles['text-align'];
        }

        // Find <img> element.
        if ( preg_match( '/<img[^>]+src=["\']([^"\']+)["\']/', $inner, $m ) ) {
            $data['imageUrl'] = $m[1];
        }
        if ( preg_match( '/<img[^>]+alt=["\']([^"\']*)["\']/', $inner, $m ) ) {
            $data['alt'] = $m[1];
        }

        // Parse max-width from <img> style.
        if ( preg_match( '/<img[^>]+style=["\']([^"\']*)["\']/', $inner, $m ) ) {
            $img_styles = $this->parse_inline_style( $m[1] );
            if ( ! empty( $img_styles['max-width'] ) && '100%' !== $img_styles['max-width'] ) {
                $data['maxWidth'] = $img_styles['max-width'];
            }
        }

        // Check for <a> wrapper around the image.
        if ( preg_match( '/<a[^>]+href=["\']([^"\']+)["\']/', $inner, $m ) ) {
            $data['linkUrl'] = $m[1];
        }
        if ( preg_match( '/<a[^>]+target=["\']([^"\']+)["\']/', $inner, $m ) ) {
            $data['linkTarget'] = $m[1];
        }

        return $data;
    }

    private function parse_button_data( $element, $inner, $wrapper_styles, $doc ) {
        $data = array(
            'label'           => '',
            'url'             => '#',
            'target'          => '_self',
            'alignment'       => 'center',
            'backgroundColor' => '#0073aa',
            'textColor'       => '#ffffff',
            'size'            => 'medium',
            'fullWidth'       => false,
        );

        // Wrapper carries text-align for alignment.
        if ( ! empty( $wrapper_styles['text-align'] ) ) {
            $data['alignment'] = $wrapper_styles['text-align'];
        }

        // Find the <a> element.
        if ( preg_match( '/<a[^>]+href=["\']([^"\']+)["\']/', $inner, $m ) ) {
            $data['url'] = $m[1];
        }
        if ( preg_match( '/<a[^>]+target=["\']([^"\']+)["\']/', $inner, $m ) ) {
            $data['target'] = $m[1];
        }
        if ( preg_match( '/<a[^>]*>(.*?)<\/a>/is', $inner, $m ) ) {
            $data['label'] = wp_strip_all_tags( $m[1] );
        }

        // Parse size from <a> class.
        if ( preg_match( '/<a[^>]+class=["\']([^"\']*)["\']/', $inner, $m ) ) {
            $a_class = $m[1];
            if ( strpos( $a_class, 'idve-button-small' ) !== false ) {
                $data['size'] = 'small';
            } elseif ( strpos( $a_class, 'idve-button-large' ) !== false ) {
                $data['size'] = 'large';
            } else {
                $data['size'] = 'medium';
            }
            // Check full width.
            if ( strpos( $a_class, 'idve-button-full' ) !== false ) {
                $data['fullWidth'] = true;
            }
        }

        // Parse colors from <a> inline style.
        if ( preg_match( '/<a[^>]+style=["\']([^"\']*)["\']/', $inner, $m ) ) {
            $a_styles = $this->parse_inline_style( $m[1] );
            if ( ! empty( $a_styles['background-color'] ) ) {
                $data['backgroundColor'] = $a_styles['background-color'];
            }
            if ( ! empty( $a_styles['color'] ) ) {
                $data['textColor'] = $a_styles['color'];
            }
            // Full width detection from style as well.
            if ( ! empty( $a_styles['width'] ) && '100%' === $a_styles['width'] ) {
                $data['fullWidth'] = true;
            }
        }

        return $data;
    }

    private function parse_spacer_data( $element, $inner, $wrapper_styles, $doc ) {
        $data = array( 'height' => 40 );

        // The spacer renders an inner div with class idve-spacer.
        if ( preg_match( '/style=["\']([^"\']*)["\']/', $inner, $m ) ) {
            $spacer_styles = $this->parse_inline_style( $m[1] );
            if ( ! empty( $spacer_styles['height'] ) ) {
                $data['height'] = intval( $spacer_styles['height'] );
            }
        }

        // Also check wrapper height (renderer puts it on wrapper too).
        if ( ! empty( $wrapper_styles['height'] ) ) {
            $data['height'] = intval( $wrapper_styles['height'] );
        }

        return $data;
    }

    private function parse_video_data( $element, $inner, $wrapper_styles, $doc ) {
        $data = array(
            'url'         => '',
            'aspectRatio' => '16:9',
        );

        // Parse aspect ratio from wrapper class on the inner video-wrapper div.
        if ( preg_match( '/idve-aspect-(\d+)-(\d+)/', $inner, $m ) ) {
            $data['aspectRatio'] = $m[1] . ':' . $m[2];
        }

        // Reverse iframe embed URL back to watch URL.
        if ( preg_match( '/youtube\.com\/embed\/([a-zA-Z0-9_-]{11})/', $inner, $m ) ) {
            $data['url'] = 'https://www.youtube.com/watch?v=' . $m[1];
        } elseif ( preg_match( '/player\.vimeo\.com\/video\/(\d+)/', $inner, $m ) ) {
            $data['url'] = 'https://vimeo.com/' . $m[1];
        } elseif ( preg_match( '/<video[^>]+src=["\']([^"\']+)["\']/', $inner, $m ) ) {
            $data['url'] = $m[1];
        } elseif ( preg_match( '/<iframe[^>]+src=["\']([^"\']+)["\']/', $inner, $m ) ) {
            // Fallback: use the iframe src directly.
            $data['url'] = $m[1];
        }

        return $data;
    }

    private function parse_divider_data( $element, $inner, $wrapper_styles, $doc ) {
        $data = array(
            'thickness' => 2,
            'color'     => '#cccccc',
            'width'     => '100%',
            'alignment' => 'center',
        );

        // The divider renders as an <hr> with inline styles.
        if ( preg_match( '/<hr[^>]+style=["\']([^"\']*)["\']/', $inner, $m ) ) {
            $hr_styles = $this->parse_inline_style( $m[1] );

            if ( ! empty( $hr_styles['height'] ) ) {
                $data['thickness'] = intval( $hr_styles['height'] );
            }
            if ( ! empty( $hr_styles['background-color'] ) ) {
                $data['color'] = $hr_styles['background-color'];
            }
            if ( ! empty( $hr_styles['width'] ) ) {
                $data['width'] = $hr_styles['width'];
            }

            // Determine alignment from margin.
            if ( ! empty( $hr_styles['margin'] ) ) {
                $margin = trim( $hr_styles['margin'] );
                if ( '0 auto 0 0' === $margin ) {
                    $data['alignment'] = 'left';
                } elseif ( '0 0 0 auto' === $margin ) {
                    $data['alignment'] = 'right';
                } else {
                    $data['alignment'] = 'center';
                }
            }
        }

        return $data;
    }

    private function parse_audio_data( $element, $inner, $wrapper_styles, $doc ) {
        $data = array(
            'audioUrl' => '',
            'controls' => true,
            'loop'     => false,
            'autoplay' => false,
            'preload'  => 'metadata',
            'caption'  => '',
        );

        // Parse <audio> attributes.
        if ( preg_match( '/<audio[^>]+src=["\']([^"\']+)["\']/', $inner, $m ) ) {
            $data['audioUrl'] = $m[1];
        }

        // Extract the full <audio> tag to check boolean attributes.
        if ( preg_match( '/<audio([^>]*)>/', $inner, $m ) ) {
            $audio_attrs = $m[1];
            $data['controls'] = ( strpos( $audio_attrs, 'controls' ) !== false );
            $data['loop']     = ( strpos( $audio_attrs, 'loop' ) !== false );
            $data['autoplay'] = ( strpos( $audio_attrs, 'autoplay' ) !== false );

            if ( preg_match( '/preload=["\']([^"\']+)["\']/', $audio_attrs, $pm ) ) {
                $data['preload'] = $pm[1];
            }
        }

        // Parse caption from <figcaption>.
        if ( preg_match( '/<figcaption>(.*?)<\/figcaption>/is', $inner, $m ) ) {
            $data['caption'] = wp_strip_all_tags( $m[1] );
        }

        return $data;
    }

    private function parse_group_data( $element, $inner, $wrapper_styles, $doc ) {
        $data = array(
            'minHeight' => '',
        );

        if ( ! empty( $wrapper_styles['min-height'] ) ) {
            $data['minHeight'] = $wrapper_styles['min-height'];
        }

        return $data;
    }

    // ── Settings Extraction ──

    /**
     * Parse block wrapper settings from the element's inline styles and attributes.
     * For button and image blocks, settings-level styles are on the inner <a>/<img>, not the wrapper.
     */
    private function parse_block_settings( $blk_el, $type ) {
        $settings = $this->default_block_settings();
        $styles   = $this->parse_inline_style( $blk_el );
        $cls      = $blk_el->getAttribute( 'class' );

        // For button/image blocks, the renderer doesn't put settings_to_style on the wrapper.
        // Instead, settings are on the inner <a>/<img> element. Parse from inner HTML.
        if ( 'button' === $type || 'image' === $type ) {
            $settings = $this->parse_inner_element_settings( $blk_el, $type, $settings );
        } else {
            // Standard blocks: settings are on the wrapper div.
            if ( ! empty( $styles['padding'] ) ) {
                $settings['padding'] = $this->parse_spacing_style( $styles['padding'] );
            }
            if ( ! empty( $styles['margin'] ) ) {
                $settings['margin'] = $this->parse_spacing_style( $styles['margin'] );
            }
            if ( ! empty( $styles['background-color'] ) ) {
                $settings['backgroundColor'] = $styles['background-color'];
            }

            // Parse border from wrapper styles.
            $border = $this->parse_border_styles( $styles );
            if ( $border ) {
                $settings['border'] = $border;
            }
        }

        // Visibility.
        if ( ! empty( $styles['display'] ) && 'none' === $styles['display'] ) {
            $settings['visible'] = false;
        }
        if ( $blk_el->getAttribute( 'data-idve-hidden' ) === '1' ) {
            $settings['visible'] = false;
        }

        // CSS class: extract non-system classes from the wrapper.
        $extra_classes = $this->extract_extra_classes( $cls );
        if ( ! empty( $extra_classes ) ) {
            $settings['cssClass'] = $extra_classes;
        }

        // CSS ID: if the element has an `id` attribute, it's always user-set (internal ID is on data-idve-id).
        $el_id = $blk_el->getAttribute( 'id' );
        if ( ! empty( $el_id ) ) {
            $settings['cssId'] = $el_id;
        }

        return $settings;
    }

    /**
     * For button and image blocks, extract settings-level styles from the inner <a>/<img> element.
     */
    private function parse_inner_element_settings( $blk_el, $type, $settings ) {
        $doc   = $blk_el->ownerDocument;
        $inner = $this->get_inner_html( $blk_el, $doc );

        if ( 'button' === $type ) {
            // Button: settings are on the <a> element.
            if ( preg_match( '/<a[^>]+style=["\']([^"\']*)["\']/', $inner, $m ) ) {
                $a_styles = $this->parse_inline_style( $m[1] );

                if ( ! empty( $a_styles['padding'] ) ) {
                    $settings['padding'] = $this->parse_spacing_style( $a_styles['padding'] );
                }
                if ( ! empty( $a_styles['margin'] ) ) {
                    $settings['margin'] = $this->parse_spacing_style( $a_styles['margin'] );
                }

                $border = $this->parse_border_styles( $a_styles );
                if ( $border ) {
                    $settings['border'] = $border;
                }
            }
        }

        if ( 'image' === $type ) {
            // Image: settings are on the <img> element.
            if ( preg_match( '/<img[^>]+style=["\']([^"\']*)["\']/', $inner, $m ) ) {
                $img_styles = $this->parse_inline_style( $m[1] );

                if ( ! empty( $img_styles['padding'] ) ) {
                    $settings['padding'] = $this->parse_spacing_style( $img_styles['padding'] );
                }
                if ( ! empty( $img_styles['margin'] ) ) {
                    $settings['margin'] = $this->parse_spacing_style( $img_styles['margin'] );
                }
                if ( ! empty( $img_styles['background-color'] ) ) {
                    $settings['backgroundColor'] = $img_styles['background-color'];
                }

                $border = $this->parse_border_styles( $img_styles );
                if ( $border ) {
                    $settings['border'] = $border;
                }
            }
        }

        return $settings;
    }

    /**
     * Parse section settings from the section element.
     */
    private function parse_section_settings( $sec_el, $xpath ) {
        $settings = $this->default_section_settings();
        $styles   = $this->parse_inline_style( $sec_el );
        $cls      = $sec_el->getAttribute( 'class' );

        if ( ! empty( $styles['padding'] ) ) {
            $settings['padding'] = $this->parse_spacing_style( $styles['padding'] );
        }
        if ( ! empty( $styles['margin'] ) ) {
            $settings['margin'] = $this->parse_spacing_style( $styles['margin'] );
        }
        if ( ! empty( $styles['background-color'] ) ) {
            $settings['backgroundColor'] = $styles['background-color'];
        }
        if ( ! empty( $styles['background-image'] ) ) {
            // Extract URL from url(...).
            if ( preg_match( '/url\(([^)]+)\)/', $styles['background-image'], $m ) ) {
                $settings['backgroundImage'] = trim( $m[1], '"\'' );
            }
        }
        if ( ! empty( $styles['background-size'] ) ) {
            $settings['backgroundSize'] = $styles['background-size'];
        }
        if ( ! empty( $styles['background-position'] ) ) {
            $settings['backgroundPosition'] = $styles['background-position'];
        }
        if ( ! empty( $styles['background-repeat'] ) ) {
            $settings['backgroundRepeat'] = $styles['background-repeat'];
        }

        // Border.
        $border = $this->parse_border_styles( $styles );
        if ( $border ) {
            $settings['border'] = $border;
        }

        // Stretch mode from CSS classes.
        if ( strpos( $cls, 'idve-section-stretch-full' ) !== false ) {
            $settings['stretch'] = 'full';
        } elseif ( strpos( $cls, 'idve-section-stretch-bg' ) !== false ) {
            $settings['stretch'] = 'full-bg';
        }

        // Check for max-width on inner container.
        $inner_el = $xpath->query( ".//*[contains(concat(' ',normalize-space(@class),' '),' idve-section-inner ')]", $sec_el );
        if ( $inner_el->length > 0 ) {
            $inner_styles = $this->parse_inline_style( $inner_el->item( 0 ) );
            if ( ! empty( $inner_styles['max-width'] ) ) {
                if ( 'full-bg' === $settings['stretch'] ) {
                    $settings['contentMaxWidth'] = $inner_styles['max-width'];
                } else {
                    $settings['maxWidth'] = $inner_styles['max-width'];
                }
            }
        }

        // CSS class: extract non-system classes.
        $extra = $this->extract_extra_classes( $cls, array( 'idve-section', 'idve-section-stretch-full', 'idve-section-stretch-bg' ) );
        if ( ! empty( $extra ) ) {
            $settings['cssClass'] = $extra;
        }

        // CSS ID: if the element has an `id` attribute, it's always user-set (internal ID is on data-idve-id).
        $el_id = $sec_el->getAttribute( 'id' );
        if ( ! empty( $el_id ) ) {
            $settings['cssId'] = $el_id;
        }

        return $settings;
    }

    /**
     * Parse row settings from the row element.
     */
    private function parse_row_settings( $row_el, $xpath ) {
        $settings = $this->default_row_settings();
        $styles   = $this->parse_inline_style( $row_el );
        $cls      = $row_el->getAttribute( 'class' );

        // Equal height from CSS class.
        if ( strpos( $cls, 'idve-row-equal-height' ) !== false ) {
            $settings['equalHeight'] = true;
        }

        // Gap from inline style.
        if ( ! empty( $styles['gap'] ) ) {
            $settings['gap'] = $styles['gap'];
        }

        // Background color.
        if ( ! empty( $styles['background-color'] ) ) {
            $settings['backgroundColor'] = $styles['background-color'];
        }

        // Background image and related properties.
        if ( ! empty( $styles['background-image'] ) ) {
            if ( preg_match( '/url\(([^)]+)\)/', $styles['background-image'], $m ) ) {
                $settings['backgroundImage'] = trim( $m[1], '"\'' );
            }
        }
        if ( ! empty( $styles['background-size'] ) ) {
            $settings['backgroundSize'] = $styles['background-size'];
        }
        if ( ! empty( $styles['background-position'] ) ) {
            $settings['backgroundPosition'] = $styles['background-position'];
        }
        if ( ! empty( $styles['background-repeat'] ) ) {
            $settings['backgroundRepeat'] = $styles['background-repeat'];
        }

        if ( ! empty( $styles['padding'] ) ) {
            $settings['padding'] = $this->parse_spacing_style( $styles['padding'] );
        }

        if ( ! empty( $styles['margin'] ) ) {
            $settings['margin'] = $this->parse_spacing_style( $styles['margin'] );
        }

        // Border.
        $border = $this->parse_border_styles( $styles );
        if ( $border ) {
            $settings['border'] = $border;
        }

        // CSS class: extract non-system classes.
        $extra = $this->extract_extra_classes( $cls, array( 'idve-row', 'idve-row-equal-height' ) );
        if ( ! empty( $extra ) ) {
            $settings['cssClass'] = $extra;
        }

        // CSS ID: if the element has an `id` attribute, it's always user-set (internal ID is on data-idve-id).
        $el_id = $row_el->getAttribute( 'id' );
        if ( ! empty( $el_id ) ) {
            $settings['cssId'] = $el_id;
        }

        return $settings;
    }

    /**
     * Parse column settings from the column element.
     */
    private function parse_column_settings( $col_el ) {
        $settings = $this->default_column_settings();
        $styles   = $this->parse_inline_style( $col_el );

        if ( ! empty( $styles['padding'] ) ) {
            $settings['padding'] = $this->parse_spacing_style( $styles['padding'] );
        }
        if ( ! empty( $styles['margin'] ) ) {
            $settings['margin'] = $this->parse_spacing_style( $styles['margin'] );
        }
        if ( ! empty( $styles['background-color'] ) ) {
            $settings['backgroundColor'] = $styles['background-color'];
        }

        // Background image and related properties.
        if ( ! empty( $styles['background-image'] ) ) {
            if ( preg_match( '/url\(([^)]+)\)/', $styles['background-image'], $m ) ) {
                $settings['backgroundImage'] = trim( $m[1], '"\'' );
            }
        }
        if ( ! empty( $styles['background-size'] ) ) {
            $settings['backgroundSize'] = $styles['background-size'];
        }
        if ( ! empty( $styles['background-position'] ) ) {
            $settings['backgroundPosition'] = $styles['background-position'];
        }
        if ( ! empty( $styles['background-repeat'] ) ) {
            $settings['backgroundRepeat'] = $styles['background-repeat'];
        }

        // Border.
        $border = $this->parse_border_styles( $styles );
        if ( $border ) {
            $settings['border'] = $border;
        }

        // Vertical alignment from align-self or justify-content.
        if ( ! empty( $styles['align-self'] ) ) {
            if ( 'center' === $styles['align-self'] ) {
                $settings['verticalAlign'] = 'center';
            } elseif ( 'flex-end' === $styles['align-self'] ) {
                $settings['verticalAlign'] = 'bottom';
            }
        }
        if ( ! empty( $styles['justify-content'] ) ) {
            if ( 'center' === $styles['justify-content'] ) {
                $settings['verticalAlign'] = 'center';
            } elseif ( 'flex-end' === $styles['justify-content'] ) {
                $settings['verticalAlign'] = 'bottom';
            }
        }

        // CSS class: extract non-system classes.
        $cls   = $col_el->getAttribute( 'class' );
        $extra = $this->extract_extra_classes( $cls, array( 'idve-col' ) );
        if ( ! empty( $extra ) ) {
            $settings['cssClass'] = $extra;
        }

        // CSS ID: if the element has an `id` attribute, it's always user-set (internal ID is on data-idve-id).
        $el_id = $col_el->getAttribute( 'id' );
        if ( ! empty( $el_id ) ) {
            $settings['cssId'] = $el_id;
        }

        return $settings;
    }

    // ── Settings Merge (Lossless Import) ──

    /**
     * Build a flat ID → element data map from the old IDVE tree.
     * Captures settings (and width for columns) keyed by element ID.
     */
    private function build_settings_map( $old_tree ) {
        $map = array();
        if ( empty( $old_tree['sections'] ) ) {
            return $map;
        }

        foreach ( $old_tree['sections'] as $section ) {
            if ( ! empty( $section['id'] ) && isset( $section['settings'] ) ) {
                $map[ $section['id'] ] = array( 'settings' => $section['settings'] );
            }
            if ( empty( $section['rows'] ) ) {
                continue;
            }
            foreach ( $section['rows'] as $row ) {
                if ( ! empty( $row['id'] ) && isset( $row['settings'] ) ) {
                    $map[ $row['id'] ] = array( 'settings' => $row['settings'] );
                }
                if ( empty( $row['columns'] ) ) {
                    continue;
                }
                foreach ( $row['columns'] as $col ) {
                    if ( ! empty( $col['id'] ) && isset( $col['settings'] ) ) {
                        $entry = array( 'settings' => $col['settings'] );
                        if ( isset( $col['width'] ) ) {
                            $entry['width'] = $col['width'];
                        }
                        $map[ $col['id'] ] = $entry;
                    }
                    if ( ! empty( $col['blocks'] ) ) {
                        $this->build_block_settings_map( $col['blocks'], $map );
                    }
                }
            }
        }

        return $map;
    }

    /**
     * Recursively add block settings to the map (handles group blocks).
     */
    private function build_block_settings_map( $blocks, &$map ) {
        foreach ( $blocks as $block ) {
            if ( ! empty( $block['id'] ) && isset( $block['settings'] ) ) {
                $map[ $block['id'] ] = array( 'settings' => $block['settings'] );
            }
            // Recurse into group block children.
            if ( ! empty( $block['blocks'] ) ) {
                $this->build_block_settings_map( $block['blocks'], $map );
            }
        }
    }

    /**
     * Walk the new tree and replace settings from the old tree map where IDs match.
     * Content/data from the new tree is kept untouched.
     */
    private function apply_old_settings( &$tree, $map ) {
        if ( empty( $tree['sections'] ) || empty( $map ) ) {
            return;
        }

        foreach ( $tree['sections'] as &$section ) {
            if ( ! empty( $section['id'] ) && isset( $map[ $section['id'] ] ) ) {
                $section['settings'] = $map[ $section['id'] ]['settings'];
            }
            if ( empty( $section['rows'] ) ) {
                continue;
            }
            foreach ( $section['rows'] as &$row ) {
                if ( ! empty( $row['id'] ) && isset( $map[ $row['id'] ] ) ) {
                    $row['settings'] = $map[ $row['id'] ]['settings'];
                }
                if ( empty( $row['columns'] ) ) {
                    continue;
                }
                foreach ( $row['columns'] as &$col ) {
                    if ( ! empty( $col['id'] ) && isset( $map[ $col['id'] ] ) ) {
                        $col['settings'] = $map[ $col['id'] ]['settings'];
                        if ( isset( $map[ $col['id'] ]['width'] ) ) {
                            $col['width'] = $map[ $col['id'] ]['width'];
                        }
                    }
                    if ( ! empty( $col['blocks'] ) ) {
                        $this->apply_old_block_settings( $col['blocks'], $map );
                    }
                }
            }
        }
    }

    /**
     * Recursively apply old settings to blocks (handles group blocks).
     */
    private function apply_old_block_settings( &$blocks, $map ) {
        foreach ( $blocks as &$block ) {
            if ( ! empty( $block['id'] ) && isset( $map[ $block['id'] ] ) ) {
                $block['settings'] = $map[ $block['id'] ]['settings'];
            }
            // Recurse into group block children.
            if ( ! empty( $block['blocks'] ) ) {
                $this->apply_old_block_settings( $block['blocks'], $map );
            }
        }
    }

    // ── ID Generation & Defaults ──

    public function generate_id( $prefix ) {
        return $prefix . '_' . bin2hex( random_bytes( 4 ) );
    }

    /**
     * Preserve element's existing ID if it matches the expected prefix, otherwise generate a new one.
     */
    private function preserve_or_generate_id( $element, $prefix ) {
        $id = $element->getAttribute( 'data-idve-id' );
        if ( ! empty( $id ) && strpos( $id, $prefix . '_' ) === 0 ) {
            return $id;
        }
        return $this->generate_id( $prefix );
    }

    private function default_section_settings() {
        return array(
            'backgroundColor'   => '',
            'backgroundImage'   => '',
            'backgroundSize'    => 'cover',
            'backgroundPosition' => 'center',
            'backgroundRepeat'  => 'no-repeat',
            'padding'           => array( 'top' => 0, 'right' => 0, 'bottom' => 0, 'left' => 0 ),
            'margin'            => array( 'top' => 0, 'right' => 0, 'bottom' => 0, 'left' => 0 ),
            'border'            => array(
                'width'  => array( 'top' => 0, 'right' => 0, 'bottom' => 0, 'left' => 0 ),
                'style'  => 'solid',
                'color'  => '#000000',
                'radius' => array( 'topLeft' => 0, 'topRight' => 0, 'bottomRight' => 0, 'bottomLeft' => 0 ),
            ),
            'maxWidth'          => '',
            'stretch'           => 'default',
            'contentMaxWidth'   => '',
            'cssClass'          => '',
            'cssId'             => '',
        );
    }

    private function default_row_settings() {
        return array(
            'equalHeight'        => false,
            'gap'                => '',
            'backgroundColor'    => '',
            'backgroundImage'    => '',
            'backgroundSize'     => 'cover',
            'backgroundPosition' => 'center',
            'backgroundRepeat'   => 'no-repeat',
            'padding'            => array( 'top' => 0, 'right' => 0, 'bottom' => 0, 'left' => 0 ),
            'margin'             => array( 'top' => 0, 'right' => 0, 'bottom' => 0, 'left' => 0 ),
            'border'             => array(
                'width'  => array( 'top' => 0, 'right' => 0, 'bottom' => 0, 'left' => 0 ),
                'style'  => 'solid',
                'color'  => '#000000',
                'radius' => array( 'topLeft' => 0, 'topRight' => 0, 'bottomRight' => 0, 'bottomLeft' => 0 ),
            ),
            'cssClass'           => '',
            'cssId'              => '',
        );
    }

    private function default_column_settings() {
        return array(
            'padding'            => array( 'top' => 0, 'right' => 0, 'bottom' => 0, 'left' => 0 ),
            'margin'             => array( 'top' => 0, 'right' => 0, 'bottom' => 0, 'left' => 0 ),
            'backgroundColor'    => '',
            'backgroundImage'    => '',
            'backgroundSize'     => 'cover',
            'backgroundPosition' => 'center',
            'backgroundRepeat'   => 'no-repeat',
            'border'             => array(
                'width'  => array( 'top' => 0, 'right' => 0, 'bottom' => 0, 'left' => 0 ),
                'style'  => 'solid',
                'color'  => '#000000',
                'radius' => array( 'topLeft' => 0, 'topRight' => 0, 'bottomRight' => 0, 'bottomLeft' => 0 ),
            ),
            'verticalAlign'      => 'top',
            'cssClass'           => '',
            'cssId'              => '',
        );
    }

    private function default_block_settings() {
        return array(
            'margin'  => array( 'top' => 0, 'right' => 0, 'bottom' => 0, 'left' => 0 ),
            'padding' => array( 'top' => 0, 'right' => 0, 'bottom' => 0, 'left' => 0 ),
            'backgroundColor' => '', 'cssClass' => '', 'cssId' => '', 'visible' => true,
        );
    }
}
