<?php

if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

trait Niwa_AI_Tools_Content_Ops {

    private function validate_replace_text_in_post_action_args( $args ) {
        $post_check = $this->validate_required_positive_id( isset( $args['post_id'] ) ? $args['post_id'] : 0, 'Ciljni post nedostaje za text replace.' );
        if ( empty( $post_check['success'] ) ) {
            return $post_check;
        }
		$managed_guard = $this->guard_managed_blog_generic_mutation(
			intval( isset( $args['post_id'] ) ? $args['post_id'] : 0 ),
			isset( $args['field'] ) ? (string) $args['field'] : 'post_content'
		);
		if ( empty( $managed_guard['success'] ) ) {
			return $managed_guard;
		}

        if ( empty( $args['field'] ) || empty( $args['find_text'] ) ) {
            return array(
                'success' => false,
                'error'   => 'Text replace u postu nema dovoljno podataka.',
            );
        }

        return array(
            'success' => true,
        );
    }


    private function validate_swap_page_image_args( $args ) {
        $post_check = $this->validate_required_positive_id(
            isset( $args['post_id'] ) ? $args['post_id'] : 0,
            'Ciljni post nedostaje za zamenu slike.'
        );
        if ( empty( $post_check['success'] ) ) {
            return $post_check;
        }
		$managed_guard = $this->guard_managed_blog_generic_mutation(
			intval( isset( $args['post_id'] ) ? $args['post_id'] : 0 ),
			'post_content'
		);
		if ( empty( $managed_guard['success'] ) ) {
			return $managed_guard;
		}
        if ( empty( $args['find_image'] ) || empty( $args['new_image'] ) ) {
            return array( 'success' => false, 'error' => 'find_image i new_image su obavezni za zamenu slike.' );
        }
        return array( 'success' => true );
    }

    private function preview_trash_recent_posts( $args ) {
        $post_type = $this->normalize_post_type( isset( $args['post_type'] ) ? $args['post_type'] : 'post' );
        $count     = max( 1, min( self::MAX_TRASH_POSTS, intval( isset( $args['count'] ) ? $args['count'] : 1 ) ) );
        $posts     = get_posts(
            array(
                'post_type'      => $post_type,
                'post_status'    => array( 'publish', 'future', 'draft', 'private' ),
                'posts_per_page' => $count,
                'orderby'        => 'date',
                'order'          => 'DESC',
            )
        );

        return $this->build_trash_posts_action( 'trash_recent_posts', $post_type, $posts );
    }

    private function preview_trash_posts_by_query( $args ) {
        $post_type = $this->normalize_post_type( isset( $args['post_type'] ) ? $args['post_type'] : 'post' );
        $query     = trim( (string) ( isset( $args['search_query'] ) ? $args['search_query'] : '' ) );
        $count     = max( 1, min( self::MAX_TRASH_POSTS, intval( isset( $args['count'] ) ? $args['count'] : 5 ) ) );

        if ( '' === $query ) {
            return array(
                'success' => false,
                'error'   => 'Nedostaje search_query za trash_posts_by_query.',
            );
        }

        $posts = get_posts(
            array(
                'post_type'      => $post_type,
                'post_status'    => array( 'publish', 'future', 'draft', 'private' ),
                'posts_per_page' => $count,
                's'              => $query,
                'orderby'        => 'date',
                'order'          => 'DESC',
            )
        );

        return $this->build_trash_posts_action( 'trash_posts_by_query', $post_type, $posts, array( 'search_query' => $query ) );
    }

    private function build_trash_posts_action( $type, $post_type, $posts, $args = array() ) {
        if ( empty( $posts ) ) {
            return array(
                'success' => false,
                'error'   => 'Nema pogodjenih stavki za prebacivanje u trash.',
            );
        }

        $targets = array();
        foreach ( $posts as $post ) {
            $targets[] = array(
                'post_id'         => intval( $post->ID ),
                'post_title'      => get_the_title( $post ),
                'previous_status' => (string) $post->post_status,
            );
        }

        return array(
            'success' => true,
            'action'  => array(
                'type'             => $type,
                'label'            => 'Prebaci u trash ' . count( $targets ) . ' ' . $post_type . ' stavki',
                'summary'          => 'Trash ' . count( $targets ) . ' ' . $post_type . ' stavki.',
                'rollback_plan'    => 'Vraca prethodni post_status za svaku trash-ovanu stavku.',
                'rollback_support' => 'full',
                'targets'          => $targets,
                'args'             => array_merge( array( 'post_type' => $post_type ), $args ),
            ),
        );
    }

    private function preview_update_front_page_title( $args ) {
        $page_id   = intval( get_option( 'page_on_front' ) );
        $new_title = trim( (string) ( isset( $args['new_title'] ) ? $args['new_title'] : '' ) );

        if ( $page_id <= 0 ) {
            return array(
                'success' => false,
                'error'   => 'WordPress front page nije podesena kao staticka stranica.',
            );
        }

        if ( '' === $new_title ) {
            return array(
                'success' => false,
                'error'   => 'new_title je obavezan za update_front_page_title.',
            );
        }

        $post = get_post( $page_id );
        if ( ! $post ) {
            return array(
                'success' => false,
                'error'   => 'Front page post ne postoji.',
            );
        }

        $visible_candidates = $this->find_front_page_visible_headline_candidates( intval( $post->ID ) );

        return array(
            'success' => true,
            'action'  => array(
                'type'             => 'update_front_page_title',
                'label'            => 'Promeni front page title i vidljivi headline',
                'summary'          => 'Front page title "' . $post->post_title . '" -> "' . $new_title . '". Vidljivi headline kandidati: ' . count( $visible_candidates ) . '.',
                'rollback_plan'    => 'Vraca prethodni front page title i izmenjeni vidljivi headline storage ako je pronadjen.',
                'rollback_support' => 'full',
                'targets'          => array(
                    array(
                        'post_id'    => intval( $post->ID ),
                        'post_title' => (string) $post->post_title,
                    ),
                ),
                'args'             => array(
                    'post_id'                    => intval( $post->ID ),
                    'new_title'                  => $new_title,
                    'visible_headline_candidates' => $visible_candidates,
                ),
            ),
        );
    }

    private function preview_replace_text_in_post( $args ) {
        $target = $this->resolve_post_target( $args );
        if ( empty( $target['success'] ) ) {
            return $target;
        }

        $field        = isset( $args['field'] ) ? (string) $args['field'] : 'post_content';
        $find_text    = (string) ( isset( $args['find_text'] ) ? $args['find_text'] : '' );
        $replace_text = (string) ( isset( $args['replace_text'] ) ? $args['replace_text'] : '' );
		$managed_guard = $this->guard_managed_blog_generic_mutation( $target['post_id'], $field );
		if ( empty( $managed_guard['success'] ) ) {
			return $managed_guard;
		}

        if ( '' === $find_text ) {
            return array(
                'success' => false,
                'error'   => 'find_text je obavezan za replace_text_in_post.',
            );
        }

        $current = $this->read_post_field_value( $target['post_id'], $field );

        // Validate that find_text exists in the target field.
        if ( 'elementor_data' === $field ) {
            // Elementor-specific parsed tree approach.
            $dry_run = $this->replace_text_in_elementor_data( $current, $find_text, $replace_text );
            if ( empty( $dry_run['success'] ) ) {
                return array(
                    'success' => false,
                    'error'   => isset( $dry_run['error'] ) ? (string) $dry_run['error'] : 'Elementor zamena teksta nije moguca.',
                );
            }
        } elseif ( 0 === strpos( $field, 'meta:' ) ) {
            // Generic meta: path — detect JSON, serialized PHP, or plain text.
            $json_check = json_decode( $current, true );
            if ( is_array( $json_check ) && JSON_ERROR_NONE === json_last_error() ) {
                $dry_run = $this->replace_text_in_json_data( $current, $find_text, $replace_text );
                if ( empty( $dry_run['success'] ) ) {
                    return array(
                        'success' => false,
                        'error'   => isset( $dry_run['error'] ) ? (string) $dry_run['error'] : 'find_text nije pronadjen u JSON meta vrednosti.',
                    );
                }
            } elseif ( is_serialized( $current ) ) {
                $dry_run = $this->replace_text_in_serialized_php( $current, $find_text, $replace_text );
                if ( empty( $dry_run['success'] ) ) {
                    return array(
                        'success' => false,
                        'error'   => isset( $dry_run['error'] ) ? (string) $dry_run['error'] : 'find_text nije pronadjen u serijalizovanoj meta vrednosti.',
                    );
                }
            } else {
                if ( false === strpos( $current, $find_text ) ) {
                    return array( 'success' => false, 'error' => 'find_text nije pronadjen u meta vrednosti.' );
                }
            }
        } else {
            // Standard fields: simple strpos check.
            if ( false === strpos( $current, $find_text ) ) {
                return array(
                    'success' => false,
                    'error'   => 'find_text nije pronadjen u ciljnom polju posta.',
                );
            }
        }

        return array(
            'success' => true,
            'action'  => array(
                'type'             => 'replace_text_in_post',
                'label'            => 'Replace text u post polju',
                'summary'          => 'Replace text u "' . $target['post_title'] . '" polju ' . $field . '.',
                'rollback_plan'    => 'Vraca prethodnu vrednost izmenjenog post polja.',
                'rollback_support' => 'full',
                'targets'          => array(
                    array(
                        'post_id'    => $target['post_id'],
                        'post_title' => $target['post_title'],
                    ),
                ),
                'args'             => array(
                    'post_id'      => $target['post_id'],
                    'field'        => $field,
                    'find_text'    => $find_text,
                    'replace_text' => $replace_text,
                ),
            ),
        );
    }


