<?php

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

trait Niwa_AI_Tools_Elementor {

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

        $mode    = isset( $args['mode'] ) ? (string) $args['mode'] : 'repair';
        $post_id = $target['post_id'];
        $current = (string) get_post_meta( $post_id, '_elementor_data', true );

        if ( '' === $current ) {
            return array( 'success' => false, 'error' => '_elementor_data je prazan za ovaj post.' );
        }

        if ( 'restore_backup' === $mode ) {
            $backup = (string) get_post_meta( $post_id, '_niwa_elementor_data_backup', true );
            if ( '' === $backup ) {
                return array( 'success' => false, 'error' => 'Nema sacuvanog Niwa backup-a za _elementor_data na ovom postu.' );
            }
            $check = json_decode( $backup, true );
            if ( null === $check && JSON_ERROR_NONE !== json_last_error() ) {
                return array( 'success' => false, 'error' => 'Sacuvani backup takodje nije validan JSON.' );
            }
        } else {
            // Repair mode: check if data is actually corrupted.
            $check = json_decode( $current, true );
            if ( is_array( $check ) ) {
                return array( 'success' => false, 'error' => '_elementor_data je vec validan JSON — repair nije potreban.' );
            }
        }

        return array(
            'success' => true,
            'action'  => array(
                'type'             => 'elementor_restore_or_repair',
                'label'            => 'restore_backup' === $mode ? 'Restore Elementor backup' : 'Repair Elementor JSON',
                'summary'          => ( 'restore_backup' === $mode ? 'Restore _elementor_data iz backup-a' : 'Repair pokvarenog _elementor_data' ) . ' za "' . $target['post_title'] . '".',
                'rollback_plan'    => 'Vraca prethodnu vrednost _elementor_data.',
                'rollback_support' => 'full',
                'targets'          => array(
                    array(
                        'post_id'    => $post_id,
                        'post_title' => $target['post_title'],
                    ),
                ),
                'args'             => array(
                    'post_id' => $post_id,
                    'mode'    => $mode,
                ),
            ),
        );
    }

    private function execute_elementor_restore_or_repair_action( $action ) {
        $post_id = intval( isset( $action['args']['post_id'] ) ? $action['args']['post_id'] : 0 );
        $mode    = (string) ( isset( $action['args']['mode'] ) ? $action['args']['mode'] : 'repair' );
        $current = (string) get_post_meta( $post_id, '_elementor_data', true );

        if ( '' === $current ) {
            return array( 'success' => false, 'error' => '_elementor_data je prazan.' );
        }

        if ( 'restore_backup' === $mode ) {
            $backup = (string) get_post_meta( $post_id, '_niwa_elementor_data_backup', true );
            if ( '' === $backup ) {
                return array( 'success' => false, 'error' => 'Nema sacuvanog backup-a.' );
            }
            $check = json_decode( $backup, true );
            if ( null === $check && JSON_ERROR_NONE !== json_last_error() ) {
                return array( 'success' => false, 'error' => 'Backup nije validan JSON: ' . json_last_error_msg() );
            }
            $new_value = $backup;
        } else {
            // Repair mode: attempt to fix common corruption patterns.
            $repaired = $this->repair_elementor_json( $current );
            if ( empty( $repaired['success'] ) ) {
                return $repaired;
            }
            $new_value = $repaired['json'];
        }

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

        return array(
            'success'          => true,
            'summary'          => 'restore_backup' === $mode
                ? '_elementor_data je vracen iz backup-a.'
                : '_elementor_data je popravljen (' . ( isset( $repaired['fixes_applied'] ) ? intval( $repaired['fixes_applied'] ) : 0 ) . ' popravke primenjene).',
            'executed_actions' => array(
                array(
                    'type'    => 'elementor_restore_or_repair',
                    'post_id' => $post_id,
                    'mode'    => $mode,
                ),
            ),
            'rollback_entries' => array(
                array(
                    'type'           => 'post_field_restore',
                    'post_id'        => $post_id,
                    'field'          => 'elementor_data',
                    'previous_value' => $current,
                ),
            ),
        );
    }

    /**
     * Attempt to repair corrupted Elementor JSON.
     *
     * Addresses common corruption patterns caused by wp_unslash() stripping backslashes:
     * - Unescaped double quotes inside JSON string values (from \" → ")
     * - Literal newline/tab characters that should be \n/\t escapes
     * - Corrupted unicode escapes
     *
     * @param string $broken The corrupted JSON string.
     * @return array          'success', 'json', 'fixes_applied', or 'error'.
     */
    private function repair_elementor_json( $broken ) {
        // First, try a straightforward approach: maybe it's just minor issues.
        $attempt = json_decode( $broken, true );
        if ( is_array( $attempt ) ) {
            // Already valid — re-encode cleanly.
            $clean = wp_json_encode( $attempt, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES );
            return array( 'success' => true, 'json' => $clean, 'fixes_applied' => 0 );
        }

        $fixes = 0;
        $repaired = $broken;

        // Fix 1: Remove literal newlines and tabs that ended up inside JSON keys and non-value positions.
        // These are caused by \n → literal newline when backslash is stripped.
        // Strategy: replace literal newlines/tabs that appear inside quoted strings improperly.
        // We use a character-by-character state machine to only fix newlines inside JSON strings.
        $result   = '';
        $in_str   = false;
        $escaped  = false;
        $len      = strlen( $repaired );

        for ( $i = 0; $i < $len; $i++ ) {
            $ch = $repaired[ $i ];

            if ( $escaped ) {
                $result .= $ch;
                $escaped = false;
                continue;
            }

            if ( '\\' === $ch ) {
                $result .= $ch;
                $escaped = true;
                continue;
            }

            if ( '"' === $ch ) {
                $in_str = ! $in_str;
                $result .= $ch;
                continue;
            }

            // If we're inside a JSON string and encounter a literal newline/tab/CR, escape it.
            if ( $in_str ) {
                if ( "\n" === $ch ) {
                    $result .= '\\n';
                    ++$fixes;
                    continue;
                }
                if ( "\r" === $ch ) {
                    $result .= '\\r';
                    ++$fixes;
                    continue;
                }
                if ( "\t" === $ch ) {
                    $result .= '\\t';
                    ++$fixes;
                    continue;
                }
            }

            $result .= $ch;
        }

        $repaired = $result;

        // Fix 2: Try to fix unescaped quotes inside string values.
        // This is trickier — unescaped quotes from \" → " break JSON structure.
        // We attempt a re-parse after the newline fix first.
        $attempt = json_decode( $repaired, true );
        if ( is_array( $attempt ) ) {
            $clean = wp_json_encode( $attempt, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES );
            if ( false !== $clean && '' !== $clean ) {
                return array( 'success' => true, 'json' => $clean, 'fixes_applied' => $fixes );
            }
        }

        // Fix 3: If still broken, try a more aggressive approach — attempt to re-escape
        // quotes inside HTML attribute values (common: style="color: red" → style=\"color: red\").
        // Look for patterns like: ="value" inside JSON strings where the quotes aren't escaped.
        $repaired = preg_replace_callback(
            '/=\s*"([^"]*?)"\s*(?=[;>\/\s])/u',
            function ( $m ) use ( &$fixes ) {
                ++$fixes;
                return '=\\"' . $m[1] . '\\"';
            },
            $repaired
        );

        $attempt = json_decode( $repaired, true );
        if ( is_array( $attempt ) ) {
            $clean = wp_json_encode( $attempt, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES );
            if ( false !== $clean && '' !== $clean ) {
                return array( 'success' => true, 'json' => $clean, 'fixes_applied' => $fixes );
            }
        }

        return array(
            'success' => false,
            'error'   => 'Automatski repair nije uspeo da popravi JSON. json_last_error: ' . json_last_error_msg() . '. Moguce je da je korupcija previse teska za automatsku popravku — potreban je rucni intervencija ili restore iz WordPress backup-a.',
        );
    }

    /* ================================================================
     * ELEMENTOR SAFE REPLACEMENT
     * ============================================================= */

    /**
     * Safely replace text within Elementor JSON data using parsed tree traversal.
     *
     * Instead of str_replace() on raw JSON (which breaks when find_text contains
     * decoded characters that differ from their JSON-encoded forms), this method:
     * 1. Decodes the JSON into a PHP array
     * 2. Walks the Elementor widget tree
     * 3. Performs str_replace() on decoded setting values
     * 4. Re-encodes to JSON
     * 5. Validates the result
     *
     * @param string $raw_json     The raw _elementor_data JSON string.
     * @param string $find_text    The decoded text to find (as returned by extract_elementor_text_nodes).
     * @param string $replace_text The decoded replacement text.
     * @return array               'success', 'new_json', 'match_count', or 'error'.
     */
    private function replace_text_in_elementor_data( $raw_json, $find_text, $replace_text ) {
        $decoded = json_decode( $raw_json, true );
        if ( ! is_array( $decoded ) ) {
            return array(
                'success' => false,
                'error'   => 'Elementor data nije validan JSON — ne mogu da izvrsim zamenu. json_last_error: ' . json_last_error_msg(),
            );
        }

        $match_count = 0;
        $this->walk_elementor_replace( $decoded, $find_text, $replace_text, $match_count );

        if ( 0 === $match_count ) {
            return array(
                'success' => false,
                'error'   => 'find_text nije pronadjen ni u jednom Elementor widget-u.',
            );
        }

        // Re-encode with the same flags Elementor uses.
        $new_json = wp_json_encode( $decoded, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES );
        if ( false === $new_json || '' === $new_json ) {
            return array(
                'success' => false,
                'error'   => 'json_encode nije uspeo nakon zamene teksta.',
            );
        }

        // Final validation: make sure the result is still valid JSON.
        $recheck = json_decode( $new_json, true );
        if ( null === $recheck && JSON_ERROR_NONE !== json_last_error() ) {
            return array(
                'success' => false,
                'error'   => 'Rezultat zamene nije validan JSON: ' . json_last_error_msg(),
            );
        }

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

    /**
     * Recursively walk Elementor element tree and replace text in widget settings.
     *
     * Uses the same text_fields list as extract_elementor_text_nodes() in chat.php
     * to ensure consistency between read (extract) and write (replace) operations.
     * IMPORTANT: Keep the $text_fields list in sync with extract_elementor_text_nodes().
     *
     * @param array  &$elements     Elementor elements array (modified in place).
     * @param string $find_text     Decoded text to find.
     * @param string $replace_text  Decoded replacement text.
     * @param int    &$match_count  Counter for total replacements made.
     * @param int    $depth         Recursion depth guard.
     */
    private function walk_elementor_replace( &$elements, $find_text, $replace_text, &$match_count, $depth = 0 ) {
        if ( ! is_array( $elements ) || $depth > 20 ) {
            return;
        }

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

            if ( isset( $element['settings'] ) && is_array( $element['settings'] ) ) {
                // ----- V3 classic: known text fields stored as direct strings. -----
                $text_fields = array(
                    'title', 'editor', 'text', 'description_text', 'description',
                    'button_text', 'inner_text', 'prefix', 'suffix',
                    'before_text', 'after_text', 'highlight_text', 'rotating_text',
                    'alert_title', 'alert_description', 'tab_title',
                    'testimonial_content', 'testimonial_name', 'testimonial_job',
                    'caption', 'link_text', 'badge_text', 'ribbon_title',
                    'sub_title', 'header_text', 'footer_text',
                );

                foreach ( $text_fields as $key ) {
                    if ( isset( $element['settings'][ $key ] )
                         && is_string( $element['settings'][ $key ] )
                         && false !== strpos( $element['settings'][ $key ], $find_text )
                    ) {
                        $element['settings'][ $key ] = str_replace(
                            $find_text,
                            $replace_text,
                            $element['settings'][ $key ]
                        );
                        ++$match_count;
                    }
                }

                // ----- V4 typed values: {$$type: "string", value: "..."} -----
                // Elementor V4 wraps text content in typed objects.  The $$type
                // annotation guarantees the value is user-visible text, so we
                // can safely scan ALL settings keys — no hard-coded list needed.
                foreach ( $element['settings'] as $key => &$typed_val ) {
                    if ( is_array( $typed_val )
                         && isset( $typed_val['$$type'] )
                         && 'string' === $typed_val['$$type']
                         && isset( $typed_val['value'] )
                         && is_string( $typed_val['value'] )
                         && false !== strpos( $typed_val['value'], $find_text )
                    ) {
                        $typed_val['value'] = str_replace( $find_text, $replace_text, $typed_val['value'] );
                        ++$match_count;
                    }
                }
                unset( $typed_val );

                // ----- V3 Repeaters: icon_list items. -----
                if ( isset( $element['settings']['icon_list'] ) && is_array( $element['settings']['icon_list'] ) ) {
                    foreach ( $element['settings']['icon_list'] as &$item ) {
                        if ( isset( $item['text'] ) && is_string( $item['text'] )
                             && false !== strpos( $item['text'], $find_text )
                        ) {
                            $item['text'] = str_replace( $find_text, $replace_text, $item['text'] );
                            ++$match_count;
                        }
                    }
                    unset( $item );
                }

                // ----- V3 Repeaters: tabs items. -----
                if ( isset( $element['settings']['tabs'] ) && is_array( $element['settings']['tabs'] ) ) {
                    foreach ( $element['settings']['tabs'] as &$tab ) {
                        foreach ( array( 'tab_title', 'tab_content' ) as $tk ) {
                            if ( isset( $tab[ $tk ] ) && is_string( $tab[ $tk ] )
                                 && false !== strpos( $tab[ $tk ], $find_text )
                            ) {
                                $tab[ $tk ] = str_replace( $find_text, $replace_text, $tab[ $tk ] );
                                ++$match_count;
                            }
                        }
                    }
                    unset( $tab );
                }

                // ----- V4 Repeaters: any sequential array of objects with typed values. -----
                foreach ( $element['settings'] as $key => &$repeater ) {
                    if ( ! is_array( $repeater ) || ! isset( $repeater[0] ) || ! is_array( $repeater[0] ) ) {
                        continue;
                    }
                    foreach ( $repeater as &$rep_item ) {
                        if ( ! is_array( $rep_item ) ) {
                            continue;
                        }
                        foreach ( $rep_item as $rk => &$rv ) {
                            if ( is_array( $rv )
                                 && isset( $rv['$$type'] )
                                 && 'string' === $rv['$$type']
                                 && isset( $rv['value'] )
                                 && is_string( $rv['value'] )
                                 && false !== strpos( $rv['value'], $find_text )
                            ) {
                                $rv['value'] = str_replace( $find_text, $replace_text, $rv['value'] );
                                ++$match_count;
                            }
                        }
                        unset( $rv );
                    }
                    unset( $rep_item );
                }
                unset( $repeater );
            }

            // Recurse into child elements.
            if ( isset( $element['elements'] ) && is_array( $element['elements'] ) ) {
                $this->walk_elementor_replace( $element['elements'], $find_text, $replace_text, $match_count, $depth + 1 );
            }
        }
        unset( $element );
    }

    /* ================================================================
     * GENERIC JSON / SERIALIZED-PHP WALKERS — builder-agnostic
     * ============================================================= */

    /**
     * Generic recursive walker that finds and replaces text in ALL string
     * values within any JSON-decoded structure.  No hard-coded field name
     * lists — works with Elementor, Beaver Builder, Divi, Brizy, Oxygen,
     * or any custom JSON structure.
     *
     * @param mixed  &$data         JSON-decoded data (modified in place).
     * @param string $find_text     Text to find.
     * @param string $replace_text  Replacement text.
     * @param int    &$match_count  Counter incremented per replaced value.
     * @param int    $depth         Recursion depth guard.
     */
    private function walk_json_strings_replace( &$data, $find_text, $replace_text, &$match_count, $depth = 0 ) {
        if ( $depth > 50 || ! is_array( $data ) ) {
            return;
        }

        foreach ( $data as $key => &$value ) {
            if ( is_string( $value ) && false !== strpos( $value, $find_text ) ) {
                $value = str_replace( $find_text, $replace_text, $value );
                ++$match_count;
            } elseif ( is_array( $value ) ) {
                $this->walk_json_strings_replace( $value, $find_text, $replace_text, $match_count, $depth + 1 );
            }
        }
        unset( $value );
    }

    /**
     * Generic JSON text replacement.  Decodes JSON, walks ALL string values,
     * replaces text, re-encodes, and validates.
     *
     * @param string $raw_json      Raw JSON string.
     * @param string $find_text     Text to find.
     * @param string $replace_text  Replacement text.
     * @return array {success, new_json, match_count} or {success: false, error}.
     */
    private function replace_text_in_json_data( $raw_json, $find_text, $replace_text ) {
        $decoded = json_decode( $raw_json, true );
        if ( ! is_array( $decoded ) ) {
            return array(
                'success' => false,
                'error'   => 'Vrednost nije validan JSON — json_last_error: ' . json_last_error_msg(),
            );
        }

        $match_count = 0;
        $this->walk_json_strings_replace( $decoded, $find_text, $replace_text, $match_count );

        if ( 0 === $match_count ) {
            return array(
                'success' => false,
                'error'   => 'find_text nije pronadjen ni u jednoj string vrednosti unutar JSON-a.',
            );
        }

        $new_json = wp_json_encode( $decoded, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES );
        if ( false === $new_json || '' === $new_json ) {
            return array( 'success' => false, 'error' => 'json_encode nije uspeo nakon zamene teksta.' );
        }

        $recheck = json_decode( $new_json, true );
        if ( null === $recheck && JSON_ERROR_NONE !== json_last_error() ) {
            return array( 'success' => false, 'error' => 'Rezultat zamene nije validan JSON: ' . json_last_error_msg() );
        }

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

    /**
     * Recursive walker for serialized-PHP structures (Beaver Builder, etc.).
     * Walks arrays, objects, and strings — replaces find_text in all strings.
     */
    private function walk_php_strings_replace( &$data, $find_text, $replace_text, &$match_count, $depth = 0 ) {
        if ( $depth > 50 ) {
            return;
        }
        if ( is_string( $data ) && false !== strpos( $data, $find_text ) ) {
            $data = str_replace( $find_text, $replace_text, $data );
            ++$match_count;
        } elseif ( is_array( $data ) ) {
            foreach ( $data as &$value ) {
                $this->walk_php_strings_replace( $value, $find_text, $replace_text, $match_count, $depth + 1 );
            }
            unset( $value );
        } elseif ( is_object( $data ) ) {
            foreach ( get_object_vars( $data ) as $prop => $value ) {
                $this->walk_php_strings_replace( $data->$prop, $find_text, $replace_text, $match_count, $depth + 1 );
            }
        }
    }

    /**
     * Replace text inside serialized PHP data.  Unserializes, walks all
     * strings, and re-serializes.  maybe_serialize() recalculates string
     * lengths so the output is always structurally valid.
     *
     * @param string $serialized    Serialized PHP string.
     * @param string $find_text     Text to find.
     * @param string $replace_text  Replacement text.
     * @return array {success, new_serialized, match_count} or {success: false, error}.
     */
    private function replace_text_in_serialized_php( $serialized, $find_text, $replace_text ) {
        $data = maybe_unserialize( $serialized );
        // If maybe_unserialize returned the same string, it was not serialized.
        if ( is_string( $data ) && $data === $serialized && is_serialized( $serialized ) === false ) {
            return array( 'success' => false, 'error' => 'Vrednost nije validan serijalizovan PHP.' );
        }

        $match_count = 0;
        $this->walk_php_strings_replace( $data, $find_text, $replace_text, $match_count );

        if ( 0 === $match_count ) {
            return array( 'success' => false, 'error' => 'find_text nije pronadjen u serijalizovanim podacima.' );
        }

        $new_serialized = maybe_serialize( $data );
        return array(
            'success'        => true,
            'new_serialized' => $new_serialized,
            'match_count'    => $match_count,
        );
    }

    /**
     * Clear page-builder caches after modifying post meta.
     * Detects which builder the meta key belongs to and clears the appropriate caches.
     *
     * @param int    $post_id  Post ID.
     * @param string $meta_key Meta key that was modified.
     */
    private function clear_builder_caches( $post_id, $meta_key ) {
        // Elementor.
        if ( '_elementor_data' === $meta_key ) {
            delete_post_meta( $post_id, '_elementor_css' );
            $css_file = wp_upload_dir()['basedir'] . '/elementor/css/post-' . intval( $post_id ) . '.css';
            if ( file_exists( $css_file ) ) {
                @unlink( $css_file );
            }
            if ( class_exists( '\Elementor\Plugin' ) ) {
                \Elementor\Plugin::$instance->files_manager->clear_cache();
            }
        }

        // Beaver Builder.
        if ( 0 === strpos( $meta_key, '_fl_builder' ) ) {
            if ( class_exists( 'FLBuilderModel' ) ) {
                \FLBuilderModel::delete_asset_cache( $post_id );
            }
        }

        // Divi Builder.
        if ( '_et_builder_data' === $meta_key || 0 === strpos( $meta_key, '_et_pb_' ) ) {
            if ( function_exists( 'et_core_clear_transients' ) ) {
                et_core_clear_transients();
            }
            delete_post_meta( $post_id, '_et_builder_css' );
        }

        // Brizy.
        if ( 0 === strpos( $meta_key, 'brizy' ) ) {
            delete_post_meta( $post_id, 'brizy_post_compiled_html' );
        }

        // Generic: clear WP object cache for this post.
        wp_cache_delete( $post_id, 'posts' );
        clean_post_cache( $post_id );
    }
}
