<?php
/**
 * Elementor Schema Extractor
 * Automatically extracts control schemas from all Elementor elements and widgets
 */

class ElementorSchemaExtractor
{

    /**
     * Extract schema for specific control fields on a single element or widget type.
     * Examples:
     *   extract_schema_for_fields('section', 'background_image', 'element')
     *   extract_schema_for_fields('section', ['background_image','background_repeat'], 'element')
     *   extract_schema_for_fields('heading', 'title', 'widget')
     *
     * @param string $type_name element name (elType) or widget name
     * @param string|array $fields control name or array of control names
     * @param string $kind 'element' or 'widget' (default: 'element')
     * @return string|array Formatted output (same shape as other extractors)
     */
    /**
     * @param string $type_name
     * @param string|array<int,string> $fields
     * @param string $kind
     * @param int|null $post_id Optional post ID to scope extraction to widgets/elements used on that post
     * @return array<string,mixed>|string
     */
    public function extract_schema_for_fields($type_name, $fields, $kind = 'element', $post_id = null)
    {
        // Accept either array or comma-separated string for fields
        $fields = is_array($fields) ? $fields : array_map('trim', explode(',', (string) $fields));

        // Allow target to be passed as a path like "elements/section" or "widgets/heading"
        $original_target = $type_name;
        $folded_kind = $kind;
        // @phpstan-ignore function.alreadyNarrowedType
        if (is_string($type_name) && strpos($type_name, '/') !== false) {
            list($maybe_category, $maybe_name) = array_map('trim', explode('/', $type_name, 2));
            if (!empty($maybe_category) && !empty($maybe_name)) {
                if (in_array(strtolower($maybe_category), ['elements', 'element', 'el', 'eltype'])) {
                    $folded_kind = 'element';
                    $type_name = $maybe_name;

                } elseif (in_array(strtolower($maybe_category), ['widgets', 'widget'])) {
                    $folded_kind = 'widget';
                    $type_name = $maybe_name;
                }
            }
        }

        // Default empty response structure (will be adjusted to nested shape on success)
        $empty_fields = new \stdClass();

        // If a post_id is provided, attempt to read its elementor data and limit the
        // extraction to only the widget/element types that appear on that post.
        if ($post_id !== null) {
            try {
                $post = get_post((int) $post_id);
                if ($post && ($post instanceof \WP_Post)) {
                    $raw = get_post_meta((int) $post_id, '_elementor_data', true);

                    if (empty($raw)) {
                        // Fallback: try to decode post_content
                        $content = $post->post_content;
                        $decoded = @json_decode($content, true);
                        if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
                            $raw = $content;
                        }
                    }

                    if (!empty($raw)) {
                        $data = @json_decode($raw, true);
                        if (json_last_error() === JSON_ERROR_NONE && is_array($data)) {
                            $found = ['widget' => [], 'elType' => []];
                            $this->collect_elementor_types($data, $found);

                            $widget_types = array_values(array_unique($found['widget'] ?? []));
                            $element_types = array_values(array_unique($found['elType'] ?? []));

                            // If the requested type does not appear on the page, return empty structure
                            if ($folded_kind === 'widget' && !in_array($type_name, $widget_types, true)) {
                                return $this->format_output(['widgets' => [$type_name => $empty_fields]]);
                            }

                            if ($folded_kind !== 'widget' && !in_array($type_name, $element_types, true)) {
                                return $this->format_output(['elements' => [$type_name => $empty_fields]]);
                            }
                        }
                    }
                }
            } catch (\Throwable $e) {
                // If anything goes wrong while scoping by post, fall back to full behavior
                error_log('Miruni: Failed to scope extract_schema_for_fields by post_id ' . $post_id . ' - ' . $e->getMessage());
            }
        }

