<?php

namespace WPAdminify\Pro;

use WPAdminify\Inc\Utils;

// no direct access allowed
if (!defined('ABSPATH')) {
    exit;
}

/**
 * Class for Media Replacement module
 */
class JLT_Adminify_Media_Replacer
{

    /**
     * Constructor to initialize hooks.
     */
    public function __construct()
    {
        add_filter('media_row_actions', [$this, 'jltwp_adminify_modify_media_list_table_edit_link'], 10, 2);
        add_filter('attachment_fields_to_edit', [$this, 'jltwp_adminify_add_media_replacement_button'], 10, 2);
        add_action('add_meta_boxes_attachment', [$this, 'jltwp_adminify_register_replace_media_metabox']);
        add_action('wp_ajax_jltwp_adminify_replace_media', [$this, 'jltwp_adminify_replace_media']);
        add_action('wp_ajax_jltwp_adminify_replace_media_upload', [$this, 'jltwp_adminify_replace_media_upload']);
        add_action('wp_ajax_jltwp_adminify_restore_original', [$this, 'jltwp_adminify_restore_original']);
        add_action('delete_attachment', [$this, 'jltwp_adminify_cleanup_backup_on_delete']);
        // add_action('edit_attachment', [$this, 'jltwp_adminify_replace_media']);
        add_filter('wp_prepare_attachment_for_js', [$this, 'jltwp_adminify_append_cache_busting_param_to_attachment_for_js'], 10, 2);
        add_filter('wp_get_attachment_image_src', [$this, 'jltwp_adminify_append_cache_busting_param_to_attachment_image_src'], 10, 2);
        add_filter('wp_get_attachment_url', [$this, 'jltwp_adminify_append_cache_busting_param_to_attachment_url'], 10, 2);
        add_filter('wp_calculate_image_srcset', [$this, 'jltwp_adminify_append_cache_busting_param_to_image_srcset'], 10, 5);
        add_filter('post_updated_messages', [$this, 'jltwp_adminify_attachment_updated_custom_message']);

        add_action('admin_enqueue_scripts', array($this, 'jltwp_adminify_enqueue_scripts'));

        // Enable/Disable Infinite Scroll
        add_filter('media_library_infinite_scrolling', '__return_true');
    }

    public function jltwp_adminify_enqueue_scripts(){
        // Resolve Pro plugin dir/url from WP_ADMINIFY_PRO_FILE so paths work in
        // both dev (single plugin) and Freemius-split (free=adminify/, pro=adminify-pro/) layouts.
        $pro_file = defined('WP_ADMINIFY_PRO_FILE') ? WP_ADMINIFY_PRO_FILE : WP_ADMINIFY_FILE;
        $pro_dir  = plugin_dir_path($pro_file);
        $pro_url  = plugin_dir_url($pro_file);

        $css_rel = 'Pro/assets/css/media-replace' . Utils::assets_ext('.css');
        $js_rel  = 'Pro/assets/js/media-replace' . Utils::assets_ext('.js');

        $css_path = $pro_dir . $css_rel;
        $js_path  = $pro_dir . $js_rel;

        $css_ver = file_exists($css_path) ? filemtime($css_path) : WP_ADMINIFY_VER;
        $js_ver  = file_exists($js_path)  ? filemtime($js_path)  : WP_ADMINIFY_VER;

        wp_enqueue_style('wp-adminify-media-replace', $pro_url . $css_rel, false, $css_ver);

        wp_enqueue_script('wp-adminify-media-replace', $pro_url . $js_rel, array('jquery'), $js_ver, true);

        $localized = array(
            'ajax_url'                  => admin_url('admin-ajax.php'),
            'nonce'                     => wp_create_nonce('adminify-replace-media-nonce'),
            'back_to_original'          => __('Back to Original', 'adminify'),
            'restoring_text'            => __('Restoring...', 'adminify'),
            'confirm_restore_title'     => __('Undo replacement?', 'adminify'),
            'confirm_restore'           => __('Brings back the previous file. Every replacement is saved, So you can keep going back.', 'adminify'),
            'confirm_restore_btn'       => __('Yes, Restore!', 'adminify'),
            'cancel'                    => __('Cancel', 'adminify'),
            'restore_success'           => __('Original restored successfully.', 'adminify'),
            'restore_error'             => __('Error restoring original.', 'adminify'),
        );

        // Attachment edit screen (post.php?post=ID&action=edit): expose backup state
        // so the JS can render the inline "Back to Original" button next to "Edit Image".
        global $pagenow;
        if ($pagenow === 'post.php' && !empty($_GET['post']) && !empty($_GET['action']) && $_GET['action'] === 'edit') {
            $current_id = (int) $_GET['post'];
            if ($current_id > 0 && get_post_type($current_id) === 'attachment') {
                $localized['current_attachment'] = array(
                    'id'         => $current_id,
                    'has_backup' => $this->jltwp_adminify_has_backup($current_id),
                    'count'      => count($this->jltwp_adminify_get_backup_stack($current_id)),
                );
            }
        }

        wp_localize_script('wp-adminify-media-replace', 'jltwp_adminify_ajax', $localized);
    }