    private function preview_update_wordpress_core( $args ) {
        require_once ABSPATH . 'wp-admin/includes/update.php';
        wp_version_check();

        $target_version = trim( (string) ( isset( $args['target_version'] ) ? $args['target_version'] : '' ) );
        $updates        = get_core_updates( array( 'dismissed' => false ) );
        if ( empty( $updates ) || ! is_array( $updates ) ) {
            return array(
                'success' => false,
                'error'   => 'Nema dostupne WordPress core nadogradnje.',
            );
        }

        $selected = null;
        foreach ( $updates as $update ) {
            if ( empty( $update->response ) || 'upgrade' !== $update->response ) {
                continue;
            }
            if ( '' !== $target_version && isset( $update->version ) && $target_version !== (string) $update->version ) {
                continue;
            }
            $selected = $update;
            break;
        }

        if ( empty( $selected ) ) {
            return array(
                'success' => false,
                'error'   => '' !== $target_version ? 'Nije dostupna trazena WordPress verzija za upgrade.' : 'Nema aktivne WordPress core nadogradnje.',
            );
        }

        global $wp_version;

        return array(
            'success' => true,
            'action'  => array(
                'type'             => 'update_wordpress_core',
                'label'            => 'Azuriraj WordPress core',
                'summary'          => 'WordPress ' . (string) $wp_version . ' -> ' . (string) $selected->version,
                'rollback_plan'    => 'Rollback nije automatski podrzan za core update.',
                'rollback_support' => 'none',
                'targets'          => array(
                    array(
                        'current_version' => (string) $wp_version,
                        'target_version'  => (string) $selected->version,
                    ),
                ),
                'args'             => array(
                    'target_version' => (string) $selected->version,
                ),
            ),
        );
    }

    private function validate_update_wordpress_plugins_action_args( $args ) {
        $plugin_files = isset( $args['plugin_files'] ) && is_array( $args['plugin_files'] ) ? array_values( array_filter( array_map( 'strval', $args['plugin_files'] ) ) ) : array();
        if ( empty( $plugin_files ) ) {
            return array(
                'success' => false,
                'error'   => 'Plugin update nema nijedan plugin file za obradu.',
            );
        }

        return array( 'success' => true );
    }

    private function validate_manage_wordpress_plugin_activation_action_args( $args ) {
        $action      = isset( $args['action'] ) ? sanitize_key( (string) $args['action'] ) : '';
        $plugin_file = isset( $args['plugin_file'] ) ? trim( (string) $args['plugin_file'] ) : '';

        if ( ! in_array( $action, array( 'activate', 'deactivate' ), true ) ) {
            return array(
                'success' => false,
                'error'   => 'Plugin activation akcija mora biti activate ili deactivate.',
            );
        }

        if ( '' === $plugin_file ) {
            return array(
                'success' => false,
                'error'   => 'Plugin activation akcija nema plugin_file.',
            );
        }

        return array( 'success' => true );
    }

    private function preview_update_wordpress_plugins( $args ) {
        $resolved = $this->resolve_wordpress_plugin_update_targets( $args );
        if ( empty( $resolved['success'] ) ) {
            return $resolved;
        }

        $updates = isset( $resolved['updates'] ) && is_array( $resolved['updates'] ) ? $resolved['updates'] : array();
        if ( empty( $updates ) ) {
            return array(
                'success' => true,
                'summary' => 'Nema dostupnih WordPress plugin nadogradnji za trazeni scope.',
                'updates' => array(),
            );
        }

        $plugin_files = array();
        $labels       = array();
        foreach ( $updates as $update ) {
            if ( ! is_array( $update ) || empty( $update['plugin_file'] ) ) {
                continue;
            }
            $plugin_files[] = (string) $update['plugin_file'];
            $labels[]       = ( isset( $update['name'] ) ? (string) $update['name'] : (string) $update['plugin_file'] ) . ' ' . ( isset( $update['current_version'] ) ? (string) $update['current_version'] : '' ) . ' -> ' . ( isset( $update['new_version'] ) ? (string) $update['new_version'] : '' );
        }

        return array(
            'success' => true,
            'action'  => array(
                'type'             => 'update_wordpress_plugins',
                'label'            => 'Azuriraj WordPress pluginove',
                'summary'          => 'Azuriraj ' . count( $plugin_files ) . ' pluginova: ' . implode( '; ', array_slice( $labels, 0, 8 ) ),
                'rollback_plan'    => 'Pre update-a se pravi fajl backup izabranih pluginova. Automatski rollback kroz admin action nije ukljucen.',
                'rollback_support' => 'manual_backup',
                'targets'          => array_slice( $updates, 0, 20 ),
                'args'             => array(
                    'plugin_files'  => array_values( array_unique( $plugin_files ) ),
                    'create_backup' => ! array_key_exists( 'create_backup', (array) $args ) || ! empty( $args['create_backup'] ),
                    'scope'         => isset( $resolved['scope'] ) ? (string) $resolved['scope'] : 'all',
                ),
            ),
        );
    }

    private function preview_manage_wordpress_plugin_activation( $args ) {
        require_once ABSPATH . 'wp-admin/includes/plugin.php';

        $args         = is_array( $args ) ? $args : array();
        $action       = isset( $args['action'] ) ? sanitize_key( (string) $args['action'] ) : '';
        $plugin_query = isset( $args['plugin'] ) ? trim( (string) $args['plugin'] ) : '';
        $network_wide = ! empty( $args['network_wide'] );

        if ( ! in_array( $action, array( 'activate', 'deactivate' ), true ) ) {
            return array(
                'success' => false,
                'error'   => 'action mora biti activate ili deactivate.',
            );
        }

        if ( '' === $plugin_query ) {
            return array(
                'success' => false,
                'error'   => 'Nedostaje plugin slug, plugin file ili naziv plugina.',
            );
        }

        $resolved = $this->resolve_wordpress_plugin_activation_target( $plugin_query );
        if ( empty( $resolved['success'] ) ) {
            return $resolved;
        }

        $plugin_file = (string) $resolved['plugin_file'];
        $plugin_name = (string) $resolved['name'];
        $is_active   = function_exists( 'is_plugin_active' ) ? is_plugin_active( $plugin_file ) : in_array( $plugin_file, (array) get_option( 'active_plugins', array() ), true );
        $is_network  = function_exists( 'is_plugin_active_for_network' ) ? is_plugin_active_for_network( $plugin_file ) : false;
        $effective_active = $network_wide ? $is_network : ( $is_active || $is_network );
        $desired_active   = 'activate' === $action;

        if ( $effective_active === $desired_active ) {
            return array(
                'success'         => true,
                'summary'         => $plugin_name . ' je vec ' . ( $desired_active ? 'aktivan' : 'neaktivan' ) . '.',
                'plugin_file'     => $plugin_file,
                'plugin_name'     => $plugin_name,
                'previous_status' => $effective_active ? 'active' : 'inactive',
                'new_status'      => $effective_active ? 'active' : 'inactive',
                'network_active'  => $is_network,
            );
        }

        return array(
            'success' => true,
            'action'  => array(
                'type'             => 'manage_wordpress_plugin_activation',
                'label'            => ( 'activate' === $action ? 'Aktiviraj plugin' : 'Deaktiviraj plugin' ),
                'summary'          => ( 'activate' === $action ? 'Aktiviraj ' : 'Deaktiviraj ' ) . $plugin_name . ' (' . $plugin_file . ')',
                'rollback_plan'    => 'Moze se vratiti suprotnom aktivacionom akcijom za isti plugin.',
                'rollback_support' => 'manual_action',
                'targets'          => array(
                    array(
                        'plugin_file'    => $plugin_file,
                        'plugin_name'    => $plugin_name,
                        'previous_status' => $effective_active ? 'active' : 'inactive',
                        'network_active' => $is_network,
                    ),
                ),
                'args'             => array(
                    'action'                  => $action,
                    'plugin_file'             => $plugin_file,
                    'plugin'                  => $plugin_query,
                    'network_wide'            => $network_wide,
                    'previous_active'         => $is_active,
                    'previous_network_active' => $is_network,
                ),
            ),
        );
    }

    private function preview_inventory_cleanup( $args ) {
        if ( ! class_exists( 'WooCommerce' ) ) {
            return array(
                'success' => false,
                'error'   => 'WooCommerce nije aktivan.',
            );
        }

        $limit = max( 1, min( self::MAX_INVENTORY_LIMIT, intval( isset( $args['limit'] ) ? $args['limit'] : 50 ) ) );
        $query = new WP_Query(
            array(
                'post_type'      => 'product',
                'post_status'    => array( 'publish', 'private', 'pending', 'future' ),
                'posts_per_page' => $limit,
                'fields'         => 'ids',
                'orderby'        => 'ID',
                'order'          => 'ASC',
                'no_found_rows'  => false,
                'meta_query'     => array(
                    array(
                        'key'     => '_stock_status',
                        'value'   => 'outofstock',
                        'compare' => '=',
                    ),
                ),
            )
        );

        $ids = array_map( 'intval', (array) $query->posts );
        if ( empty( $ids ) ) {
            return array(
                'success' => false,
                'error'   => 'Nema out-of-stock proizvoda za cleanup.',
            );
        }

        $targets = array();
        foreach ( $ids as $product_id ) {
            if ( ! $this->is_product_effectively_outofstock( $product_id ) ) {
                continue;
            }
            $targets[] = array(
                'product_id' => $product_id,
                'title'      => get_the_title( $product_id ),
            );
        }

        if ( empty( $targets ) ) {
            return array(
                'success' => false,
                'error'   => 'Nema stvarno out-of-stock proizvoda za cleanup; preskoceni su stale variable parent zapisi.',
            );
        }

        return array(
            'success' => true,
            'action'  => array(
                'type'             => 'inventory_cleanup',
                'label'            => 'Inventory cleanup',
                'summary'          => 'Draft ' . count( $targets ) . ' out-of-stock proizvoda i _za_noindex=1.',
                'rollback_plan'    => 'Vraca prethodni post_status i _za_noindex.',
                'rollback_support' => 'full',
                'targets'          => $targets,
                'args'             => array(
                    'product_ids' => array_map(
                        function( $target ) {
                            return intval( $target['product_id'] );
                        },
                        $targets
                    ),
                ),
            ),
        );
    }

