<?php

/**
 * Helper functions for syncing WordPress data with Hub.
 *
 * @package northmill-online-shared
 */
namespace Northmill_Online_Shared_Sync\Northmill_Online_Shared\Helpers;

use Exception;
use Imagick;
use Northmill_Online_Shared_Sync\Northmill_Online_Shared\Abstract_Shared_Base;
use Northmill_Online_Shared_Sync\Northmill_Online_Shared\Abstracts\Hub_API\Abstract_API_Base;
use Northmill_Online_Shared_Sync\Northmill_Online_Shared\Modules\Loggers\Log_Level_Enum;
use WP_Error;
use WP_Post;
defined('ABSPATH') || exit;
/**
 * WP_Data_Sync_Helpers class
 *
 * @used Northmill Booking
 */
class WP_Data_Sync_Helpers
{
    /**
     * Updates meta value if it's not the same as before
     *
     * @param string      $meta_key   Key of meta to update.
     * @param string|null $meta_value New value.
     * @param bool|null   $autoload   Whether to autoload the option. Default is null, which means it will use the default value for the autoload parameter.
     *
     * @used Northmill Booking
     */
    public static function update_option(string $meta_key, ?string $meta_value, ?bool $autoload = null): void
    {
        $current = get_option($meta_key, '');
        $meta_value = (string) $meta_value;
        if ($meta_value === $current) {
            return;
        }
        update_option($meta_key, $meta_value, $autoload);
        Abstract_Shared_Base::base_instance()->logger->log('sync', 'WP option set', array('meta' => $meta_key, 'val' => $meta_value));
    }
    /**
     * Updates meta value if it's not the same as before
     *
     * @param int    $post_id    Post ID.
     * @param string $meta_key   Key of meta to update.
     * @param mixed  $meta_value Array or scalar value.
     *
     * @used Northmill Booking
     */
    public static function update_meta(int $post_id, string $meta_key, mixed $meta_value): void
    {
        $current = get_post_meta($post_id, $meta_key, \true);
        if (!is_array($meta_value) && $meta_value === $current) {
            return;
        }
        update_post_meta($post_id, $meta_key, $meta_value);
        Abstract_Shared_Base::base_instance()->logger->log('sync', 'Set meta', array('post' => $post_id, 'meta' => $meta_key, 'val' => $meta_value));
    }
    /**
     * Get all posts of a certain post type.
     *
     * @param string $post_type Post type slug.
     *
     * @return WP_Post[]
     *
     * @used Northmill Express Order
     */
    public static function get_posts(string $post_type): array
    {
        return get_posts(array('post_type' => $post_type, 'status' => 'publish', 'posts_per_page' => -1, 'ignore_sticky_posts' => \true));
    }
    /**
     * Gets post id that has unique mf id.
     *
     * @param string $mf_id     Unique mf id.
     * @param string $post_type Post Type.
     *
     * @return int|bool Post id on success, else false
     */
    public static function get_post_id(string $mf_id, string $post_type): bool|int
    {
        $posts = get_posts(array('post_type' => $post_type, 'status' => 'all', 'posts_per_page' => 1, 'ignore_sticky_posts' => \true, 'meta_query' => array(array('key' => 'mf_id', 'value' => $mf_id))));
        if (!empty($posts)) {
            return $posts[0]->ID;
        }
        return \false;
    }
    /**
     * Gets post content by post id.
     *
     * @param int $page_id Post ID.
     *
     * @return string Post content.
     *
     * @used Northmill Webshop Theme
     */
    public static function get_post_content(int $page_id): string
    {
        $p = get_post($page_id);
        return $p ? $p->post_content : '';
    }
    /**
     * Creates or updates post that has unique mf id
     *
     * @param string $mf_id     Unique mf id for this post.
     * @param string $name      Title of the post.
     * @param string $post_type Post type.
     * @param string $content   Post Content.
     *
     * @return int|bool Post ID on success, else false
     *
     * @used Northmill Express Order
     * @used Northmill Booking
     */
    public static function update_post(string $mf_id, string $name, string $post_type, string $content = ''): bool|int
    {
        $post_id = self::get_post_id($mf_id, $post_type);
        if ($post_id) {
            $post = get_post($post_id);
            $updates = array();
            if ($name !== $post->post_title) {
                $updates['post_title'] = $name;
            }
            if ($content !== $post->post_content) {
                $updates['post_content'] = $content;
            }
            if (!empty($updates)) {
                $updates['ID'] = $post_id;
                wp_update_post($updates);
                Abstract_Shared_Base::base_instance()->logger->log('sync', 'Updated post in WP.', array('hub_id' => $mf_id, 'post_type' => $post_type));
            }
        } else {
            $post_id = wp_insert_post(array('post_type' => $post_type, 'post_title' => $name, 'post_status' => 'publish', 'post_content' => $content));
            if (is_wp_error($post_id)) {
                Abstract_Shared_Base::base_instance()->logger->log('sync', 'Error creating new post in WP.', array('hub_id' => $mf_id, 'error' => $post_id), Log_Level_Enum::ERROR);
                return \false;
            }
            if (!$post_id) {
                Abstract_Shared_Base::base_instance()->logger->log('sync', 'Error creating new post in WP. wp_insert_post returned 0.', array('hub_id' => $mf_id), Log_Level_Enum::ERROR);
                return \false;
            }
            self::update_meta($post_id, 'mf_id', $mf_id);
            Abstract_Shared_Base::base_instance()->logger->log('sync', 'New post created in WP', array('hub_id' => $mf_id, 'post_id' => $post_id, 'name' => $name));
        }
        return $post_id;
    }
    /**
     * Removes all old imports of $post_type, that doesn't have the latest import guid.
     *
     * @param string $latest_import_guid Latest import guid.
     * @param string $post_type          Post type.
     *
     * @used Northmill Express Order
     * @used Northmill Booking
     */
    public static function delete_posts_from_old_import(string $latest_import_guid, string $post_type): void
    {
        $posts = get_posts(array('post_type' => $post_type, 'nopaging' => \true, 'fields' => 'ids', 'ignore_sticky_posts' => \true, 'post_status' => 'all', 'meta_query' => array(array('key' => 'mf_import_guid', 'compare' => '!=', 'value' => $latest_import_guid))));
        if (empty($posts)) {
            return;
        }
        foreach ($posts as $post) {
            $mf_id = get_post_meta($post, 'mf_id', \true);
            if (!$mf_id) {
                continue;
            }
            self::delete_post($mf_id, $post_type);
        }
    }
    /**
     * Deletes a post that has unique mf id. If it's not found then it does nothing
     *
     * @param string $mf_id     Unique mf id.
     * @param string $post_type Post type-.
     *
     * @return bool True on deleted, false on not found
     *
     * @used Northmill Booking
     */
    public static function delete_post(string $mf_id, string $post_type): bool
    {
        $post_id = self::get_post_id($mf_id, $post_type);
        if ($post_id) {
            $title = get_the_title($post_id);
            wp_delete_post($post_id);
            Abstract_Shared_Base::base_instance()->logger->log('sync', 'Post deleted in WP', array('hub_id' => $mf_id, 'post_type' => $post_type, 'title' => $title));
            return \true;
        }
        return \false;
    }
    /**
     * Gets term that has unique mf id
     *
     * @param string $mf_id    Unique mf id for this term.
     * @param string $taxonomy Taxonomy slug.
     * @param string $meta_key Key of meta to query for mf_id. Default is 'mf_id'.
     *
     * @return int|bool Term ID on success, else false
     *
     * @used Northmill Express Order
     * @used Northmill Booking
     */
    public static function get_term(string $mf_id, string $taxonomy, string $meta_key = 'mf_id'): bool|int
    {
        $terms = get_terms(array('taxonomy' => $taxonomy, 'hide_empty' => \false, 'meta_query' => array(array('key' => $meta_key, 'value' => $mf_id)), 'number' => 1));
        return empty($terms) ? \false : $terms[0]->term_id;
    }
    /**
     * Creates or updates term that has unique mf id
     *
     * @param string $mf_id    Unique mf id for this term.
     * @param string $name     Title of the term.
     * @param string $taxonomy Taxonomy slug.
     * @param array  $args     Additional args for wp_insert_term/wp_update_term. Supported keys are 'parent' (int) and 'slug' (string).
     *
     * @return int|bool Term ID on success, else false
     *
     * @used Northmill Booking
     */
    public static function update_term(string $mf_id, string $name, string $taxonomy, array $args = array()): bool|int
    {
        $term_id = self::get_term($mf_id, $taxonomy);
        if (!$term_id) {
            // INSERT: Pass parent (and slug if provided).
            $insert_args = array();
            if (isset($args['parent'])) {
                $insert_args['parent'] = (int) $args['parent'];
            }
            if (isset($args['slug'])) {
                $insert_args['slug'] = $args['slug'];
            }
            $inserted_term = wp_insert_term($name, $taxonomy, $insert_args);
            if (is_wp_error($inserted_term)) {
                Abstract_Shared_Base::base_instance()->logger->log('sync', 'Error creating new term in WP.', array('hub_id' => $mf_id, 'name' => $name, 'tax' => $taxonomy, 'args' => $args, 'insert_args' => $insert_args, 'error' => $inserted_term), Log_Level_Enum::ERROR);
                return \false;
            }
            $term_id = (int) $inserted_term['term_id'];
            update_term_meta($term_id, 'mf_id', $mf_id);
            Abstract_Shared_Base::base_instance()->logger->log('sync', 'New term created in WP', array('hub_id' => $mf_id, 'term' => $taxonomy, 'name' => $name, 'parent' => $insert_args['parent'] ?? 0));
        } else {
            // UPDATE: Change name/slug if needed; also set parent if provided and different.
            $term = get_term($term_id, $taxonomy);
            $update_args = array();
            if ($term && $term->name !== $name) {
                $update_args['name'] = $name;
                $update_args['slug'] = wp_unique_term_slug(sanitize_title($name), $term);
            }
            if (isset($args['parent']) && $term->parent !== (int) $args['parent']) {
                $update_args['parent'] = (int) $args['parent'];
            }
            if (!empty($update_args)) {
                wp_update_term($term_id, $taxonomy, $update_args);
                Abstract_Shared_Base::base_instance()->logger->log('sync', 'Updated term in WP', array('hub_id' => $mf_id, 'term' => $taxonomy) + $update_args);
            }
        }
        return $term_id;
    }
    /**
     * Updates term meta value if it's not the same as before
     *
     * @param int    $term_id    Term id.
     * @param string $meta_key   Key of meta to update.
     * @param mixed  $meta_value New value.
     *
     * @used Northmill Booking
     */
    public static function update_term_meta(int $term_id, string $meta_key, mixed $meta_value): void
    {
        $current = get_term_meta($term_id, $meta_key, \true);
        if (!is_array($meta_value) && $meta_value === $current) {
            return;
        }
        update_term_meta($term_id, $meta_key, $meta_value);
        Abstract_Shared_Base::base_instance()->logger->log('sync', 'Set term meta', array('term' => $term_id, 'meta' => $meta_key, 'val' => $meta_value));
    }
    /**
     * Adds a page as a nav item to a menu.
     *
     * @param int $menu_id Menu WP ID.
     * @param int $page_id Page WP ID.
     *
     * @used Northmill Booking
     * @used Northmill Webshop Theme
     */
    public static function add_nav_menu_item(int $menu_id, int $page_id): void
    {
        $menu_items = wp_get_nav_menu_items($menu_id);
        if (\false === $menu_items) {
            return;
        }
        if (self::has_menu_item($menu_items, $page_id)) {
            return;
        }
        wp_update_nav_menu_item($menu_id, 0, array('menu-item-title' => get_the_title($page_id), 'menu-item-object-id' => $page_id, 'menu-item-object' => 'page', 'menu-item-status' => 'publish', 'menu-item-type' => 'post_type'));
    }
    /**
     * Removes a page from a menu
     *
     * @param int $menu_id Menu WP ID.
     * @param int $page_id Page WP ID.
     *
     * @used Northmill Booking
     * @used Northmill Webshop Theme
     */
    public static function remove_nav_menu_item(int $menu_id, int $page_id): void
    {
        $menu_items = wp_get_nav_menu_items($menu_id);
        if (\false === $menu_items) {
            return;
        }
        foreach ($menu_items as $menu_item) {
            if ($menu_item->object_id !== $page_id) {
                continue;
            }
            wp_delete_post($menu_item->ID);
        }
    }
    /**
     * Checks if a page nav item exists in a menu
     *
     * @param array $menu_items The menu items.
     * @param int   $page_id    The page ID.
     *
     * @return bool
     */
    public static function has_menu_item(array $menu_items, int $page_id): bool
    {
        if (empty($menu_items)) {
            return \false;
        }
        foreach ($menu_items as $menu_item) {
            if (is_a($menu_item, 'WP_Post') && (int) $menu_item->object_id === $page_id) {
                return \true;
            }
        }
        return \false;
    }
    /**
     * Imports image to WordPress if it doesn't exist.
     *
     * @param string            $mf_id    Unique mf id for this attachment.
     * @param string            $filename Filename of the image (as provided by upstream).
     * @param string            $url      URL to the image.
     * @param Abstract_API_Base $api      API instance.
     *
     * @return int|false Attachment Id, false on failure
     *
     * @used Northmill Express Order
     * @used Northmill Booking
     */
    public static function import_image(string $mf_id, string $filename, string $url, Abstract_API_Base $api): false|int
    {
        $attachment_id = self::get_post_id($mf_id, 'attachment');
        if ($attachment_id) {
            return $attachment_id;
        }
        // Determine and validate extension.
        $ext_from_url_path = '';
        $url_path = wp_parse_url($url, \PHP_URL_PATH);
        if (is_string($url_path) && '' !== $url_path) {
            $ext_from_url_path = pathinfo($url_path, \PATHINFO_EXTENSION);
        }
        $ext = strtolower('' !== $ext_from_url_path ? $ext_from_url_path : pathinfo((string) $filename, \PATHINFO_EXTENSION));
        $allowed_ext = array('jpg', 'jpeg', 'png', 'gif', 'webp', 'heic', 'heif', 'jfif', 'pjpeg', 'pjp', 'bmp');
        if (!in_array($ext, $allowed_ext, \true)) {
            Abstract_Shared_Base::base_instance()->logger->log('sync', 'Cannot import image of unknown file extension', array('url' => $url), Log_Level_Enum::ERROR);
            return \false;
        }
        Abstract_Shared_Base::base_instance()->logger->log('sync', 'Start import of new image', array('url' => $url));
        $image_data = $api->get_image_file($url);
        if (!$image_data) {
            Abstract_Shared_Base::base_instance()->logger->log('sync', 'Error during import of image. Could not be fetched.', array('url' => $url), Log_Level_Enum::ERROR);
            return \false;
        }
        // Core includes.
        require_once \ABSPATH . 'wp-admin/includes/media.php';
        require_once \ABSPATH . 'wp-admin/includes/file.php';
        require_once \ABSPATH . 'wp-admin/includes/image.php';
        // Safe, unique temp file.
        $base_name = $filename;
        if ('' === $base_name) {
            $base_name = is_string($url_path) ? basename($url_path) : '';
        }
        $sanitized_name = sanitize_file_name($base_name);
        if ('' === $sanitized_name) {
            $sanitized_name = 'image-' . wp_generate_password(8, \false) . '.' . $ext;
        }
        $tmp_path = wp_tempnam($sanitized_name);
        if (!$tmp_path || !is_string($tmp_path)) {
            Abstract_Shared_Base::base_instance()->logger->log('sync', 'Failed to allocate a temporary file for image import.', array('suggested_name' => $sanitized_name), Log_Level_Enum::ERROR);
            return \false;
        }
        $file_array = array('tmp_name' => $tmp_path, 'name' => $sanitized_name);
        $stored = File_System_Helper::put_contents($file_array['tmp_name'], $image_data);
        if (!$stored) {
            Abstract_Shared_Base::base_instance()->logger->log('sync', 'Error during import of image. Image could not be written to temp folder.', array('url' => $url), Log_Level_Enum::ERROR);
            if (file_exists($file_array['tmp_name'])) {
                wp_delete_file($file_array['tmp_name']);
            }
            return \false;
        }
        // ---- BMP -> JPEG (HARD FAIL).
        if ('bmp' === $ext) {
            $converted = self::convert_bmp_to_jpeg($file_array['tmp_name'], $file_array['name']);
            if (is_wp_error($converted)) {
                Abstract_Shared_Base::base_instance()->logger->log('sync', 'BMP conversion failed. Aborting import.', array('error' => $converted->get_error_message(), 'url' => $url), Log_Level_Enum::ERROR);
                if (file_exists($file_array['tmp_name'])) {
                    wp_delete_file($file_array['tmp_name']);
                }
                return \false;
            }
            $file_array['tmp_name'] = $converted['tmp_name'];
            $file_array['name'] = $converted['name'];
            $ext = 'jpg';
        }
        // ---- HEIC/HEIF -> JPEG (HARD FAIL).
        if (in_array($ext, array('heic', 'heif'), \true)) {
            $converted = self::convert_heic_to_jpeg($file_array['tmp_name'], $file_array['name']);
            if (is_wp_error($converted)) {
                Abstract_Shared_Base::base_instance()->logger->log('sync', 'HEIC/HEIF conversion failed. Aborting import.', array('error' => $converted->get_error_message(), 'url' => $url), Log_Level_Enum::ERROR);
                if (file_exists($file_array['tmp_name'])) {
                    wp_delete_file($file_array['tmp_name']);
                }
                return \false;
            }
            $file_array['tmp_name'] = $converted['tmp_name'];
            $file_array['name'] = $converted['name'];
            $ext = 'jpg';
        }
        // ---- JFIF/PJPEG/PJP -> JPEG (HARD FAIL).
        if (in_array($ext, array('jfif', 'pjpeg', 'pjp'), \true)) {
            $normalized = self::normalize_jpeg_variant_to_jpg($file_array['tmp_name'], $file_array['name']);
            if (is_wp_error($normalized)) {
                Abstract_Shared_Base::base_instance()->logger->log('sync', 'JPEG variant normalization failed. Aborting import.', array('error' => $normalized->get_error_message(), 'url' => $url), Log_Level_Enum::ERROR);
                if (file_exists($file_array['tmp_name'])) {
                    wp_delete_file($file_array['tmp_name']);
                }
                return \false;
            }
            $file_array['tmp_name'] = $normalized['tmp_name'];
            $file_array['name'] = $normalized['name'];
            $ext = 'jpg';
        }
        // ---- WEBP: convert to JPEG (no alpha) or PNG (alpha). HARD FAIL on any issue.
        if ('webp' === $ext) {
            $converted = self::convert_webp_to_raster($file_array['tmp_name'], $file_array['name']);
            if (is_wp_error($converted)) {
                Abstract_Shared_Base::base_instance()->logger->log('sync', 'WEBP conversion failed. Aborting import.', array('error' => $converted->get_error_message(), 'url' => $url), Log_Level_Enum::ERROR);
                if (file_exists($file_array['tmp_name'])) {
                    wp_delete_file($file_array['tmp_name']);
                }
                return \false;
            }
            $file_array['tmp_name'] = $converted['tmp_name'];
            $file_array['name'] = $converted['name'];
            $ext = $converted['ext'];
        }
        // Hand off to WP — moves file to uploads and generates metadata/thumbnails.
        $attachment_id = media_handle_sideload($file_array);
        if (is_wp_error($attachment_id)) {
            Abstract_Shared_Base::base_instance()->logger->log('sync', 'Error during import of image.', array('error' => $attachment_id, 'url' => $url), Log_Level_Enum::ERROR);
            // Best effort cleanup of remaining temp file.
            if (is_readable($file_array['tmp_name'])) {
                wp_delete_file($file_array['tmp_name']);
            }
            return \false;
        }
        self::update_meta($attachment_id, 'mf_id', $mf_id);
        Abstract_Shared_Base::base_instance()->logger->log('sync', 'Successfully downloaded image.', array('hub_id' => $mf_id, 'filename' => $file_array['name'], 'url' => $url, 'stored_as_ext' => $ext));
        return $attachment_id;
    }
    /**
     * Convert a temporary BMP file to a temporary JPEG file (HARD FAIL on error).
     * Deletes the original BMP temp file on success.
     *
     * @param string $tmp_path  Existing temp file path (BMP).
     * @param string $orig_name Original filename (used for building a nicer new name).
     * @return array|WP_Error { tmp_name: string, name: string } or WP_Error on failure.
     */
    private static function convert_bmp_to_jpeg(string $tmp_path, string $orig_name): WP_Error|array
    {
        $editor = wp_get_image_editor($tmp_path);
        if (is_wp_error($editor)) {
            return $editor;
        }
        if (method_exists($editor, 'set_quality')) {
            $editor->set_quality(90);
        }
        $new_name = preg_replace('/\.bmp$/i', '.jpg', $orig_name);
        if (!is_string($new_name) || $new_name === $orig_name) {
            $new_name = $orig_name . '.jpg';
        }
        $new_path = wp_tempnam(sanitize_file_name($new_name));
        if (!$new_path || !is_string($new_path)) {
            return new WP_Error('bmp_temp_alloc_failed', 'Failed to allocate temporary JPG file for BMP conversion.');
        }
        $saved = $editor->save($new_path, 'image/jpeg');
        if (is_wp_error($saved) || empty($saved['path'])) {
            if (file_exists($new_path)) {
                wp_delete_file($new_path);
            }
            return is_wp_error($saved) ? $saved : new WP_Error('bmp_save_failed', 'Failed saving BMP as JPEG.');
        }
        if (file_exists($tmp_path)) {
            wp_delete_file($tmp_path);
        }
        Abstract_Shared_Base::base_instance()->logger->log('sync', 'BMP converted to JPEG.', array('new_tmp' => $saved['path']));
        return array('tmp_name' => $saved['path'], 'name' => basename($new_name));
    }
    /**
     * Normalize JPEG variants (.jfif, .pjpeg, .pjp) to a temporary .jpg file (HARD FAIL on error).
     * Deletes the original temp file on success.
     *
     * @param string $tmp_path  Existing temp file path.
     * @param string $orig_name Original filename.
     * @return array|WP_Error { tmp_name: string, name: string } or WP_Error on failure.
     */
    private static function normalize_jpeg_variant_to_jpg(string $tmp_path, string $orig_name): WP_Error|array
    {
        $editor = wp_get_image_editor($tmp_path);
        if (is_wp_error($editor)) {
            return $editor;
        }
        if (method_exists($editor, 'set_quality')) {
            $editor->set_quality(90);
        }
        $new_name = preg_replace('/\.(jfif|pjpeg|pjp)$/i', '.jpg', $orig_name);
        if (!is_string($new_name) || $new_name === $orig_name) {
            $new_name = $orig_name . '.jpg';
        }
        $new_path = wp_tempnam(sanitize_file_name($new_name));
        if (!$new_path || !is_string($new_path)) {
            return new WP_Error('jpg_norm_temp_alloc_failed', 'Failed to allocate temporary JPG file for JPEG normalization.');
        }
        $saved = $editor->save($new_path, 'image/jpeg');
        if (is_wp_error($saved) || empty($saved['path'])) {
            if (file_exists($new_path)) {
                wp_delete_file($new_path);
            }
            return is_wp_error($saved) ? $saved : new WP_Error('jpg_norm_save_failed', 'Failed normalizing JPEG variant to JPG.');
        }
        if (file_exists($tmp_path)) {
            wp_delete_file($tmp_path);
        }
        Abstract_Shared_Base::base_instance()->logger->log('sync', 'JPEG variant normalized to JPG.', array('new_tmp' => $saved['path']));
        return array('tmp_name' => $saved['path'], 'name' => basename($new_name));
    }
    /**
     * Convert a temporary WEBP file to JPEG (if no alpha) or PNG (if alpha).
     * HARD FAIL on error. Deletes original temp on success.
     *
     * @param string $tmp_path  Existing temp file path (WEBP).
     * @param string $orig_name Original filename (used to build new name).
     * @return array|WP_Error { tmp_name: string, name: string, ext: string } or WP_Error on failure.
     */
    private static function convert_webp_to_raster(string $tmp_path, string $orig_name): WP_Error|array
    {
        // First ensure WordPress can open it via its editor.
        $editor = wp_get_image_editor($tmp_path);
        if (is_wp_error($editor)) {
            return $editor;
        }
        $has_alpha = self::detect_webp_alpha($tmp_path);
        // Pick target format and output name.
        $target_ext = $has_alpha ? 'png' : 'jpg';
        $pattern = '/\.webp$/i';
        $new_name = preg_replace($pattern, '.' . $target_ext, $orig_name);
        if (!is_string($new_name) || $new_name === $orig_name) {
            $new_name = $orig_name . '.' . $target_ext;
        }
        $new_path = wp_tempnam(sanitize_file_name($new_name));
        if (!$new_path || !is_string($new_path)) {
            return new WP_Error('webp_temp_alloc_failed', 'Failed to allocate temporary file for WEBP conversion.');
        }
        // Set quality: WP maps this to PNG compression as well.
        if (method_exists($editor, 'set_quality')) {
            $editor->set_quality(90);
        }
        $mime = $has_alpha ? 'image/png' : 'image/jpeg';
        $saved = $editor->save($new_path, $mime);
        if (is_wp_error($saved) || empty($saved['path'])) {
            if (file_exists($new_path)) {
                wp_delete_file($new_path);
            }
            return is_wp_error($saved) ? $saved : new WP_Error('webp_save_failed', 'Failed converting WEBP to raster format.');
        }
        // Remove original WEBP temp.
        if (file_exists($tmp_path)) {
            wp_delete_file($tmp_path);
        }
        Abstract_Shared_Base::base_instance()->logger->log('sync', 'WEBP converted to ' . strtoupper($target_ext) . '.', array('new_tmp' => $saved['path'], 'alpha' => $has_alpha));
        return array('tmp_name' => $saved['path'], 'name' => basename($new_name), 'ext' => $target_ext);
    }
    /**
     * Best-effort detection of alpha in WEBP.
     * Uses Imagick if available; otherwise conservatively assumes alpha may exist and returns true
     * to preserve transparency by choosing PNG (safer default over losing alpha).
     *
     * @param string $tmp_path Path to WEBP file.
     * @return bool True if alpha likely present, false otherwise.
     */
    private static function detect_webp_alpha(string $tmp_path): bool
    {
        // Prefer Imagick for reliable alpha detection.
        if (class_exists('Imagick')) {
            try {
                $img = new Imagick();
                $img->readImage($tmp_path);
                // If multiple frames exist, treat as possibly animated with alpha somewhere → prefer PNG.
                if ($img->getNumberImages() > 1) {
                    return \true;
                }
                // getImageAlphaChannel() returns an int mode; >0 means alpha is used/enabled.
                if (method_exists($img, 'getImageAlphaChannel')) {
                    return (bool) $img->getImageAlphaChannel();
                }
                // Fallback: check channel depth for alpha when method not available.
                if (method_exists($img, 'getImageChannelDepth') && defined('Imagick::CHANNEL_ALPHA')) {
                    return $img->getImageChannelDepth(Imagick::CHANNEL_ALPHA) > 1;
                }
            } catch (Exception) {
                // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
                // If Imagick throws, fall through to conservative fallback below.
            }
        }
        // Without trustworthy detection, be conservative: assume alpha may exist to avoid losing transparency.
        return \true;
    }
    /**
     * Convert a temporary HEIC/HEIF file to a temporary JPEG file (HARD FAIL on error).
     * Deletes the original temp file on success.
     *
     * @param string $tmp_path  Existing temp file path (HEIC/HEIF).
     * @param string $orig_name Original filename (used for building a nicer new name).
     * @return array|WP_Error { tmp_name: string, name: string } or WP_Error on failure.
     */
    private static function convert_heic_to_jpeg(string $tmp_path, string $orig_name): WP_Error|array
    {
        $editor = wp_get_image_editor($tmp_path);
        // Requires Imagick with libheif.
        if (is_wp_error($editor)) {
            return $editor;
        }
        if (method_exists($editor, 'set_quality')) {
            $editor->set_quality(90);
        }
        $new_name = preg_replace('/\.(heic|heif)$/i', '.jpg', $orig_name);
        if (!is_string($new_name) || $new_name === $orig_name) {
            $new_name = $orig_name . '.jpg';
        }
        $new_path = wp_tempnam(sanitize_file_name($new_name));
        if (!$new_path || !is_string($new_path)) {
            return new WP_Error('heic_temp_alloc_failed', 'Failed to allocate temporary JPG file for HEIC/HEIF conversion.');
        }
        $saved = $editor->save($new_path, 'image/jpeg');
        if (is_wp_error($saved) || empty($saved['path'])) {
            if (file_exists($new_path)) {
                wp_delete_file($new_path);
            }
            return is_wp_error($saved) ? $saved : new WP_Error('heic_save_failed', 'Failed saving HEIC/HEIF as JPEG.');
        }
        if (file_exists($tmp_path)) {
            wp_delete_file($tmp_path);
        }
        Abstract_Shared_Base::base_instance()->logger->log('sync', 'HEIC/HEIF converted to JPEG.', array('new_tmp' => $saved['path']));
        return array('tmp_name' => $saved['path'], 'name' => basename($new_name));
    }
}