    /**
     * Modify the 'Edit' link to be 'Edit or Replace'.
     */
    public function jltwp_adminify_modify_media_list_table_edit_link($actions, $post)
    {
        foreach ($actions as $key => $value) {
            if ($key == 'edit') {
                $actions['edit'] = '<a href="' . get_edit_post_link($post) . '" aria-label="Edit or Replace">Edit or Replace</a>';
            }
        }

        // Add "Back to Original" row action when at least one backup exists for this attachment.
        if (is_object($post) && property_exists($post, 'ID') && $this->jltwp_adminify_has_backup($post->ID)) {
            $count = count($this->jltwp_adminify_get_backup_stack($post->ID));
            $label = esc_html__('Back to Original', 'adminify');
            if ($count > 1) {
                $label .= ' <span class="count">(' . (int) $count . ')</span>';
            }
            $actions['jltwp-adminify-back-to-original'] = sprintf(
                '<a href="#" class="jltwp-adminify-back-to-original" data-attachment-id="%d" aria-label="%s">%s</a>',
                (int) $post->ID,
                esc_attr__('Step back to the previously replaced file', 'adminify'),
                $label
            );
        }

        return $actions;
    }

    /**
     * Add media replacement button in the edit screen of media/attachment.
     */
    public function jltwp_adminify_add_media_replacement_button($fields, $post)
    {
        global $pagenow, $typenow;

        // Skip on attachment edit screen (post.php) — handled by side metabox.
        if ($pagenow === 'post.php' && $typenow === 'attachment') {
            return $fields;
        }

        // Avoid on post creation and editing screen to prevent layout issues
        if ($typenow == 'attachment' || ($typenow != 'attachment' && $pagenow != 'post-new.php' && $pagenow != 'post.php')) {
            wp_enqueue_media();

            $fields['jltwp-adminify-media-replace'] = [
                'label' => '',
                'input' => 'html',
                'html'  => $this->jltwp_adminify_render_replace_media_box($post),
            ];
        }

        return $fields;
    }

    /**
     * Register Replace Media metabox in the side column on attachment edit screens.
     */
    public function jltwp_adminify_register_replace_media_metabox()
    {
        add_meta_box(
            'jltwp-adminify-media-replace',
            __('Media Replace', 'adminify'),
            [$this, 'jltwp_adminify_render_replace_media_metabox'],
            'attachment',
            'side',
            'default'
        );
    }

    /**
     * Render callback for the side metabox.
     */
    public function jltwp_adminify_render_replace_media_metabox($post)
    {
        wp_enqueue_media();
        echo wp_kses_post( $this->jltwp_adminify_render_replace_media_box($post, true) );
    }