    private function preview_swap_page_image( $args ) {
        $target = $this->resolve_post_target( $args );
        if ( empty( $target['success'] ) ) {
            return $target;
        }

        $find_image  = trim( (string) ( isset( $args['find_image'] ) ? $args['find_image'] : '' ) );
        $new_image   = trim( (string) ( isset( $args['new_image'] ) ? $args['new_image'] : '' ) );

        if ( '' === $find_image || '' === $new_image ) {
            return array( 'success' => false, 'error' => 'find_image i new_image su obavezni.' );
        }

        $resolved = $this->resolve_swap_image_params( $find_image, $new_image );
        if ( empty( $resolved['success'] ) ) {
            return $resolved;
        }

        $post_id  = $target['post_id'];
        $find_url = $resolved['find_url'];
        $patterns = $this->build_image_find_patterns( $find_url );

        // Check if the find pattern exists anywhere in the post.
        $found_in = array();
        $post     = get_post( $post_id );

        foreach ( $patterns as $pattern ) {
            if ( false !== strpos( (string) $post->post_content, $pattern ) ) {
                $found_in[] = 'post_content';
                break;
            }
        }

        $all_meta = get_post_meta( $post_id );
        if ( is_array( $all_meta ) ) {
            foreach ( $all_meta as $meta_key => $meta_values ) {
                if ( 0 === strpos( $meta_key, '_niwa_' ) || 0 === strpos( $meta_key, '_transient_' ) ) {
                    continue;
                }
                foreach ( $meta_values as $raw_val ) {
                    $searchable = is_string( $raw_val ) ? $raw_val : '';
                    foreach ( $patterns as $pattern ) {
                        if ( false !== strpos( $searchable, $pattern ) ) {
                            $found_in[] = 'meta:' . $meta_key;
                            break 2;
                        }
                    }
                }
            }
        }

        if ( empty( $found_in ) ) {
            return array(
                'success' => false,
                'error'   => 'Slika "' . $find_url . '" nije pronadjena ni u post_content ni u post meta vrednostima ovog posta.',
            );
        }

        return array(
            'success' => true,
            'action'  => array(
                'type'             => 'swap_page_image',
                'label'            => 'Zameni sliku na stranici',
                'summary'          => 'Zameni sliku u "' . $target['post_title'] . '": pronadjeno u ' . implode( ', ', $found_in ) . '.',
                'rollback_plan'    => 'Vraca prethodne URL-ove slika u svim izmenjenim poljima.',
                'rollback_support' => 'full',
                'targets'          => array(
                    array(
                        'post_id'    => $post_id,
                        'post_title' => $target['post_title'],
                    ),
                ),
                'args'             => array(
                    'post_id'    => $post_id,
                    'find_image' => $find_image,
                    'new_image'  => $new_image,
                    'found_in'   => $found_in,
                ),
            ),
        );
    }

    private function is_product_effectively_outofstock( $product_id ) {
        $product_id = intval( $product_id );
        if ( $product_id <= 0 ) {
            return true;
        }

        if ( function_exists( 'wc_get_product' ) ) {
            $product = wc_get_product( $product_id );
            if ( $product && method_exists( $product, 'is_type' ) && $product->is_type( 'variable' ) && method_exists( $product, 'get_children' ) ) {
                foreach ( (array) $product->get_children() as $variation_id ) {
                    $variation = wc_get_product( intval( $variation_id ) );
                    if ( ! $variation || ! method_exists( $variation, 'is_type' ) || ! $variation->is_type( 'variation' ) ) {
                        continue;
                    }
                    if ( method_exists( $variation, 'get_status' ) && ! in_array( (string) $variation->get_status(), array( 'publish', 'private' ), true ) ) {
                        continue;
                    }
                    if ( method_exists( $variation, 'is_in_stock' ) && $variation->is_in_stock() ) {
                        return false;
                    }
                    if ( method_exists( $variation, 'get_stock_status' ) && 'outofstock' !== (string) $variation->get_stock_status() ) {
                        return false;
                    }
                }

                return true;
            }

            if ( $product && method_exists( $product, 'get_stock_status' ) ) {
                return 'outofstock' === (string) $product->get_stock_status();
            }
        }

        return 'outofstock' === (string) get_post_meta( $product_id, '_stock_status', true );
    }

    private function execute_trash_posts_action( $action ) {
        $executed = array();
        $rollback = array();

        foreach ( (array) $action['targets'] as $target ) {
            $post_id = isset( $target['post_id'] ) ? intval( $target['post_id'] ) : 0;
            $post    = get_post( $post_id );
            if ( ! $post ) {
                $this->rollback_entries( $rollback );
                return array( 'success' => false, 'error' => 'Post vise ne postoji: #' . $post_id );
            }

            $trashed = wp_trash_post( $post_id );
            if ( false === $trashed ) {
                $this->rollback_entries( $rollback );
                return array( 'success' => false, 'error' => 'Trash nije uspeo za post #' . $post_id );
            }

            $rollback[] = array(
                'type'            => 'post_status_restore',
                'post_id'         => $post_id,
                'previous_status' => (string) $post->post_status,
            );
            $executed[] = array(
                'type'    => $action['type'],
                'post_id' => $post_id,
                'title'   => get_the_title( $post_id ),
            );
        }

        return array(
            'success'          => true,
            'summary'          => 'Trashovano ' . count( $executed ) . ' stavki.',
            'executed_actions' => $executed,
            'rollback_entries' => $rollback,
        );
    }

    private function execute_update_front_page_title_action( $action ) {
        $post_id   = intval( isset( $action['args']['post_id'] ) ? $action['args']['post_id'] : 0 );
        $new_title = (string) ( isset( $action['args']['new_title'] ) ? $action['args']['new_title'] : '' );
        $post      = get_post( $post_id );

        if ( ! $post ) {
            return array( 'success' => false, 'error' => 'Front page post vise ne postoji.' );
        }

        $rollback_entries = array(
            array(
                'type'           => 'post_field_restore',
                'post_id'        => $post_id,
                'field'          => 'post_title',
                'previous_value' => (string) $post->post_title,
            ),
        );

        $updated = wp_update_post(
            array(
                'ID'         => $post_id,
                'post_title' => $new_title,
            ),
            true
        );

        if ( is_wp_error( $updated ) ) {
            return array( 'success' => false, 'error' => $updated->get_error_message() );
        }

        $visible = $this->update_front_page_visible_headline_storage( $post_id, $new_title );
        if ( empty( $visible['success'] ) ) {
            $this->rollback_entries( $rollback_entries );
            return array(
                'success' => false,
                'error'   => isset( $visible['error'] ) ? (string) $visible['error'] : 'Vidljivi front page headline nije mogao da se azurira.',
            );
        }

        $visible_rollback = isset( $visible['rollback_entries'] ) && is_array( $visible['rollback_entries'] ) ? $visible['rollback_entries'] : array();
        $rollback_entries = array_merge( $rollback_entries, $visible_rollback );

        $executed_actions = array(
            array(
                'type'    => 'update_front_page_title',
                'post_id' => $post_id,
                'field'   => 'post_title',
            ),
        );

        if ( ! empty( $visible['executed_actions'] ) && is_array( $visible['executed_actions'] ) ) {
            $executed_actions = array_merge( $executed_actions, $visible['executed_actions'] );
        }

        $visible_updated = ! empty( $visible['updated'] );
        $summary         = 'Front page title je azuriran.';
        if ( $visible_updated ) {
            $summary .= ' Vidljivi headline je azuriran u ' . count( (array) $visible['locations'] ) . ' storage lokaciji/lokacija.';
        } elseif ( ! empty( $visible['already_matching'] ) ) {
            $summary .= ' Vidljivi headline je vec imao trazenu vrednost.';
        } else {
            $summary .= ' Vidljivi headline kandidat nije pronadjen; proveri rendered output pre tvrdnje da je frontend promenjen.';
        }

        return array(
            'success'          => true,
            'summary'          => $summary,
            'visible_headline_updated' => $visible_updated,
            'visible_headline_locations' => isset( $visible['locations'] ) ? (array) $visible['locations'] : array(),
            'executed_actions' => $executed_actions,
            'rollback_entries' => $rollback_entries,
        );
    }

    private function find_front_page_visible_headline_candidates( $post_id ) {
        $candidates = array();

        $elementor_raw = (string) get_post_meta( $post_id, '_elementor_data', true );
        if ( '' !== $elementor_raw ) {
            $elementor_data = json_decode( $elementor_raw, true );
            if ( is_array( $elementor_data ) ) {
                $elementor_text = '';
                if ( $this->find_first_elementor_heading_text( $elementor_data, $elementor_text ) ) {
                    $candidates[] = array(
                        'field' => 'meta:_elementor_data',
                        'kind'  => 'elementor_heading_widget',
                        'text'  => $elementor_text,
                    );
                }
            }
        }

        $post = get_post( $post_id );
        if ( $post ) {
            $content_headline = $this->find_first_html_heading_text( (string) $post->post_content );
            if ( '' !== $content_headline ) {
                $candidates[] = array(
                    'field' => 'post_content',
                    'kind'  => 'html_heading',
                    'text'  => $content_headline,
                );
            }
        }

        return $candidates;
    }