        // Resolve target element/widget class/instance
        try {
            if ($folded_kind === 'widget') {
                // @phpstan-ignore-next-line class.notFound
                $wm = \Elementor\Plugin::$instance->widgets_manager ?? null;
                // @phpstan-ignore-next-line
                $available = $wm && method_exists($wm, 'get_widget_types') ? $wm->get_widget_types() : [];

                // get_widget_types may return either an array of widgets or a single widget instance
                if (is_array($available)) {
                    $target = $available[$type_name] ?? null;
                } elseif (is_object($available)) {
                    // If a single widget instance is returned, use it as the target
                    $target = $available;
                } else {
                    $target = null;
                }
            } else {
                // @phpstan-ignore-next-line class.notFound
                $em = \Elementor\Plugin::$instance->elements_manager ?? null;
                // @phpstan-ignore-next-line
                $target = $em && method_exists($em, 'get_element_types') ? $em->get_element_types($type_name) : null;
            }
        } catch (\Throwable $e) {
            $target = null;
        }

        if (!$target) {
            // Return nested minimal structure matching caller's category intent
            if ($folded_kind === 'widget') {
                return $this->format_output(['widgets' => [$type_name => $empty_fields]]);
            }

            return $this->format_output(['elements' => [$type_name => $empty_fields]]);
        }

        // Reuse existing extractor for element controls
        $controls = $this->extract_controls_from_element($target);

        $out = [];
        if (empty($fields)) {
            // If no specific fields requested, return all
            $out = $controls;
        } else {
            foreach ($fields as $f) {
                if (array_key_exists($f, $controls)) {
                    $out[$f] = $controls[$f];
                    continue;
                }

                $out[$f] = null;
            }
        }

        // Return a compact, nested result when caller used the path-style target
        // @phpstan-ignore function.alreadyNarrowedType
        if (is_string($original_target) && strpos($original_target, '/') !== false) {
            if ($folded_kind === 'widget') {
                return $this->format_output(['widgets' => [$type_name => $out]]);
            }

            return $this->format_output(['elements' => [$type_name => $out]]);
        }

        // Backwards-compatible flat response
        $result = [
            'kind' => $folded_kind,
            'type' => $type_name,
            'fields' => $out,
        ];