    /**
     * Build Replace Media UI markup. Reused by the modal field and the side metabox.
     *
     * @param \WP_Post|object $post
     * @param bool            $is_metabox  When true, render without the postbox wrapper (metabox provides one).
     */
    protected function jltwp_adminify_render_replace_media_box($post, $is_metabox = false)
    {
        $image_mime_type = is_object($post) && property_exists($post, 'post_mime_type') ? $post->post_mime_type : '';
        $attachment_id   = is_object($post) && property_exists($post, 'ID') ? (int) $post->ID : 0;

        // Restrict the native file picker to the exact mime + extension of the
        // current attachment so the user cannot pick something incompatible.
        $current_path = $attachment_id ? get_attached_file($attachment_id) : '';
        $file_ext = $current_path ? strtolower(pathinfo($current_path, PATHINFO_EXTENSION)) : '';

        $accept_parts = array();
        if ($image_mime_type !== '') {
            $accept_parts[] = $image_mime_type;
        }
        if ($file_ext !== '') {
            $accept_parts[] = '.' . $file_ext;
        }
        $accept = !empty($accept_parts) ? implode(',', $accept_parts) : '*/*';

        $note          = esc_html__('Note: Keep the same attachement ID, filename, metadata and date - existing links and embeds still work.', 'adminify');
        $btn_label     = esc_html__('Select New Media', 'adminify');
        $confirm_title = esc_html__('Replace this Media?', 'adminify');
        $confirm_desc  = esc_html__('A backup will be saved. You can restore it later.', 'adminify');
        $cancel_label  = esc_html__('Cancel', 'adminify');
        $do_label      = esc_html__('Replace', 'adminify');

        $inner  = '<div class="jltwp-adminify-replace-media-wrap" data-attachment-id="' . esc_attr($attachment_id) . '" data-mime-type="' . esc_attr($image_mime_type) . '" data-file-ext="' . esc_attr($file_ext) . '">';

        $inner .= '<div class="jltwp-adminify-replace-trigger-row">';
        $inner .= '<button type="button" class="jltwp-adminify-btn jltwp-adminify-btn-outline jltwp-adminify-replace-trigger">';
        $inner .= '<svg class="jltwp-adminify-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>';
        $inner .= '<span>' . $btn_label . '</span>';
        $inner .= '</button>';
        $inner .= '</div>';

        $inner .= '<input type="file" class="jltwp-adminify-replace-file" accept="' . esc_attr($accept) . '" hidden />';

        // Inline confirmation card (hidden until a file is picked).
        $inner .= '<div class="jltwp-adminify-replace-confirm" hidden>';
        $inner .= '<div class="jltwp-adminify-replace-confirm-card">';
        $inner .= '<div class="jltwp-adminify-replace-confirm-header">';
        $inner .= '<div class="jltwp-adminify-replace-confirm-icon" aria-hidden="true">';
        $inner .= '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>';
        $inner .= '</div>';
        $inner .= '<div class="jltwp-adminify-replace-confirm-text">';
        $inner .= '<strong class="jltwp-adminify-replace-confirm-title">' . $confirm_title . '</strong>';
        $inner .= '<p class="jltwp-adminify-replace-confirm-desc">' . $confirm_desc . '</p>';
        $inner .= '</div>';
        $inner .= '</div>';
        $inner .= '<div class="jltwp-adminify-replace-confirm-preview" hidden>';
        $inner .= '<img class="jltwp-adminify-replace-preview-img" alt="" />';
        $inner .= '<div class="jltwp-adminify-replace-preview-meta">';
        $inner .= '<span class="jltwp-adminify-replace-preview-name"></span>';
        $inner .= '<span class="jltwp-adminify-replace-preview-size"></span>';
        $inner .= '</div>';
        $inner .= '</div>';
        $inner .= '<div class="jltwp-adminify-replace-confirm-actions">';
        $inner .= '<button type="button" class="jltwp-adminify-btn jltwp-adminify-btn-ghost jltwp-adminify-replace-cancel">' . $cancel_label . '</button>';
        $inner .= '<button type="button" class="jltwp-adminify-btn jltwp-adminify-btn-primary jltwp-adminify-replace-do">' . $do_label . '</button>';
        $inner .= '</div>';
        $inner .= '</div>';
        $inner .= '</div>';

        $inner .= '<div class="jltwp-adminify-replace-message" hidden></div>';
        $inner .= '<p class="jltwp-adminify-replace-notes"><em>' . $note . '</em></p>';
        $inner .= '</div>';

        if ($is_metabox) {
            return $inner;
        }

        return '
            <div id="media-replace-div" class="postbox jltwp-adminify-replace-postbox">
                <div class="postbox-header jltwp-adminify-replace-postbox-header">
                    <h2 class="hndle ui-sortable-handle jltwp-adminify-replace-postbox-title">' . esc_html__('Media Replace', 'adminify') . '</h2>
                    <p class="jltwp-adminify-replace-postbox-subtitle">' . esc_html__('Upload and Swap with new file. (*same type)', 'adminify') . '</p>
                </div>
                <div class="inside jltwp-adminify-replace-postbox-inside">' . $inner . '</div>
            </div>
        ';
    }