    private function update_front_page_visible_headline_storage( $post_id, $new_title ) {
        $new_title        = sanitize_text_field( (string) $new_title );
        $rollback_entries = array();
        $executed_actions = array();
        $locations        = array();
        $already_matching = false;
        $preferred_old    = '';

        $elementor_raw = (string) get_post_meta( $post_id, '_elementor_data', true );
        if ( '' !== $elementor_raw ) {
            $elementor_data = json_decode( $elementor_raw, true );
            if ( is_array( $elementor_data ) ) {
                $previous = '';
                $changed  = $this->replace_first_elementor_heading_text( $elementor_data, $new_title, $previous );
                if ( $changed ) {
                    if ( $this->normalize_visible_heading_text( $previous ) === $this->normalize_visible_heading_text( $new_title ) ) {
                        $already_matching = true;
                    } else {
                        $preferred_old = $previous;
                        $new_json = wp_json_encode( $elementor_data );
                        if ( false === $new_json ) {
                            return array( 'success' => false, 'error' => 'Elementor headline JSON nije mogao da se enkoduje.' );
                        }

                        $written = $this->write_post_field_value( $post_id, 'meta:_elementor_data', $new_json );
                        if ( empty( $written['success'] ) ) {
                            return $written;
                        }

                        $locations[] = 'meta:_elementor_data';
                        $executed_actions[] = array(
                            'type'    => 'update_front_page_visible_headline',
                            'post_id' => $post_id,
                            'field'   => 'meta:_elementor_data',
                        );
                        $rollback_entries[] = array(
                            'type'           => 'post_field_restore',
                            'post_id'        => $post_id,
                            'field'          => 'meta:_elementor_data',
                            'previous_value' => $elementor_raw,
                        );
                    }
                }
            }
        }

        $post = get_post( $post_id );
        if ( $post ) {
            $content_previous = '';
            $new_content      = $this->replace_first_html_heading_text( (string) $post->post_content, $new_title, $content_previous, $preferred_old );
            if ( null !== $new_content ) {
                if ( $this->normalize_visible_heading_text( $content_previous ) === $this->normalize_visible_heading_text( $new_title ) ) {
                    $already_matching = true;
                } elseif ( $new_content !== (string) $post->post_content ) {
                    $written = $this->write_post_field_value( $post_id, 'post_content', $new_content );
                    if ( empty( $written['success'] ) ) {
                        return $written;
                    }

                    $locations[] = 'post_content';
                    $executed_actions[] = array(
                        'type'    => 'update_front_page_visible_headline',
                        'post_id' => $post_id,
                        'field'   => 'post_content',
                    );
                    $rollback_entries[] = array(
                        'type'           => 'post_field_restore',
                        'post_id'        => $post_id,
                        'field'          => 'post_content',
                        'previous_value' => (string) $post->post_content,
                    );
                }
            }
        }

        return array(
            'success'          => true,
            'updated'          => ! empty( $locations ),
            'already_matching' => $already_matching,
            'locations'        => $locations,
            'executed_actions' => $executed_actions,
            'rollback_entries' => $rollback_entries,
        );
    }

    private function find_first_elementor_heading_text( $elements, &$text, $depth = 0 ) {
        if ( ! is_array( $elements ) || $depth > 20 ) {
            return false;
        }

        foreach ( $elements as $element ) {
            if ( ! is_array( $element ) ) {
                continue;
            }

            if ( $this->elementor_element_is_heading_widget( $element ) && isset( $element['settings'] ) && is_array( $element['settings'] ) ) {
                $candidate = $this->read_elementor_heading_setting_text( $element['settings'] );
                if ( '' !== $this->normalize_visible_heading_text( $candidate ) ) {
                    $text = $candidate;
                    return true;
                }
            }

            if ( isset( $element['elements'] ) && $this->find_first_elementor_heading_text( $element['elements'], $text, $depth + 1 ) ) {
                return true;
            }
        }

        return false;
    }

    private function replace_first_elementor_heading_text( &$elements, $new_title, &$previous, $depth = 0 ) {
        if ( ! is_array( $elements ) || $depth > 20 ) {
            return false;
        }

        foreach ( $elements as &$element ) {
            if ( ! is_array( $element ) ) {
                continue;
            }

            if ( $this->elementor_element_is_heading_widget( $element ) && isset( $element['settings'] ) && is_array( $element['settings'] ) ) {
                if ( $this->write_elementor_heading_setting_text( $element['settings'], $new_title, $previous ) ) {
                    unset( $element );
                    return true;
                }
            }

            if ( isset( $element['elements'] ) && $this->replace_first_elementor_heading_text( $element['elements'], $new_title, $previous, $depth + 1 ) ) {
                unset( $element );
                return true;
            }
        }
        unset( $element );

        return false;
    }

    private function elementor_element_is_heading_widget( $element ) {
        return isset( $element['widgetType'] ) && 'heading' === (string) $element['widgetType'];
    }

    private function read_elementor_heading_setting_text( $settings ) {
        if ( isset( $settings['title'] ) && is_string( $settings['title'] ) ) {
            return (string) $settings['title'];
        }

        if ( isset( $settings['title'] ) && is_array( $settings['title'] )
             && isset( $settings['title']['$$type'], $settings['title']['value'] )
             && 'string' === $settings['title']['$$type']
             && is_string( $settings['title']['value'] )
        ) {
            return (string) $settings['title']['value'];
        }

        return '';
    }

    private function write_elementor_heading_setting_text( &$settings, $new_title, &$previous ) {
        if ( isset( $settings['title'] ) && is_string( $settings['title'] ) ) {
            if ( '' === $this->normalize_visible_heading_text( $settings['title'] ) ) {
                return false;
            }
            $previous          = (string) $settings['title'];
            $settings['title'] = (string) $new_title;
            return true;
        }

        if ( isset( $settings['title'] ) && is_array( $settings['title'] )
             && isset( $settings['title']['$$type'], $settings['title']['value'] )
             && 'string' === $settings['title']['$$type']
             && isset( $settings['title']['value'] )
             && is_string( $settings['title']['value'] )
        ) {
            if ( '' === $this->normalize_visible_heading_text( $settings['title']['value'] ) ) {
                return false;
            }
            $previous                   = (string) $settings['title']['value'];
            $settings['title']['value'] = (string) $new_title;
            return true;
        }

        return false;
    }

    private function find_first_html_heading_text( $content ) {
        if ( ! preg_match( '~<h([1-6])\b[^>]*>(.*?)</h\1>~is', (string) $content, $matches ) ) {
            return '';
        }

        return $this->normalize_visible_heading_text( $matches[2] );
    }

    private function replace_first_html_heading_text( $content, $new_title, &$previous, $preferred_previous = '' ) {
        $content = (string) $content;
        if ( '' === $content || ! preg_match_all( '~<h([1-6])\b([^>]*)>(.*?)</h\1>~is', $content, $matches, PREG_OFFSET_CAPTURE ) ) {
            return null;
        }

        $preferred = $this->normalize_visible_heading_text( $preferred_previous );
        $count     = count( $matches[0] );

        for ( $i = 0; $i < $count; $i++ ) {
            $inner = isset( $matches[3][ $i ][0] ) ? (string) $matches[3][ $i ][0] : '';
            $plain = $this->normalize_visible_heading_text( $inner );
            if ( '' === $plain ) {
                continue;
            }
            if ( '' !== $preferred && $plain !== $preferred ) {
                continue;
            }

            $level      = (string) $matches[1][ $i ][0];
            $attrs      = (string) $matches[2][ $i ][0];
            $full       = (string) $matches[0][ $i ][0];
            $offset     = intval( $matches[0][ $i ][1] );
            $previous   = $plain;
            $replacement = '<h' . $level . $attrs . '>' . esc_html( $new_title ) . '</h' . $level . '>';

            return substr( $content, 0, $offset ) . $replacement . substr( $content, $offset + strlen( $full ) );
        }

        return null;
    }