        return $this->format_output($result);
    }


    /**
     * Recursively scan Elementor data structure to collect widget types and element types.
     * If $targets is provided, will stop early when all targets have been found.
     *
     * @param array<string,string> $node
     * @param array<string, array<int|string, string>> $found
     * @param-out array<string, array<int|string, string>> $found
     * @param array<string, array<int|string, string>>|null $targets Optional list of targets to look for, keys: 'widget' and/or 'elType'
     * @return bool True if search completed early because targets were satisfied, false otherwise
     */
    function collect_elementor_types(array $node, array &$found, ?array $targets = null): bool
    {
        // If node is associative array with widgetType or elType
        if (isset($node['widgetType'])) {
            $found['widget'][] = $node['widgetType'];
        }

        if (isset($node['elType'])) {
            $found['elType'][] = $node['elType'];
        }

        // If targets specified, check whether we've satisfied them and can stop
        if (is_array($targets) && !empty($targets)) {
            $satisfied = true;
            foreach ($targets as $cat => $names) {
                // @phpstan-ignore function.alreadyNarrowedType
                if (!is_array($names) || empty($names)) {
                    continue;
                }

                $intersect = array_intersect($found[$cat] ?? [], $names);
                if (empty($intersect)) {
                    $satisfied = false;
                    break;
                }
            }

            if ($satisfied) {
                return true;
            }
        }

        // Walk children
        foreach ($node as $key => $value) {
            // @phpstan-ignore function.impossibleType
            if (is_array($value)) {
                // Numeric-indexed arrays are often lists of elements
                $done = $this->collect_elementor_types($value, $found, $targets);
                if ($done) {
                    return true;
                }
            }
        }

        return false;
    }







    /**
     * Extract controls from an element/widget
     */
    /**
     * @param \Elementor\Element_Base|\Elementor\Element_Base[] $element
     * @return array<string,mixed>
     */
    private function extract_controls_from_element($element)
    {
        if (!$element) {
            return [];
        }

        $temp_element = null;

        // If the element is already an instantiated object, try to clone it to avoid side-effects
        if (is_object($element)) {
            $temp_element = $element;
            // @phpstan-ignore function.impossibleType
        } elseif (is_string($element)) {
            // Try multiple constructor signatures to be defensive
            $id = 'temp_' . uniqid();
            try {
                // Some Elementor block classes expect constructor args; try passing array first
                $temp_element = new $element([
                    'id' => $id,
                    'settings' => []
                ]);
            } catch (\ArgumentCountError $e) {
                // Try no-arg constructor
                try {
                    $temp_element = new $element();
                } catch (\Throwable $e2) {
                    error_log('Miruni: Failed to instantiate element class ' . $element . ' - ' . $e2->getMessage());
                    return [];
                }
            } catch (\Throwable $e) {
                // Last resort: try no-arg constructor
                try {
                    $temp_element = new $element();
                } catch (\Throwable $e2) {
                    error_log('Miruni: Failed to instantiate element class ' . $element . ' - ' . $e2->getMessage());
                    return [];
                }
            }
        } else {
            // Unknown type
            return [];
        }



        try {
            // Ensure Elementor is in edit mode so group controls (like typography) register their fields
            try {
                /** @phpstan-ignore-next-line */
                \Elementor\Plugin::$instance->editor->set_edit_mode(true);
                // Touch common group control classes to ensure they are autoloaded
                if (!class_exists('\\Elementor\\Group_Control_Typography')) {
                    @class_exists('\\Elementor\\Group_Control_Typography');
                }
            } catch (\Throwable $e) {
                error_log("Miruni: Failed to set Elementor edit mode - " . $e->getMessage());
                // Non-fatal
            }

            $controls = $temp_element->get_controls();
        } catch (\Throwable $e) {
            $name = $element->get_name();
            error_log('Miruni: Error calling get_controls on element ' . $name . ' - ' . $e->getMessage());
            return [];
        }

        $schema = [];

        foreach ($controls as $control_name => $control_config) {
            $schema[$control_name] = $this->parse_control_schema($control_config);
        }

        return $schema;
    }

    /**
     * Parse individual control schema
     *
     * @param array<string,mixed> $control
     * @return array<string,mixed>
     */
    private function parse_control_schema(array $control): array
    {
        $schema = [
            'type' => $control['type'] ?? 'unknown',
            'label' => $control['label'] ?? '',
            'description' => $control['description'] ?? '',
            'default' => $control['default'] ?? null,
            'required' => !empty($control['required']),
            'conditions' => $control['condition'] ?? [],
        ];

        // Add type-specific properties
        switch ($control['type']) {
            case \Elementor\Controls_Manager::SELECT:
            case \Elementor\Controls_Manager::CHOOSE:
                $schema['options'] = $control['options'] ?? [];
                $schema['behavioral_rules'] = $this->get_select_behavioral_rules($control);
                break;

            case \Elementor\Controls_Manager::SLIDER:
                $schema['range'] = $control['range'] ?? [];
                $schema['size_units'] = $control['size_units'] ?? ['px'];
                $schema['behavioral_rules'] = $this->get_slider_behavioral_rules($control);
                break;

            case \Elementor\Controls_Manager::DIMENSIONS:
                $schema['size_units'] = $control['size_units'] ?? ['px', '%', 'em'];
                $schema['allowed_dimensions'] = $control['allowed_dimensions'] ?? 'all';
                $schema['behavioral_rules'] = $this->get_dimensions_behavioral_rules($control);
                break;

            case \Elementor\Controls_Manager::COLOR:
                $schema['alpha'] = $control['alpha'] ?? true;
                $schema['behavioral_rules'] = $this->get_color_behavioral_rules($control);
                break;

            case \Elementor\Controls_Manager::MEDIA:
                $schema['media_types'] = $control['media_types'] ?? ['image', 'video'];
                $schema['structure'] = [
                    'id' => 'integer',
                    'url' => 'string',
                    'alt' => 'string',
                    'source' => 'library|external',
                    'size' => 'string'
                ];
                $schema['behavioral_rules'] = $this->get_media_behavioral_rules();
                break;

            case \Elementor\Controls_Manager::GALLERY:
                $schema['media_types'] = ['image'];
                $schema['structure'] = [
                    'type' => 'array',
                    'items' => [
                        'id' => 'integer',
                        'url' => 'string',
                        'alt' => 'string'
                    ]
                ];
                $schema['behavioral_rules'] = $this->get_gallery_behavioral_rules($control);
                break;
        }

        // Add group control behavioral rules
        if (isset($control['group_type'])) {
            $schema['behavioral_rules'] = array_merge(
                $schema['behavioral_rules'] ?? [],
                $this->get_group_control_behavioral_rules($control)
            );
        }

        return $schema;
    }

    /**
     * Get behavioral rules for media controls
     * @return array<mixed>
     */
    private function get_media_behavioral_rules(): array
    {
        return [
            'field_precedence' => [
                'description' => 'Order of field importance for determining final value',
                'order' => ['id', 'url', 'alt', 'source', 'size'],
                'rules' => [
                    'id_takes_precedence' => [
                        'description' => 'If id is provided and > 0, it takes precedence over url',
                        'condition' => 'id > 0',
                        'effect' => 'url is ignored, image loaded from media library using id'
                    ],
                    'url_fallback' => [
                        'description' => 'If id is 0 or empty, url is used for external images',
                        'condition' => 'id <= 0 || id === ""',
                        'effect' => 'source should be set to "external", url is used directly'
                    ],
                    'source_consistency' => [
                        'description' => 'source field must match the actual source',
                        'rules' => [
                            'library_source' => 'When id > 0, source should be "library"',
                            'external_source' => 'When using url without id, source should be "external"'
                        ]
                    ]
                ]
            ],
            'modification_guidelines' => [
                'change_to_library_image' => [
                    'description' => 'To change to a library image',
                    'required_fields' => ['id'],
                    'recommended_fields' => ['url', 'alt', 'size'],
                    'source_value' => 'library',
                    'example' => [
                        'id' => 123,
                        'url' => 'https://site.com/wp-content/uploads/image.jpg',
                        'alt' => 'Image description',
                        'source' => 'library',
                        'size' => 'full'
                    ]
                ],
                'change_to_external_image' => [
                    'description' => 'To change to an external image',
                    'required_fields' => ['url'],
                    'recommended_approach' => 'Set id to 0 or empty string',
                    'source_value' => 'external',
                    'example' => [
                        'id' => 0,
                        'url' => 'https://external-site.com/image.jpg',
                        'alt' => 'External image',
                        'source' => 'external',
                        'size' => ''
                    ]
                ],
                'clear_image' => [
                    'description' => 'To remove/clear the image',
                    'approach' => 'Set both id and url to empty',
                    'example' => [
                        'id' => '',
                        'url' => '',
                        'alt' => '',
                        'source' => 'library',
                        'size' => ''
                    ]
                ]
            ],
            'validation_rules' => [
                'id_validation' => 'Must be integer or empty string/0',
                'url_validation' => 'Must be valid URL or empty string',
                'source_validation' => 'Must be "library" or "external"',
                'size_validation' => 'WordPress image size name or empty string'
            ]
        ];
    }

    /**
     * @param mixed $control
     * @return array<string,mixed>
     */
    private function get_dimensions_behavioral_rules($control)
    {
        return [
            'field_precedence' => [
                'unit_inheritance' => [
                    'description' => 'Unit applies to all dimension values',
                    'effect' => 'Changing unit affects interpretation of all values'
                ],
                'linked_behavior' => [
                    'description' => 'isLinked controls whether all sides use same value',
                    'when_linked' => 'All sides should have same value',
                    'when_unlinked' => 'Each side can have different value'
                ]
            ],
            'modification_guidelines' => [
                'change_all_sides' => [
                    'description' => 'To change all sides equally',
                    'set_isLinked' => '1',
                    'set_all_values' => 'same_value'
                ],
                'change_individual_sides' => [
                    'description' => 'To change sides individually',
                    'set_isLinked' => '',
                    'set_values' => 'independently'
                ],
                'change_unit' => [
                    'description' => 'When changing unit, values may need adjustment',
                    'consideration' => 'Convert existing pixel values to new unit if needed'
                ]
            ]
        ];
    }




    /**
     * Get behavioral rules for select/choose controls
     * @param array<string,mixed> $control
     * @return array<string,mixed>
     */
    private function get_select_behavioral_rules($control)
    {
        $options = $control['options'] ?? [];

        return [
            'value_validation' => [
                'description' => 'Value must match one of the defined options',
                'valid_options' => array_keys($options),
                'fallback_behavior' => 'Falls back to default if invalid value provided'
            ],
            'option_dependencies' => [
                'description' => 'Some options may enable/disable other controls',
                'check_conditions' => 'Other controls may have conditions based on this value'
            ]
        ];
    }

    /**
     * Get behavioral rules for slider controls
     * @param array<string,mixed> $control
     * @return array<string,mixed>
     */
    private function get_slider_behavioral_rules($control)
    {
        return [
            'value_structure' => [
                'description' => 'Slider values are objects with size and unit',
                'required_format' => ['size' => 'number', 'unit' => 'string'],
                'range_enforcement' => 'Size value is constrained by range settings'
            ],
            'unit_behavior' => [
                'description' => 'Unit determines how size value is interpreted',
                'available_units' => $control['size_units'] ?? ['px']
            ]
        ];
    }

    /**
     * Get behavioral rules for color controls
     * @param array<string,mixed> $control
     * @return array<string,mixed>
     */
    private function get_color_behavioral_rules($control)
    {
        return [
            'format_support' => [
                'hex' => 'Supports #RRGGBB and #RRGGBBAA formats',
                'rgb' => 'Supports rgb() and rgba() functions',
                'transparency' => $control['alpha'] ?? true ? 'Alpha/transparency supported' : 'No transparency support'
            ],
            'value_precedence' => [
                'description' => 'Any valid CSS color value accepted',
                'recommendation' => 'Use hex format for consistency'
            ]
        ];
    }

    /**
     * Get behavioral rules for gallery controls
     * @param array<string,mixed> $control
     * @return array<string,mixed>
     */
    private function get_gallery_behavioral_rules($control): array
    {
        return [
            'array_behavior' => [
                'description' => 'Gallery is array of media objects',
                'each_item' => 'Follows same rules as single media control',
                'order_significance' => 'Array order determines display order'
            ],
            'modification_approach' => [
                'add_images' => 'Append to existing array',
                'remove_images' => 'Filter out by id or url',
                'reorder_images' => 'Rearrange array elements'
            ]
        ];
    }

    /**
     * Get behavioral rules for group controls (background, border, etc.)
     * @param array<string,mixed> $control
     * @return array<string,mixed>
     */
    private function get_group_control_behavioral_rules($control)
    {
        $group_type = $control['group_type'] ?? '';

        switch ($group_type) {
            case 'background':
                return [
                    'type_dependency' => [
                        'description' => 'Background type determines which fields are active',
                        'classic' => ['color', 'image', 'position', 'repeat', 'size'],
                        'gradient' => ['color', 'color_b', 'gradient_type', 'gradient_angle'],
                        'video' => ['video_link', 'video_start', 'video_end'],
                        'slideshow' => ['slideshow_gallery', 'slideshow_transition']
                    ],
                    'field_relevance' => [
                        'description' => 'Only fields relevant to selected type are processed',
                        'ignored_fields' => 'Fields for other background types are ignored'
                    ]
                ];

            case 'border':
                return [
                    'border_dependency' => [
                        'description' => 'Border width, style, and color work together',
                        'requirement' => 'Style must be set for border to appear',
                        'width_behavior' => 'Width can be set per side or linked'
                    ]
                ];

            default:
                return [];
        }
    }

    /**
     * Format output based on specified format
     * @param array<string,mixed> $data
     */
    private function format_output($data): string
    {
        $output = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
        if ($output === false) {
            return '';
        }
        return $output;
    }

}