    /**
     * AJAX: replace existing media via direct file upload (native picker / drag-drop).
     */
    public function jltwp_adminify_replace_media_upload()
    {
        check_ajax_referer('adminify-replace-media-nonce', 'security');

        if (!current_user_can('upload_files')) {
            wp_send_json_error(array('message' => __('You do not have permission to perform this action.', 'adminify')));
        }

        $attachment_id = isset($_POST['attachment_id']) ? (int) $_POST['attachment_id'] : 0;
        if ($attachment_id <= 0) {
            wp_send_json_error(array('message' => __('Missing attachment ID.', 'adminify')));
        }

        if (empty($_FILES['replacement_file']) || !is_array($_FILES['replacement_file']) || !empty($_FILES['replacement_file']['error'])) {
            $err_code = isset($_FILES['replacement_file']['error']) ? (int) $_FILES['replacement_file']['error'] : 0;
            $msg = $err_code === UPLOAD_ERR_INI_SIZE || $err_code === UPLOAD_ERR_FORM_SIZE
                ? __('File is too large.', 'adminify')
                : __('No file uploaded.', 'adminify');
            wp_send_json_error(array('message' => $msg));
        }

        $old_post = get_post($attachment_id);
        if (!$old_post || $old_post->post_type !== 'attachment') {
            wp_send_json_error(array('message' => __('Attachment not found.', 'adminify')));
        }

        $file = $_FILES['replacement_file'];

        if (!is_uploaded_file($file['tmp_name'])) {
            wp_send_json_error(array('message' => __('Invalid upload.', 'adminify')));
        }

        $check = wp_check_filetype_and_ext($file['tmp_name'], $file['name']);
        if (empty($check['type'])) {
            wp_send_json_error(array('message' => __('File type not allowed.', 'adminify')));
        }

        if ($check['type'] !== $old_post->post_mime_type) {
            wp_send_json_error(array('message' => __('Replacement file must be the same type as the original.', 'adminify')));
        }

        // Enforce extension parity with the existing attachment file name.
        $old_ext = strtolower(pathinfo((string) get_attached_file($attachment_id), PATHINFO_EXTENSION));
        $new_ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
        if ($old_ext && $new_ext) {
            $jpeg_pair = in_array($old_ext, array('jpg', 'jpeg'), true) && in_array($new_ext, array('jpg', 'jpeg'), true);
            if (!$jpeg_pair && $old_ext !== $new_ext) {
                wp_send_json_error(array(
                    'message' => sprintf(
                        /* translators: 1: required extension, 2: provided extension */
                        __('Replacement file must have a .%1$s extension (got .%2$s).', 'adminify'),
                        $old_ext,
                        $new_ext
                    ),
                ));
            }
        }

        $old_attachment_meta = wp_get_attachment_metadata($attachment_id);
        $old_attachment_meta = is_array($old_attachment_meta) ? $old_attachment_meta : array();
        $old_media_file_path = array_key_exists('original_image', $old_attachment_meta) ? wp_get_original_image_path($attachment_id) : get_attached_file($attachment_id);

        if (!$old_media_file_path) {
            wp_send_json_error(array('message' => __('Original file path not found.', 'adminify')));
        }

        // Snapshot the existing original file BEFORE deletion so the user can restore it later.
        $this->jltwp_adminify_backup_original($attachment_id, $old_media_file_path);

        $this->jltwp_adminify_delete_media_files($attachment_id);

        if (!file_exists(dirname($old_media_file_path))) {
            wp_mkdir_p(dirname($old_media_file_path));
        }

        if (!@move_uploaded_file($file['tmp_name'], $old_media_file_path)) {
            wp_send_json_error(array('message' => __('Failed to move uploaded file.', 'adminify')));
        }

        @chmod($old_media_file_path, 0644);

        if (!function_exists('wp_generate_attachment_metadata')) {
            require_once ABSPATH . 'wp-admin/includes/image.php';
        }

        $new_meta = wp_generate_attachment_metadata($attachment_id, $old_media_file_path);
        wp_update_attachment_metadata($attachment_id, $new_meta);

        $this->jltwp_adminify_rewrite_post_content_urls($attachment_id, $old_attachment_meta, $new_meta);

        $options_extra = get_option('adminify_extra', []);
        $recently_replaced_media = isset($options_extra['recently_replaced_media']) && is_array($options_extra['recently_replaced_media']) ? $options_extra['recently_replaced_media'] : [];
        if (count($recently_replaced_media) >= 5) {
            array_shift($recently_replaced_media);
        }
        $recently_replaced_media[] = $attachment_id;
        $options_extra['recently_replaced_media'] = array_values(array_unique($recently_replaced_media));
        update_option('adminify_extra', $options_extra);

        wp_send_json_success(array(
            'message'       => __('Image replaced successfully.', 'adminify'),
            'attachment_id' => $attachment_id,
            'has_backup'    => true,
        ));
    }