    private function normalize_visible_heading_text( $text ) {
        $decoded = html_entity_decode( (string) $text, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
        $plain   = wp_strip_all_tags( $decoded );
        return trim( preg_replace( '/\s+/u', ' ', $plain ) );
    }

    private function execute_replace_text_in_post_action( $action ) {
        $post_id      = intval( isset( $action['args']['post_id'] ) ? $action['args']['post_id'] : 0 );
        $field        = (string) ( isset( $action['args']['field'] ) ? $action['args']['field'] : 'post_content' );
        $find_text    = (string) ( isset( $action['args']['find_text'] ) ? $action['args']['find_text'] : '' );
        $replace_text = (string) ( isset( $action['args']['replace_text'] ) ? $action['args']['replace_text'] : '' );
		$managed_guard = $this->guard_managed_blog_generic_mutation( $post_id, $field );
		if ( empty( $managed_guard['success'] ) ) {
			return $managed_guard;
		}
        $current      = $this->read_post_field_value( $post_id, $field );

        // --- Elementor-safe path: parse, walk, replace, re-encode, validate ---
        if ( 'elementor_data' === $field ) {
            // Store independent backup before any modification.
            update_post_meta( $post_id, '_niwa_elementor_data_backup', wp_slash( $current ) );

            $result = $this->replace_text_in_elementor_data( $current, $find_text, $replace_text );
            if ( empty( $result['success'] ) ) {
                return $result;
            }

            $written = $this->write_post_field_value( $post_id, $field, $result['new_json'] );
            if ( empty( $written['success'] ) ) {
                return $written;
            }

            return array(
                'success'          => true,
                'summary'          => 'Elementor tekst je azuriran (' . intval( $result['match_count'] ) . ' widget(a) izmenjen(o)).',
                'executed_actions' => array(
                    array(
                        'type'    => 'replace_text_in_post',
                        'post_id' => $post_id,
                        'field'   => $field,
                    ),
                ),
                'rollback_entries' => array(
                    array(
                        'type'           => 'post_field_restore',
                        'post_id'        => $post_id,
                        'field'          => $field,
                        'previous_value' => $current,
                    ),
                ),
            );
        }

        // --- Generic meta: path — JSON, serialized PHP, or plain text ---
        if ( 0 === strpos( $field, 'meta:' ) ) {
            $meta_key = substr( $field, 5 );

            // Backup before modification.
            update_post_meta( $post_id, '_niwa_meta_backup_' . md5( $meta_key ), wp_slash( $current ) );

            $json_check = json_decode( $current, true );
            if ( is_array( $json_check ) && JSON_ERROR_NONE === json_last_error() ) {
                // JSON meta value — use generic JSON walker.
                $result = $this->replace_text_in_json_data( $current, $find_text, $replace_text );
                if ( empty( $result['success'] ) ) {
                    return $result;
                }
                $written = $this->write_post_field_value( $post_id, $field, $result['new_json'] );
                if ( empty( $written['success'] ) ) {
                    return $written;
                }
                return array(
                    'success'          => true,
                    'summary'          => 'Meta JSON tekst je azuriran (' . intval( $result['match_count'] ) . ' string vrednost(i) izmenjen(o)) u ' . $meta_key . '.',
                    'executed_actions' => array( array( 'type' => 'replace_text_in_post', 'post_id' => $post_id, 'field' => $field ) ),
                    'rollback_entries' => array( array( 'type' => 'post_field_restore', 'post_id' => $post_id, 'field' => $field, 'previous_value' => $current ) ),
                );
            }

            if ( is_serialized( $current ) ) {
                // Serialized PHP meta value — use PHP walker.
                $result = $this->replace_text_in_serialized_php( $current, $find_text, $replace_text );
                if ( empty( $result['success'] ) ) {
                    return $result;
                }
                update_post_meta( $post_id, $meta_key, $result['new_serialized'] );
                $this->clear_builder_caches( $post_id, $meta_key );
                return array(
                    'success'          => true,
                    'summary'          => 'Serijalizovani meta tekst je azuriran (' . intval( $result['match_count'] ) . ' string(ova) izmenjen(o)) u ' . $meta_key . '.',
                    'executed_actions' => array( array( 'type' => 'replace_text_in_post', 'post_id' => $post_id, 'field' => $field ) ),
                    'rollback_entries' => array( array( 'type' => 'post_field_restore', 'post_id' => $post_id, 'field' => $field, 'previous_value' => $current ) ),
                );
            }

            // Plain string meta value.
            if ( false === strpos( $current, $find_text ) ) {
                return array( 'success' => false, 'error' => 'find_text nije pronadjen u meta vrednosti.' );
            }
            $new_value = str_replace( $find_text, $replace_text, $current );
            $written   = $this->write_post_field_value( $post_id, $field, $new_value );
            if ( empty( $written['success'] ) ) {
                return $written;
            }
            return array(
                'success'          => true,
                'summary'          => 'Meta tekst je azuriran u ' . $meta_key . '.',
                'executed_actions' => array( array( 'type' => 'replace_text_in_post', 'post_id' => $post_id, 'field' => $field ) ),
                'rollback_entries' => array( array( 'type' => 'post_field_restore', 'post_id' => $post_id, 'field' => $field, 'previous_value' => $current ) ),
            );
        }

        // --- Standard fields: plain str_replace ---
        if ( false === strpos( $current, $find_text ) ) {
            return array( 'success' => false, 'error' => 'find_text vise nije prisutan u ciljnom post polju.' );
        }

        $new_value = str_replace( $find_text, $replace_text, $current );
        $written   = $this->write_post_field_value( $post_id, $field, $new_value );
        if ( empty( $written['success'] ) ) {
            return $written;
        }

        return array(
            'success'          => true,
            'summary'          => 'Post polje je azurirano.',
            'executed_actions' => array(
                array(
                    'type'    => 'replace_text_in_post',
                    'post_id' => $post_id,
                    'field'   => $field,
                ),
            ),
            'rollback_entries' => array(
                array(
                    'type'           => 'post_field_restore',
                    'post_id'        => $post_id,
                    'field'          => $field,
                    'previous_value' => $current,
                ),
            ),
        );
    }

	/**
	 * Managed blog fields may only be changed through update_blog_post.
	 */
	private function guard_managed_blog_generic_mutation( $post_id, $field ) {
		$post_id = intval( $post_id );
		$field   = (string) $field;
		$post    = $post_id > 0 ? get_post( $post_id ) : null;
		if ( ! $post || 'post' !== $post->post_type || '1' !== (string) get_post_meta( $post_id, Niwa_AI_Blog_Quality_Gate::META_MANAGED, true ) ) {
			return array( 'success' => true );
		}
		$managed_field = in_array( $field, array( 'post_title', 'post_content', 'post_excerpt' ), true ) || 0 === strpos( $field, 'meta:' );
		if ( ! $managed_field ) {
			return array( 'success' => true );
		}
		return array(
			'success' => false,
			'error'   => 'This is a Niwa-managed blog post. Use update_blog_post so dependent assets, SEO metadata, source hash, and the quality report are regenerated together.',
			'error_code' => 'managed_blog_requires_canonical_update',
		);
	}

    private function execute_swap_page_image_action( $action ) {
        $post_id    = intval( isset( $action['args']['post_id'] ) ? $action['args']['post_id'] : 0 );
        $find_image = trim( (string) ( isset( $action['args']['find_image'] ) ? $action['args']['find_image'] : '' ) );
        $new_image  = trim( (string) ( isset( $action['args']['new_image'] ) ? $action['args']['new_image'] : '' ) );

        $resolved = $this->resolve_swap_image_params( $find_image, $new_image );
        if ( empty( $resolved['success'] ) ) {
            return $resolved;
        }

        $find_url   = $resolved['find_url'];
        $new_url    = $resolved['new_url'];
        $new_att_id = $resolved['new_att_id'];

        // If new_image is an external URL, upload it to Media Library first.
        if ( 0 === $new_att_id && 0 === strpos( $new_url, 'http' ) ) {
            if ( ! class_exists( 'Niwa_AI_Admin_Autonomy_Image_Utils' ) || ! method_exists( 'Niwa_AI_Admin_Autonomy_Image_Utils', 'ingest_from_url' ) ) {
                return array( 'success' => false, 'error' => 'Image_Utils klasa nije dostupna za upload slike.' );
            }
            $ingested = \Niwa_AI_Admin_Autonomy_Image_Utils::ingest_from_url( $new_url, 'niwa-swap-' . time(), $post_id );
            if ( is_wp_error( $ingested ) ) {
                return array( 'success' => false, 'error' => 'Upload nove slike nije uspeo: ' . $ingested->get_error_message() );
            }
            if ( ! is_array( $ingested ) || empty( $ingested['attachment_id'] ) ) {
                return array( 'success' => false, 'error' => 'Upload nove slike nije vratio validan attachment.' );
            }
            $new_att_id = intval( $ingested['attachment_id'] );
            $new_url    = (string) wp_get_attachment_url( $new_att_id );
        }

        $patterns        = $this->build_image_find_patterns( $find_url );
        $rollback_entries = array();
        $total_replaced   = 0;

        // Replace in post_content.
        $post    = get_post( $post_id );
        $content = (string) $post->post_content;
        $content_changed = false;
        foreach ( $patterns as $pattern ) {
            if ( false !== strpos( $content, $pattern ) ) {
                $content = str_replace( $pattern, $new_url, $content );
                $content_changed = true;
                ++$total_replaced;
            }
        }
        if ( $content_changed ) {
            $rollback_entries[] = array(
                'type'           => 'post_field_restore',
                'post_id'        => $post_id,
                'field'          => 'post_content',
                'previous_value' => (string) $post->post_content,
            );
            wp_update_post( array( 'ID' => $post_id, 'post_content' => $content ), true );
        }

        // Replace in ALL post meta.
        $all_meta = get_post_meta( $post_id );
        if ( is_array( $all_meta ) ) {
            foreach ( $all_meta as $meta_key => $meta_values ) {
                if ( 0 === strpos( $meta_key, '_niwa_' ) || 0 === strpos( $meta_key, '_transient_' ) ) {
                    continue;
                }

                foreach ( $meta_values as $raw_val ) {
                    if ( ! is_string( $raw_val ) || '' === $raw_val ) {
                        continue;
                    }

                    $has_match = false;
                    foreach ( $patterns as $pattern ) {
                        if ( false !== strpos( $raw_val, $pattern ) ) {
                            $has_match = true;
                            break;
                        }
                    }
                    if ( ! $has_match ) {
                        continue;
                    }

                    // Save backup of the meta value before modification.
                    $rollback_entries[] = array(
                        'type'           => 'post_field_restore',
                        'post_id'        => $post_id,
                        'field'          => 'meta:' . $meta_key,
                        'previous_value' => $raw_val,
                    );

                    $new_val = $raw_val;
                    foreach ( $patterns as $pattern ) {
                        $new_val = str_replace( $pattern, $new_url, $new_val );
                    }

                    // Validate JSON integrity if the value was JSON.
                    $was_json = ( null !== json_decode( $raw_val, true ) && JSON_ERROR_NONE === json_last_error() );
                    if ( $was_json ) {
                        $recheck = json_decode( $new_val, true );
                        if ( null === $recheck && JSON_ERROR_NONE !== json_last_error() ) {
                            continue; // Skip this meta — replacement broke JSON.
                        }
                        update_post_meta( $post_id, $meta_key, wp_slash( $new_val ) );
                    } else {
                        update_post_meta( $post_id, $meta_key, $new_val );
                    }

                    $this->clear_builder_caches( $post_id, $meta_key );
                    ++$total_replaced;
                }
            }
        }

        if ( 0 === $total_replaced ) {
            return array( 'success' => false, 'error' => 'Slika nije pronadjena ni u jednom polju posta.' );
        }

        clean_post_cache( $post_id );

        return array(
            'success'          => true,
            'summary'          => 'Slika je zamenjena na ' . $total_replaced . ' lokacija(e). Nova slika: attachment #' . $new_att_id . '.',
            'executed_actions' => array(
                array(
                    'type'       => 'swap_page_image',
                    'post_id'    => $post_id,
                    'new_att_id' => $new_att_id,
                    'new_url'    => $new_url,
                ),
            ),
            'rollback_entries' => $rollback_entries,
        );
    }

    private function guard_against_plugin_version_bump( $path, $patch_haystack ) {
        $normalized = strtolower( wp_normalize_path( (string) $path ) );
        if ( '' === $normalized ) {
            return null;
        }
        $is_plugin_root = false !== strpos( $normalized, '/niwa-ai/niwa-ai.php' ) || str_ends_with( $normalized, '/niwa-ai.php' );
        if ( ! $is_plugin_root ) {
            return null;
        }
        if ( false !== stripos( (string) $patch_haystack, 'NIWA_AI_VERSION' )
            || preg_match( '/^\s*\*\s*Version:\s*\d/m', (string) $patch_haystack )
        ) {
            return array(
                'success' => false,
                'error'   => 'Version bump is not allowed via the coding tool pipeline. The parent release flow owns NIWA_AI_VERSION and the plugin header Version line.',
                'policy'  => 'no_subagent_version_bump',
            );
        }
        return null;
    }


    private function execute_update_wordpress_core_action( $action ) {
        require_once ABSPATH . 'wp-admin/includes/update.php';
        require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
        wp_version_check();

        $target_version = trim( (string) ( isset( $action['args']['target_version'] ) ? $action['args']['target_version'] : '' ) );
        $updates        = get_core_updates( array( 'dismissed' => false ) );
        $selected       = null;

        foreach ( (array) $updates as $update ) {
            if ( empty( $update->response ) || 'upgrade' !== $update->response ) {
                continue;
            }
            if ( '' !== $target_version && isset( $update->version ) && $target_version !== (string) $update->version ) {
                continue;
            }
            $selected = $update;
            break;
        }

        if ( empty( $selected ) ) {
            return array( 'success' => false, 'error' => 'Trazena WordPress core nadogradnja vise nije dostupna.' );
        }

        $skin     = new Automatic_Upgrader_Skin();
        $upgrader = new Core_Upgrader( $skin );
        $result   = $upgrader->upgrade( $selected );

        if ( is_wp_error( $result ) ) {
            return array( 'success' => false, 'error' => $result->get_error_message() );
        }

        if ( false === $result ) {
            return array( 'success' => false, 'error' => 'WordPress core upgrade nije uspeo.' );
        }

        return array(
            'success'          => true,
            'summary'          => 'WordPress core upgrade je pokrenut/zavrsen.',
            'executed_actions' => array(
                array(
                    'type'           => 'update_wordpress_core',
                    'target_version' => isset( $selected->version ) ? (string) $selected->version : '',
                ),
            ),
            'rollback_entries' => array(),
        );
    }

    private function execute_update_wordpress_plugins_action( $action ) {
        require_once ABSPATH . 'wp-admin/includes/plugin.php';
        require_once ABSPATH . 'wp-admin/includes/update.php';
        require_once ABSPATH . 'wp-admin/includes/file.php';
        require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';

        $args         = isset( $action['args'] ) && is_array( $action['args'] ) ? $action['args'] : array();
        $plugin_files = isset( $args['plugin_files'] ) && is_array( $args['plugin_files'] ) ? array_values( array_unique( array_filter( array_map( 'strval', $args['plugin_files'] ) ) ) ) : array();
        if ( empty( $plugin_files ) ) {
            return array( 'success' => false, 'error' => 'Plugin update nema nijedan plugin file za obradu.' );
        }

        wp_update_plugins();
        $available_updates = get_plugin_updates();
        $still_available   = array();
        foreach ( $plugin_files as $plugin_file ) {
            if ( isset( $available_updates[ $plugin_file ] ) ) {
                $still_available[] = $plugin_file;
            }
        }

        if ( empty( $still_available ) ) {
            return array(
                'success'          => true,
                'summary'          => 'Nema vise dostupnih update-a za izabrane pluginove.',
                'executed_actions' => array(),
                'rollback_entries' => array(),
            );
        }

        $backup = array( 'success' => true, 'backup_id' => '', 'backup_path' => '', 'errors' => array() );
        if ( ! array_key_exists( 'create_backup', $args ) || ! empty( $args['create_backup'] ) ) {
            $backup = $this->create_wordpress_plugin_update_backup( $still_available );
            if ( empty( $backup['success'] ) ) {
                return array(
                    'success' => false,
                    'error'   => isset( $backup['error'] ) ? (string) $backup['error'] : 'Plugin backup pre update-a nije uspeo.',
                    'errors'  => isset( $backup['errors'] ) ? (array) $backup['errors'] : array(),
                );
            }
        }

        $skin     = new Automatic_Upgrader_Skin();
        $upgrader = new Plugin_Upgrader( $skin );
        $result   = $upgrader->bulk_upgrade( $still_available );

        if ( is_wp_error( $result ) ) {
            return array( 'success' => false, 'error' => $result->get_error_message(), 'backup' => $backup );
        }

        if ( false === $result || ! is_array( $result ) ) {
            return array( 'success' => false, 'error' => 'WordPress plugin upgrade nije uspeo.', 'backup' => $backup );
        }

        wp_clean_plugins_cache( true );
        $updated = array();
        $failed  = array();
        foreach ( $still_available as $plugin_file ) {
            $item = isset( $result[ $plugin_file ] ) ? $result[ $plugin_file ] : null;
            if ( is_wp_error( $item ) ) {
                $failed[] = $plugin_file . ': ' . $item->get_error_message();
                continue;
            }
            if ( false === $item || null === $item ) {
                $failed[] = $plugin_file . ': update failed';
                continue;
            }
            $updated[] = $plugin_file;
        }

        if ( empty( $updated ) ) {
            return array(
                'success' => false,
                'error'   => 'Nijedan plugin nije azuriran. ' . implode( '; ', array_slice( $failed, 0, 5 ) ),
                'backup'  => $backup,
            );
        }

        $summary = 'Azurirano pluginova: ' . count( $updated ) . '.';
        if ( ! empty( $backup['backup_path'] ) ) {
            $summary .= ' Backup: ' . (string) $backup['backup_path'];
        }
        if ( ! empty( $failed ) ) {
            $summary .= ' Neki nisu prosli: ' . implode( '; ', array_slice( $failed, 0, 5 ) );
        }

        return array(
            'success'          => true,
            'summary'          => $summary,
            'executed_actions' => array(
                array(
                    'type'         => 'update_wordpress_plugins',
                    'updated'      => array_values( $updated ),
                    'failed'       => array_values( $failed ),
                    'backup_id'    => isset( $backup['backup_id'] ) ? (string) $backup['backup_id'] : '',
                    'backup_path'  => isset( $backup['backup_path'] ) ? (string) $backup['backup_path'] : '',
                ),
            ),
            'rollback_entries' => array(),
        );
    }

    private function execute_manage_wordpress_plugin_activation_action( $action ) {
        require_once ABSPATH . 'wp-admin/includes/plugin.php';

        $args         = isset( $action['args'] ) && is_array( $action['args'] ) ? $action['args'] : array();
        $mode         = isset( $args['action'] ) ? sanitize_key( (string) $args['action'] ) : '';
        $plugin_file  = isset( $args['plugin_file'] ) ? trim( (string) $args['plugin_file'] ) : '';
        $network_wide = ! empty( $args['network_wide'] );

        if ( ! in_array( $mode, array( 'activate', 'deactivate' ), true ) || '' === $plugin_file ) {
            return array( 'success' => false, 'error' => 'Plugin activation action nije validan.' );
        }

        $plugins = function_exists( 'get_plugins' ) ? get_plugins() : array();
        if ( ! isset( $plugins[ $plugin_file ] ) ) {
            return array( 'success' => false, 'error' => 'Plugin nije instaliran: ' . $plugin_file );
        }

        $plugin_name             = isset( $plugins[ $plugin_file ]['Name'] ) ? (string) $plugins[ $plugin_file ]['Name'] : $plugin_file;
        $previous_active         = function_exists( 'is_plugin_active' ) ? is_plugin_active( $plugin_file ) : in_array( $plugin_file, (array) get_option( 'active_plugins', array() ), true );
        $previous_network_active = function_exists( 'is_plugin_active_for_network' ) ? is_plugin_active_for_network( $plugin_file ) : false;

        if ( 'activate' === $mode ) {
            $result = activate_plugin( $plugin_file, '', $network_wide, false );
            if ( is_wp_error( $result ) ) {
                return array( 'success' => false, 'error' => $result->get_error_message() );
            }
        } else {
            deactivate_plugins( array( $plugin_file ), false, $network_wide );
        }

        wp_clean_plugins_cache( true );

        $new_active         = function_exists( 'is_plugin_active' ) ? is_plugin_active( $plugin_file ) : in_array( $plugin_file, (array) get_option( 'active_plugins', array() ), true );
        $new_network_active = function_exists( 'is_plugin_active_for_network' ) ? is_plugin_active_for_network( $plugin_file ) : false;
        $new_effective      = $network_wide ? $new_network_active : ( $new_active || $new_network_active );

        if ( 'activate' === $mode && ! $new_effective ) {
            return array( 'success' => false, 'error' => 'Plugin aktivacija nije prosla verifikaciju: ' . $plugin_file );
        }

        if ( 'deactivate' === $mode && $new_effective ) {
            return array( 'success' => false, 'error' => 'Plugin deaktivacija nije prosla verifikaciju: ' . $plugin_file );
        }

        $summary = $plugin_name . ' je ' . ( 'activate' === $mode ? 'aktiviran' : 'deaktiviran' ) . '.';

        return array(
            'success'          => true,
            'summary'          => $summary,
            'plugin_file'      => $plugin_file,
            'plugin_name'      => $plugin_name,
            'previous_status'  => ( $previous_active || $previous_network_active ) ? 'active' : 'inactive',
            'new_status'       => $new_effective ? 'active' : 'inactive',
            'network_active'   => $new_network_active,
            'executed_actions' => array(
                array(
                    'type'                    => 'manage_wordpress_plugin_activation',
                    'action'                  => $mode,
                    'plugin_file'             => $plugin_file,
                    'plugin_name'             => $plugin_name,
                    'previous_active'         => $previous_active,
                    'previous_network_active' => $previous_network_active,
                    'new_active'              => $new_active,
                    'new_network_active'      => $new_network_active,
                ),
            ),
            'rollback_entries' => array(),
        );
    }

    private function resolve_wordpress_plugin_update_targets( $args ) {
        require_once ABSPATH . 'wp-admin/includes/plugin.php';
        require_once ABSPATH . 'wp-admin/includes/update.php';

        $args      = is_array( $args ) ? $args : array();
        $scope     = isset( $args['scope'] ) ? sanitize_key( (string) $args['scope'] ) : 'all';
        $scope     = in_array( $scope, array( 'all', 'active', 'inactive', 'specific' ), true ) ? $scope : 'all';
        $selectors = isset( $args['plugin_slugs'] ) && is_array( $args['plugin_slugs'] ) ? array_map( array( $this, 'normalize_plugin_update_selector' ), $args['plugin_slugs'] ) : array();
        if ( isset( $args['plugin_files'] ) && is_array( $args['plugin_files'] ) ) {
            $selectors = array_merge( $selectors, array_map( array( $this, 'normalize_plugin_update_selector' ), $args['plugin_files'] ) );
        }
        $selectors = array_values( array_filter( array_unique( $selectors ) ) );

        if ( 'specific' === $scope && empty( $selectors ) ) {
            return array(
                'success' => false,
                'error'   => 'Za specific plugin update treba plugin_slugs ili plugin_files.',
            );
        }

        wp_update_plugins();
        $available_updates = get_plugin_updates();
        $all_plugins       = function_exists( 'get_plugins' ) ? get_plugins() : array();
        $updates           = array();

        foreach ( (array) $available_updates as $plugin_file => $update ) {
            $plugin_file = (string) $plugin_file;
            $is_active   = function_exists( 'is_plugin_active' ) ? is_plugin_active( $plugin_file ) : false;
            if ( 'active' === $scope && ! $is_active ) {
                continue;
            }
            if ( 'inactive' === $scope && $is_active ) {
                continue;
            }

            $slug = dirname( $plugin_file );
            if ( '.' === $slug || '' === $slug ) {
                $slug = preg_replace( '/\.php$/', '', basename( $plugin_file ) );
            }

            $name       = isset( $all_plugins[ $plugin_file ]['Name'] ) ? (string) $all_plugins[ $plugin_file ]['Name'] : $plugin_file;
            $normalized = array(
                $this->normalize_plugin_update_selector( $plugin_file ),
                $this->normalize_plugin_update_selector( $slug ),
                $this->normalize_plugin_update_selector( basename( $plugin_file ) ),
                $this->normalize_plugin_update_selector( $name ),
            );

            if ( 'specific' === $scope && empty( array_intersect( $selectors, $normalized ) ) ) {
                continue;
            }

            $update_data = isset( $update->update ) ? $update->update : null;
            $new_version = '';
            if ( is_object( $update_data ) && isset( $update_data->new_version ) ) {
                $new_version = (string) $update_data->new_version;
            } elseif ( is_object( $update_data ) && isset( $update_data->version ) ) {
                $new_version = (string) $update_data->version;
            } elseif ( isset( $update->new_version ) ) {
                $new_version = (string) $update->new_version;
            }

            $updates[] = array(
                'plugin_file'     => $plugin_file,
                'slug'            => $slug,
                'name'            => $name,
                'current_version' => isset( $all_plugins[ $plugin_file ]['Version'] ) ? (string) $all_plugins[ $plugin_file ]['Version'] : '',
                'new_version'     => $new_version,
                'active'          => $is_active,
            );
        }

        return array(
            'success' => true,
            'scope'   => $scope,
            'updates' => $updates,
        );
    }

    private function normalize_plugin_update_selector( $value ) {
        $value = strtolower( trim( str_replace( '\\', '/', (string) $value ) ) );
        return preg_replace( '/[^a-z0-9_\.\/\-]/', '', $value );
    }

    private function resolve_wordpress_plugin_activation_target( $selector ) {
        require_once ABSPATH . 'wp-admin/includes/plugin.php';

        $selector   = trim( (string) $selector );
        $normalized = $this->normalize_plugin_activation_selector( $selector );
        if ( '' === $normalized ) {
            return array(
                'success' => false,
                'error'   => 'Plugin selector je prazan.',
            );
        }

        $plugins = function_exists( 'get_plugins' ) ? get_plugins() : array();
        $matches = array();
        foreach ( (array) $plugins as $plugin_file => $data ) {
            $plugin_file = (string) $plugin_file;
            $name        = isset( $data['Name'] ) ? (string) $data['Name'] : $plugin_file;
            $slug        = dirname( $plugin_file );
            if ( '.' === $slug || '' === $slug ) {
                $slug = preg_replace( '/\.php$/', '', basename( $plugin_file ) );
            }

            $candidates = array(
                $plugin_file,
                $slug,
                basename( $plugin_file ),
                preg_replace( '/\.php$/', '', basename( $plugin_file ) ),
                $name,
            );

            $candidate_keys = array();
            foreach ( $candidates as $candidate ) {
                $candidate_keys[] = $this->normalize_plugin_activation_selector( $candidate );
            }

            if ( in_array( $normalized, array_filter( array_unique( $candidate_keys ) ), true ) ) {
                $matches[] = array(
                    'plugin_file' => $plugin_file,
                    'name'        => $name,
                    'version'     => isset( $data['Version'] ) ? (string) $data['Version'] : '',
                );
            }
        }

        if ( empty( $matches ) ) {
            return array(
                'success'  => false,
                'error'    => 'Nije pronadjen instaliran plugin za selector: ' . $selector,
                'selector' => $selector,
            );
        }

        if ( count( $matches ) > 1 ) {
            return array(
                'success' => false,
                'error'   => 'Plugin selector nije jednoznacan. Koristi tacan plugin file.',
                'matches' => array_slice( $matches, 0, 8 ),
            );
        }

        $match = $matches[0];
        return array(
            'success'     => true,
            'plugin_file' => (string) $match['plugin_file'],
            'name'        => (string) $match['name'],
            'version'     => (string) $match['version'],
        );
    }

    private function normalize_plugin_activation_selector( $value ) {
        $value = strtolower( trim( str_replace( '\\', '/', (string) $value ) ) );
        return preg_replace( '/[^a-z0-9_\.\/\-]+/', '', $value );
    }

    private function create_wordpress_plugin_update_backup( $plugin_files ) {
        $base_dir = $this->get_plugin_update_backup_base_dir();
        if ( '' === $base_dir ) {
            return array( 'success' => false, 'error' => 'Plugin backup direktorijum nije dostupan.' );
        }

        Niwa_AI_Admin_Autonomy_Workspace::ensure_private_directory( $base_dir );
        $backup_id  = 'plugins-' . gmdate( 'Ymd-His' ) . '-' . substr( md5( implode( '|', (array) $plugin_files ) . ':' . microtime( true ) ), 0, 8 );
        $backup_dir = trailingslashit( $base_dir ) . $backup_id;
        $files_dir  = trailingslashit( $backup_dir ) . 'files';
        Niwa_AI_Admin_Autonomy_Workspace::ensure_private_directory( $files_dir );

        if ( ! is_dir( $files_dir ) || ! is_writable( $backup_dir ) ) {
            return array( 'success' => false, 'error' => 'Ne mogu da napravim plugin backup direktorijum.' );
        }

        $manifest = array(
            'id'             => $backup_id,
            'created_at'     => time(),
            'created_at_utc' => gmdate( 'Y-m-d H:i:s' ),
            'plugin_version' => defined( 'NIWA_AI_VERSION' ) ? NIWA_AI_VERSION : '',
            'plugin_files'   => array_values( (array) $plugin_files ),
            'files'          => array(),
            'errors'         => array(),
        );

        foreach ( (array) $plugin_files as $plugin_file ) {
            $source = $this->resolve_plugin_update_backup_source( $plugin_file );
            if ( empty( $source['success'] ) ) {
                $manifest['errors'][] = isset( $source['error'] ) ? (string) $source['error'] : 'Plugin backup source nije validan.';
                continue;
            }

            $relative = ltrim( str_replace( '\\', '/', (string) $source['relative'] ), '/' );
            $target   = trailingslashit( $files_dir ) . $relative;
            $copied   = $this->copy_plugin_update_backup_item( (string) $source['path'], $target, $relative, $manifest );
            $manifest = $copied['manifest'];
        }

        $manifest['file_count'] = count( $manifest['files'] );
        if ( empty( $manifest['file_count'] ) ) {
            return array(
                'success' => false,
                'error'   => 'Plugin backup nije snimio nijedan fajl.',
                'errors'  => $manifest['errors'],
            );
        }

        $manifest_path = trailingslashit( $backup_dir ) . 'manifest.json';
        $written       = Niwa_AI_Admin_Autonomy_Workspace::write_private_file( $manifest_path, wp_json_encode( $manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ), LOCK_EX );
        if ( false === $written ) {
            return array( 'success' => false, 'error' => 'Plugin backup manifest nije mogao da se upise.' );
        }

        return array(
            'success'       => true,
            'backup_id'     => $backup_id,
            'backup_path'   => $backup_dir,
            'manifest_path' => $manifest_path,
            'file_count'    => $manifest['file_count'],
            'errors'        => $manifest['errors'],
        );
    }

    private function get_plugin_update_backup_base_dir() {
        $workspace_root = class_exists( 'Niwa_AI_Admin_Autonomy_Workspace' )
            ? Niwa_AI_Admin_Autonomy_Workspace::get_private_workspace_root()
            : '';
        return '' !== $workspace_root ? trailingslashit( $workspace_root ) . 'plugin-update-backups' : '';
    }

    private function resolve_plugin_update_backup_source( $plugin_file ) {
        $plugin_file = ltrim( str_replace( '\\', '/', (string) $plugin_file ), '/' );
        if ( '' === $plugin_file || false !== strpos( $plugin_file, '..' ) || ! defined( 'WP_PLUGIN_DIR' ) ) {
            return array( 'success' => false, 'error' => 'Nesiguran plugin path za backup: ' . $plugin_file );
        }

        $plugins_root = realpath( WP_PLUGIN_DIR );
        if ( false === $plugins_root || ! is_dir( $plugins_root ) ) {
            return array( 'success' => false, 'error' => 'WP_PLUGIN_DIR nije dostupan za backup.' );
        }

        $dir = dirname( $plugin_file );
        if ( '.' === $dir || '' === $dir ) {
            $source   = trailingslashit( $plugins_root ) . $plugin_file;
            $relative = $plugin_file;
        } else {
            $source   = trailingslashit( $plugins_root ) . $dir;
            $relative = $dir;
        }

        $real = realpath( $source );
        if ( false === $real || ! file_exists( $real ) ) {
            return array( 'success' => false, 'error' => 'Plugin backup source ne postoji: ' . $plugin_file );
        }

        $root_norm = wp_normalize_path( untrailingslashit( $plugins_root ) );
        $real_norm = wp_normalize_path( $real );
        if ( $real_norm !== $root_norm && 0 !== strpos( $real_norm, trailingslashit( $root_norm ) ) ) {
            return array( 'success' => false, 'error' => 'Plugin backup source je van plugins direktorijuma: ' . $plugin_file );
        }

        return array(
            'success'  => true,
            'path'     => $real,
            'relative' => $relative,
        );
    }

    private function copy_plugin_update_backup_item( $source, $target, $relative_root, $manifest ) {
        $manifest = is_array( $manifest ) ? $manifest : array( 'files' => array(), 'errors' => array() );
        if ( is_file( $source ) ) {
            Niwa_AI_Admin_Autonomy_Workspace::ensure_private_directory( dirname( $target ) );
            if ( ! Niwa_AI_Admin_Autonomy_Workspace::copy_private_file( $source, $target ) ) {
                $manifest['errors'][] = 'Backup copy failed: ' . $relative_root;
            } else {
                $manifest['files'][] = array(
                    'relative_path' => $relative_root,
                    'bytes'         => filesize( $source ),
                    'sha256'        => hash_file( 'sha256', $source ),
                );
            }
            return array( 'manifest' => $manifest );
        }

        if ( ! is_dir( $source ) ) {
            $manifest['errors'][] = 'Backup source is not a file or directory: ' . $relative_root;
            return array( 'manifest' => $manifest );
        }

        $source_root = wp_normalize_path( untrailingslashit( $source ) );
        $iterator    = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator( $source, FilesystemIterator::SKIP_DOTS ),
            RecursiveIteratorIterator::SELF_FIRST
        );

        foreach ( $iterator as $item ) {
            $path = $item->getPathname();
            if ( $item->isLink() ) {
                continue;
            }

            $relative = ltrim( str_replace( $source_root, '', wp_normalize_path( $path ) ), '/' );
            $dest     = trailingslashit( $target ) . $relative;
            if ( $item->isDir() ) {
                Niwa_AI_Admin_Autonomy_Workspace::ensure_private_directory( $dest );
                continue;
            }

            Niwa_AI_Admin_Autonomy_Workspace::ensure_private_directory( dirname( $dest ) );
            if ( ! Niwa_AI_Admin_Autonomy_Workspace::copy_private_file( $path, $dest ) ) {
                $manifest['errors'][] = 'Backup copy failed: ' . trailingslashit( $relative_root ) . $relative;
                continue;
            }

            $manifest['files'][] = array(
                'relative_path' => trailingslashit( $relative_root ) . $relative,
                'bytes'         => filesize( $path ),
                'sha256'        => hash_file( 'sha256', $path ),
            );
        }

        return array( 'manifest' => $manifest );
    }