    /**
     * Rewrite filenames in post_content when an attachment's main file or size
     * filenames change after a replace/restore. Without this, blocks/HTML in
     * existing posts still point at old size filenames that no longer exist.
     *
     * Only runs against post_content (HTML/Gutenberg blocks). Skips postmeta to
     * avoid corrupting serialized data length headers.
     */
    protected function jltwp_adminify_rewrite_post_content_urls($attachment_id, $old_meta, $new_meta)
    {
        global $wpdb;

        $old_meta = is_array($old_meta) ? $old_meta : array();
        $new_meta = is_array($new_meta) ? $new_meta : array();

        $pairs = array();

        $old_main = !empty($old_meta['file']) ? basename($old_meta['file']) : '';
        $new_main = !empty($new_meta['file']) ? basename($new_meta['file']) : basename((string) get_attached_file($attachment_id));

        if ($old_main && $new_main && $old_main !== $new_main) {
            $pairs[$old_main] = $new_main;
        }

        $old_sizes = !empty($old_meta['sizes']) && is_array($old_meta['sizes']) ? $old_meta['sizes'] : array();
        $new_sizes = !empty($new_meta['sizes']) && is_array($new_meta['sizes']) ? $new_meta['sizes'] : array();

        foreach ($old_sizes as $size_name => $old_info) {
            if (empty($old_info['file'])) {
                continue;
            }
            $new_file = isset($new_sizes[$size_name]['file']) ? $new_sizes[$size_name]['file'] : ($new_main ?: '');
            if (!$new_file || $new_file === $old_info['file']) {
                continue;
            }
            $pairs[$old_info['file']] = $new_file;
        }

        if (empty($pairs)) {
            return;
        }

        // Sort keys by length descending so longer filenames replace before
        // shorter ones that may be substrings (rare but possible).
        uksort($pairs, function ($a, $b) {
            return strlen($b) - strlen($a);
        });

        foreach ($pairs as $old => $new) {
            $wpdb->query(
                $wpdb->prepare(
                    "UPDATE {$wpdb->posts} SET post_content = REPLACE(post_content, %s, %s) WHERE post_content LIKE %s",
                    $old,
                    $new,
                    '%' . $wpdb->esc_like($old) . '%'
                )
            );
        }

        clean_attachment_cache($attachment_id);
    }

    /**
     * Read the backup stack stored in postmeta. Each entry is an associative
     * array with path/name/mime/date keys. The newest replacement is appended
     * last so popping restores in reverse order (LIFO).
     *
     * Migrates the legacy single-entry format transparently.
     */
    protected function jltwp_adminify_get_backup_stack($attachment_id)
    {
        $raw = get_post_meta($attachment_id, '_jltwp_adminify_replace_backup', true);
        if (empty($raw) || !is_array($raw)) {
            return array();
        }
        // Legacy single-entry format had top-level path/name/mime/date keys.
        if (isset($raw['path'])) {
            return array($raw);
        }
        return array_values($raw);
    }

    protected function jltwp_adminify_has_backup($attachment_id)
    {
        $stack = $this->jltwp_adminify_get_backup_stack($attachment_id);
        foreach ($stack as $entry) {
            if (!empty($entry['path']) && file_exists($entry['path'])) {
                return true;
            }
        }
        return false;
    }

    /**
     * Resolve the directory used to keep replace-media backups (created on demand).
     * Stored under jltwp-adminify-backups/media-replace so other future backup
     * categories can live alongside it without collisions.
     */
    protected function jltwp_adminify_get_backup_dir()
    {
        $upload_dir = wp_upload_dir();
        $dir = trailingslashit($upload_dir['basedir']) . 'jltwp-adminify-backups/media-replace';
        if (!file_exists($dir)) {
            wp_mkdir_p($dir);
        }
        return $dir;
    }

    /**
     * Resolve the configured backup retention limit (minimum 1). Older backups
     * beyond this count are pruned when a new replacement is recorded.
     */
    protected function jltwp_adminify_get_backup_limit()
    {
        $limit = 1;
        if (class_exists('\\WPAdminify\\Inc\\Admin\\AdminSettings')) {
            $options = (array) \WPAdminify\Inc\Admin\AdminSettings::get_instance()->get();
            if (isset($options['media_attachments']['media_replace_backup_limit'])) {
                $limit = (int) $options['media_attachments']['media_replace_backup_limit'];
            }
        }
        return max(1, $limit);
    }

    /**
     * Push the current attachment file onto the backup stack so each replacement
     * remains restorable. Restore pops the newest entry, walking back step by step:
     * Replace 1 → Replace 2 → Replace 3 → Restore (now Replace 2) → Restore (now Replace 1) → Restore (now Original).
     */
    protected function jltwp_adminify_backup_original($attachment_id, $source_path)
    {
        if (!$source_path || !file_exists($source_path)) {
            return false;
        }

        $dir = $this->jltwp_adminify_get_backup_dir();
        $basename = basename($source_path);
        $pi = pathinfo($basename);
        $ext = !empty($pi['extension']) ? '.' . $pi['extension'] : '';
        $name = isset($pi['filename']) ? $pi['filename'] : 'attachment';

        $dest_filename = $name . '_original' . $ext;
        if (file_exists(trailingslashit($dir) . $dest_filename)) {
            $dest_filename = wp_unique_filename($dir, $dest_filename);
        }
        $dest = trailingslashit($dir) . $dest_filename;

        if (!@copy($source_path, $dest)) {
            return false;
        }
        @chmod($dest, 0644);

        // Snapshot size files so a restore can rebuild metadata identically. We
        // rely on wp_generate_attachment_metadata to regenerate sizes from the
        // restored source, so only the source file itself is needed.
        $entry = array(
            'path'  => $dest,
            'name'  => $basename,
            'mime'  => get_post_mime_type($attachment_id),
            'meta'  => wp_get_attachment_metadata($attachment_id),
            'date'  => current_time('mysql'),
        );

        $stack = $this->jltwp_adminify_get_backup_stack($attachment_id);
        $stack[] = $entry;

        // Enforce retention limit: drop oldest entries (FIFO) and unlink their files.
        $limit = $this->jltwp_adminify_get_backup_limit();
        while (count($stack) > $limit) {
            $oldest = array_shift($stack);
            if (!empty($oldest['path']) && file_exists($oldest['path'])) {
                @unlink($oldest['path']);
            }
        }

        update_post_meta($attachment_id, '_jltwp_adminify_replace_backup', array_values($stack));

        return $entry;
    }

    /**
     * AJAX: restore the backed-up original file as the current attachment file.
     */
    public function jltwp_adminify_restore_original()
    {
        check_ajax_referer('adminify-replace-media-nonce', 'security');

        if (!current_user_can('upload_files')) {
            wp_send_json_error(array('message' => __('You do not have permission to perform this action.', 'adminify')));
        }

        $attachment_id = isset($_POST['attachment_id']) ? (int) $_POST['attachment_id'] : 0;
        if ($attachment_id <= 0) {
            wp_send_json_error(array('message' => __('Missing attachment ID.', 'adminify')));
        }

        $post = get_post($attachment_id);
        if (!$post || $post->post_type !== 'attachment') {
            wp_send_json_error(array('message' => __('Attachment not found.', 'adminify')));
        }

        $stack = $this->jltwp_adminify_get_backup_stack($attachment_id);
        // Pop the newest backup entry that still has a usable file on disk.
        $entry = null;
        while (!empty($stack)) {
            $candidate = array_pop($stack);
            if (!empty($candidate['path']) && file_exists($candidate['path'])) {
                $entry = $candidate;
                break;
            }
        }

        if (!$entry) {
            // Nothing restorable left; clear meta to clean up dangling pointers.
            delete_post_meta($attachment_id, '_jltwp_adminify_replace_backup');
            wp_send_json_error(array('message' => __('No backup found for this attachment.', 'adminify')));
        }

        $old_meta = wp_get_attachment_metadata($attachment_id);
        $old_meta = is_array($old_meta) ? $old_meta : array();
        $target = array_key_exists('original_image', $old_meta) ? wp_get_original_image_path($attachment_id) : get_attached_file($attachment_id);

        if (!$target) {
            wp_send_json_error(array('message' => __('Original file path not found.', 'adminify')));
        }

        $this->jltwp_adminify_delete_media_files($attachment_id);

        if (!file_exists(dirname($target))) {
            wp_mkdir_p(dirname($target));
        }

        if (!@copy($entry['path'], $target)) {
            wp_send_json_error(array('message' => __('Failed to restore backup file.', 'adminify')));
        }
        @chmod($target, 0644);

        if (!function_exists('wp_generate_attachment_metadata')) {
            require_once ABSPATH . 'wp-admin/includes/image.php';
        }

        $new_meta = wp_generate_attachment_metadata($attachment_id, $target);
        wp_update_attachment_metadata($attachment_id, $new_meta);

        $this->jltwp_adminify_rewrite_post_content_urls($attachment_id, $old_meta, $new_meta);

        if (!empty($entry['mime'])) {
            wp_update_post(array(
                'ID'             => $attachment_id,
                'post_mime_type' => $entry['mime'],
            ));
        }

        @unlink($entry['path']);

        // Persist remaining stack so the user can keep stepping back through history.
        if (!empty($stack)) {
            update_post_meta($attachment_id, '_jltwp_adminify_replace_backup', array_values($stack));
        } else {
            delete_post_meta($attachment_id, '_jltwp_adminify_replace_backup');
        }

        $options_extra = get_option('adminify_extra', []);
        $recently_replaced_media = isset($options_extra['recently_replaced_media']) && is_array($options_extra['recently_replaced_media']) ? $options_extra['recently_replaced_media'] : array();
        if (count($recently_replaced_media) >= 5) {
            array_shift($recently_replaced_media);
        }
        $recently_replaced_media[] = $attachment_id;
        $options_extra['recently_replaced_media'] = array_values(array_unique($recently_replaced_media));
        update_option('adminify_extra', $options_extra);

        wp_send_json_success(array(
            'message'       => __('Original restored successfully.', 'adminify'),
            'attachment_id' => $attachment_id,
        ));
    }