    private function execute_inventory_cleanup_action( $action ) {
        $ids      = array_values( array_unique( array_map( 'intval', isset( $action['args']['product_ids'] ) ? (array) $action['args']['product_ids'] : array() ) ) );
        $executed = array();
        $rollback = array();

        foreach ( $ids as $product_id ) {
            $post = get_post( $product_id );
            if ( ! $post || 'product' !== $post->post_type ) {
                $this->rollback_entries( $rollback );
                return array( 'success' => false, 'error' => 'Proizvod ne postoji: #' . $product_id );
            }

            if ( ! $this->is_product_effectively_outofstock( $product_id ) ) {
                $this->rollback_entries( $rollback );
                return array( 'success' => false, 'error' => 'Proizvod nije stvarno out-of-stock prema Woo varijacijama: #' . $product_id );
            }

            $rollback[] = array(
                'type'             => 'inventory_cleanup_restore',
                'product_id'       => $product_id,
                'previous_status'  => (string) $post->post_status,
                'had_noindex_meta' => metadata_exists( 'post', $product_id, '_za_noindex' ) ? 1 : 0,
                'previous_noindex' => metadata_exists( 'post', $product_id, '_za_noindex' ) ? (string) get_post_meta( $product_id, '_za_noindex', true ) : '',
            );

            $updated = wp_update_post(
                array(
                    'ID'          => $product_id,
                    'post_status' => 'draft',
                ),
                true
            );

            if ( is_wp_error( $updated ) ) {
                $this->rollback_entries( $rollback );
                return array( 'success' => false, 'error' => 'Neuspesan draft update za proizvod #' . $product_id . ': ' . $updated->get_error_message() );
            }

            update_post_meta( $product_id, '_za_noindex', '1' );
            $executed[] = array(
                'type'       => 'inventory_cleanup',
                'product_id' => $product_id,
            );
        }

        return array(
            'success'          => true,
            'summary'          => 'Draftovano ' . count( $executed ) . ' out-of-stock proizvoda i postavljen _za_noindex=1.',
            'executed_actions' => $executed,
            'rollback_entries' => $rollback,
        );
    }