    /**
     * Remove the backup file when its parent attachment is deleted.
     */
    public function jltwp_adminify_cleanup_backup_on_delete($attachment_id)
    {
        $stack = $this->jltwp_adminify_get_backup_stack($attachment_id);
        foreach ($stack as $entry) {
            if (!empty($entry['path']) && file_exists($entry['path'])) {
                @unlink($entry['path']);
            }
        }
        delete_post_meta($attachment_id, '_jltwp_adminify_replace_backup');
    }

    /**
     * Replace existing media with the newly updated file.
     */
    public function jltwp_adminify_replace_media()
    {
        // Security check - verify nonce and capability
        check_ajax_referer('adminify-replace-media-nonce', 'security');

        if (!current_user_can('upload_files')) {
            wp_send_json_error(array('message' => __('You do not have permission to perform this action.', 'adminify')));
        }

        if (empty($_POST['attachment_id']) || empty($_POST['new-attachment-id'])) {
            wp_send_json_error(array('message' => __('Missing attachment ID(s).', 'adminify')));
        }

        $old_attachment_id = intval($_POST['attachment_id']);
        $new_attachment_id = intval($_POST['new-attachment-id']);

        if ($old_attachment_id <= 0 || $new_attachment_id <= 0 || $old_attachment_id === $new_attachment_id) {
            wp_send_json_error(array('message' => __('Invalid attachment selection.', 'adminify')));
        }

        $old_post = get_post($old_attachment_id);
        $new_post = get_post($new_attachment_id);

        if (!$old_post || !$new_post || $old_post->post_type !== 'attachment' || $new_post->post_type !== 'attachment') {
            wp_send_json_error(array('message' => __('Attachment not found.', 'adminify')));
        }

        if ($old_post->post_mime_type !== $new_post->post_mime_type) {
            wp_send_json_error(array('message' => __('The selected file must be the same type as the original.', 'adminify')));
        }

        $new_attachment_meta = wp_get_attachment_metadata($new_attachment_id);
        $new_attachment_meta = is_array($new_attachment_meta) ? $new_attachment_meta : array();
        $new_media_file_path = array_key_exists('original_image', $new_attachment_meta) ? wp_get_original_image_path($new_attachment_id) : get_attached_file($new_attachment_id);

        if (!$new_media_file_path || !is_file($new_media_file_path)) {
            wp_send_json_error(array('message' => __('Source file for replacement not found.', 'adminify')));
        }

        $old_attachment_meta = wp_get_attachment_metadata($old_attachment_id);
        $old_attachment_meta = is_array($old_attachment_meta) ? $old_attachment_meta : array();
        $old_media_file_path = array_key_exists('original_image', $old_attachment_meta) ? wp_get_original_image_path($old_attachment_id) : get_attached_file($old_attachment_id);

        if (!$old_media_file_path) {
            wp_send_json_error(array('message' => __('Original file path not found.', 'adminify')));
        }

        $this->jltwp_adminify_delete_media_files($old_attachment_id);

        if (!file_exists(dirname($old_media_file_path))) {
            wp_mkdir_p(dirname($old_media_file_path));
        }

        if (!@copy($new_media_file_path, $old_media_file_path)) {
            wp_send_json_error(array('message' => __('Failed to copy replacement file.', 'adminify')));
        }

        if (!function_exists('wp_generate_attachment_metadata')) {
            require_once ABSPATH . 'wp-admin/includes/image.php';
        }

        $old_media_post_meta_updated = wp_generate_attachment_metadata($old_attachment_id, $old_media_file_path);
        wp_update_attachment_metadata($old_attachment_id, $old_media_post_meta_updated);

        wp_delete_attachment($new_attachment_id, true);

        $options_extra = get_option('adminify_extra', []);
        $recently_replaced_media = isset($options_extra['recently_replaced_media']) && is_array($options_extra['recently_replaced_media']) ? $options_extra['recently_replaced_media'] : [];
        if (count($recently_replaced_media) >= 5) {
            array_shift($recently_replaced_media);
        }
        $recently_replaced_media[] = $old_attachment_id;
        $options_extra['recently_replaced_media'] = array_values(array_unique($recently_replaced_media));
        update_option('adminify_extra', $options_extra);

        wp_send_json_success(array(
            'message'       => __('Media file replaced successfully.', 'adminify'),
            'attachment_id' => $old_attachment_id,
        ));
    }