    /* ================================================================
     * SWAP PAGE IMAGE — builder-agnostic image replacement
     * ============================================================= */

    /**
     * Resolve find_image to a URL pattern and new_image to a final URL.
     * Shared by preview and execute.
     */
    private function resolve_swap_image_params( $find_image, $new_image ) {
        $find_url = '';

        // Resolve find_image.
        if ( is_numeric( $find_image ) ) {
            $find_url = (string) wp_get_attachment_url( intval( $find_image ) );
            if ( ! $find_url ) {
                return array( 'success' => false, 'error' => 'Attachment ID ' . $find_image . ' ne postoji u Media Library.' );
            }
        } elseif ( 0 === strpos( $find_image, 'http' ) ) {
            $find_url = $find_image;
        } else {
            // Treat as filename — will search for it in content.
            $find_url = $find_image;
        }

        // Resolve new_image.
        $new_url = '';
        $new_att_id = 0;
        if ( is_numeric( $new_image ) ) {
            $new_att_id = intval( $new_image );
            $new_url    = (string) wp_get_attachment_url( $new_att_id );
            if ( ! $new_url ) {
                return array( 'success' => false, 'error' => 'Replacement attachment ID ' . $new_image . ' ne postoji.' );
            }
        } elseif ( 0 === strpos( $new_image, 'http' ) ) {
            $new_url    = $new_image;
            $new_att_id = 0; // Will be uploaded during execute.
        } else {
            return array( 'success' => false, 'error' => 'new_image mora biti URL ili attachment ID.' );
        }

        return array(
            'success'    => true,
            'find_url'   => $find_url,
            'new_url'    => $new_url,
            'new_att_id' => $new_att_id,
        );
    }

    /**
     * Build an array of find patterns from a URL.
     * Returns full URL, path-only, and filename-only patterns.
     */
    private function build_image_find_patterns( $find_url ) {
        $patterns = array( $find_url );

        // If it's a full URL, also try path-only and filename.
        $parsed = wp_parse_url( $find_url );
        if ( ! empty( $parsed['path'] ) ) {
            $path_only = $parsed['path'];
            if ( ! in_array( $path_only, $patterns, true ) ) {
                $patterns[] = $path_only;
            }
        }

        $filename = basename( $find_url );
        // Only use filename pattern if it looks like an image file.
        if ( preg_match( '/\.(jpe?g|png|gif|webp|svg|avif|bmp|ico)$/i', $filename ) ) {
            // We DON'T add bare filename as a replacement pattern — too aggressive.
            // It's used only for detection in preview.
        }

        return $patterns;
    }

    private function resolve_post_target( $args ) {
        $target_type  = isset( $args['target_type'] ) ? (string) $args['target_type'] : 'front_page';
        $target_value = trim( (string) ( isset( $args['target_value'] ) ? $args['target_value'] : '' ) );

        if ( 'front_page' === $target_type ) {
            $page_id = intval( get_option( 'page_on_front' ) );
            if ( $page_id <= 0 ) {
                return array( 'success' => false, 'error' => 'Front page nije podesena kao staticka stranica.' );
            }
            $post = get_post( $page_id );
        } elseif ( 'post_id' === $target_type ) {
            $post = get_post( intval( $target_value ) );
        } elseif ( 'slug' === $target_type ) {
            $post = get_page_by_path( $target_value, OBJECT, array( 'page', 'post', 'product' ) );
        } else {
            $posts = get_posts(
                array(
                    'post_type'      => array( 'page', 'post', 'product' ),
                    'post_status'    => array( 'publish', 'draft', 'private', 'future' ),
                    'posts_per_page' => 1,
                    's'              => $target_value,
                )
            );
            $post = ! empty( $posts[0] ) ? $posts[0] : null;
        }

        if ( empty( $post ) ) {
            return array( 'success' => false, 'error' => 'Ciljni post nije pronadjen.' );
        }

        return array(
            'success'    => true,
            'post_id'    => intval( $post->ID ),
            'post_title' => (string) $post->post_title,
        );
    }

    private function read_post_field_value( $post_id, $field ) {
        // Generic meta: prefix — read ANY post meta key.
        if ( 0 === strpos( $field, 'meta:' ) ) {
            $meta_key = substr( $field, 5 );
            return (string) get_post_meta( $post_id, $meta_key, true );
        }

        // Backward compat: elementor_data maps to _elementor_data meta.
        if ( 'elementor_data' === $field ) {
            return (string) get_post_meta( $post_id, '_elementor_data', true );
        }

        $post = get_post( $post_id );
        if ( ! $post ) {
            return '';
        }

        if ( 'post_title' === $field ) {
            return (string) $post->post_title;
        }
        if ( 'post_excerpt' === $field ) {
            return (string) $post->post_excerpt;
        }

        return (string) $post->post_content;
    }

    private function write_post_field_value( $post_id, $field, $value ) {
        // Generic meta: prefix — write ANY post meta key.
        if ( 0 === strpos( $field, 'meta:' ) ) {
            $meta_key = substr( $field, 5 );
            // JSON values need wp_slash() to survive WordPress's wp_unslash() in update_metadata().
            $is_json = ( null !== json_decode( (string) $value, true ) && JSON_ERROR_NONE === json_last_error() );
            $store   = $is_json ? wp_slash( (string) $value ) : (string) $value;
            update_post_meta( $post_id, $meta_key, $store );
            $this->clear_builder_caches( $post_id, $meta_key );
            return array( 'success' => true );
        }

        // Backward compat: elementor_data specific path.
        if ( 'elementor_data' === $field ) {
            update_post_meta( $post_id, '_elementor_data', wp_slash( (string) $value ) );
            $this->clear_builder_caches( $post_id, '_elementor_data' );
            return array( 'success' => true );
        }

        $payload = array( 'ID' => intval( $post_id ) );
        if ( 'post_title' === $field ) {
            $payload['post_title'] = (string) $value;
        } elseif ( 'post_excerpt' === $field ) {
            $payload['post_excerpt'] = (string) $value;
        } else {
            $payload['post_content'] = (string) $value;
        }

        $updated = wp_update_post( $payload, true );
        if ( is_wp_error( $updated ) ) {
            return array(
                'success' => false,
                'error'   => $updated->get_error_message(),
            );
        }

        clean_post_cache( $post_id );
        return array( 'success' => true );
    }
}