    /**
     * Delete the existing/old media files when performing media replacement.
     */
    public function jltwp_adminify_delete_media_files($post_id)
    {
        $attachment_meta = wp_get_attachment_metadata($post_id);
        $attachment_file_path = get_attached_file($post_id);

        if (isset($attachment_meta['sizes']) && is_array($attachment_meta['sizes'])) {
            foreach ($attachment_meta['sizes'] as $size_info) {
                $intermediate_file_path = str_replace(basename($attachment_file_path), $size_info['file'], $attachment_file_path);
                wp_delete_file($intermediate_file_path);
            }
        }

        wp_delete_file($attachment_file_path);

        if (array_key_exists('original_image', $attachment_meta)) {
            $original_file_path = wp_get_original_image_path($post_id);
            wp_delete_file($original_file_path);
        }
    }

    /**
     * Customize the attachment updated message.
     */
    public function jltwp_adminify_attachment_updated_custom_message($messages)
    {
        if (isset($messages['attachment'][4])) {
            $messages['attachment'][4] = 'Media file updated. You may need to <a href="https://fabricdigital.co.nz/blog/how-to-hard-refresh-your-browser-and-clear-cache" target="_blank">hard refresh</a> your browser to see the updated media preview image below.';
        }
        return $messages;
    }

    /**
     * Append cache busting parameter to the end of image srcset.
     */
    public function jltwp_adminify_append_cache_busting_param_to_image_srcset($sources, $size_array, $image_src, $image_meta, $attachment_id)
    {
        $options_extra = get_option('adminify_extra', []);
        $recently_replaced_media = $options_extra['recently_replaced_media'] ?? [];

        if (in_array($attachment_id, $recently_replaced_media)) {
            foreach ($sources as $size => $source) {
                $source['url'] .= (false === strpos($source['url'], '?') ? '?' : '&') . 't=' . time();
                $sources[$size] = $source;
            }
        }
        return $sources;
    }

    /**
     * Append cache busting parameter to the end of image src.
     */
    public function jltwp_adminify_append_cache_busting_param_to_attachment_image_src($image, $attachment_id)
    {
        $options_extra = get_option('adminify_extra', []);
        $recently_replaced_media = $options_extra['recently_replaced_media'] ?? [];

        if (!empty($image[0]) && in_array($attachment_id, $recently_replaced_media)) {
            $image[0] .= (false === strpos($image[0], '?') ? '?' : '&') . 't=' . time();
        }

        return $image;
    }

    /**
     * Append cache busting parameter to image src for JS.
     */
    public function jltwp_adminify_append_cache_busting_param_to_attachment_for_js($response, $attachment)
    {
        $options_extra = get_option('adminify_extra', []);
        $recently_replaced_media = $options_extra['recently_replaced_media'] ?? [];

        if (in_array($attachment->ID, $recently_replaced_media)) {
            $response['url'] .= (false === strpos($response['url'], '?') ? '?' : '&') . 't=' . time();
            if (isset($response['sizes'])) {
                foreach ($response['sizes'] as $size_name => $size) {
                    $response['sizes'][$size_name]['url'] .= (false === strpos($size['url'], '?') ? '?' : '&') . 't=' . time();
                }
            }
        }

        $response['jltwpAdminifyHasBackup'] = $this->jltwp_adminify_has_backup($attachment->ID);
        $response['jltwpAdminifyBackupCount'] = count($this->jltwp_adminify_get_backup_stack($attachment->ID));

        return $response;
    }

    /**
     * Append cache busting parameter to attachment URL.
     */
    public function jltwp_adminify_append_cache_busting_param_to_attachment_url($url, $attachment_id)
    {
        $options_extra = get_option('adminify_extra', []);
        $recently_replaced_media = $options_extra['recently_replaced_media'] ?? [];

        if (in_array($attachment_id, $recently_replaced_media)) {
            $url .= (false === strpos($url, '?') ? '?' : '&') . 't=' . time();
        }

        return $url;
    }
}

// Initialize the class
new JLT_Adminify_Media_Replacer();
