<?php
// EN: If this file is called directly, abort.
// IT: Se questo file viene chiamato direttamente, interrompi.
if (! defined('ABSPATH')) exit; // Exit
/**
 * FILE: class-pk-inex-managerprocessor.php (modified)
 * FROM → PKInexpress plugin (original)
 * TO → PKInexpress plugin (hardened, bilingual comments)
 * DESCRIPTION:
 * EN: Core manager for import/update/delete/taxonomy/image handling. Replaces direct cURL
 * EN: uses with WP HTTP API wrapper, sanitizes inputs, limits logging to WP_DEBUG,
 * EN: fixes delete iteration bug and makes taxonomy assignment more robust.
 * IT: Gestore centrale per import/aggiornamento/eliminazione/tassonomie/immagini.
 * IT: Sostituisce l'uso diretto di cURL con la HTTP API di WP tramite un wrapper,
 * IT: sanitizza gli input, limita i log a WP_DEBUG, corregge il ciclo di delete e rende
 * IT: l'assegnazione delle tassonomie più robusta.
 */

if (! defined('ABSPATH')) {
    exit; // EN: Exit if accessed directly / IT: Uscire se accesso diretto
}

abstract class PKINEX_ManagerProcessor
{
    /**
     * EN: Stores plugin options array.
     * IT: Memorizza l'array delle opzioni del plugin.
     *
     * @var array
     */
    protected $options = [];

    /**
     * EN: Constructor - optionally loads options from the database.
     * IT: Costruttore - opzionalmente carica le opzioni dal database.
     *
     * @param bool $load_options Whether to load options immediately / Se caricare le opzioni subito.
     */
    public function __construct($load_options = true)
    {
        if ($load_options) {
            $this->options = get_option('pkinex_options', []);
        }
    }

    // =======================
    // REMOTE CHECK WRAPPER
    // =======================

    /**
     * EN: Check remote URL availability and return HTTP status code or WP_Error.
     * EN: Uses wp_remote_head with fallback to wp_remote_get. Logs only when WP_DEBUG is enabled.
     * IT: Verifica la raggiungibilità di un URL remoto e restituisce il codice HTTP o WP_Error.
     * IT: Usa wp_remote_head con fallback a wp_remote_get. Logga solo se WP_DEBUG è attivo.
     *
     * @param string $url
     * @param int    $timeout
     * @return int|WP_Error HTTP status code or WP_Error
     */
    protected function pkinex_check_remote_url(string $url, int $timeout = 8)
    {
        $url = esc_url_raw(trim($url));
        if (empty($url)) {
            return new WP_Error('pkinex_invalid_url', __('Invalid URL', 'pk-inexpress'));
        }

        // Try HEAD first (lightweight)
        $args = [
            'timeout'     => $timeout,
            'redirection' => 5,
            'httpversion' => '1.1',
            'headers'     => ['Accept' => 'image/*, application/xml, text/xml, */*'],
        ];

        $response = wp_remote_head($url, $args);

        if (is_wp_error($response)) {
            // Some servers don't support HEAD properly, fallback to GET
            $response = wp_remote_get($url, array_merge($args, ['timeout' => $timeout + 2]));
            if (is_wp_error($response)) {
                return $response; // WP_Error
            }
        }

        $code = wp_remote_retrieve_response_code($response);
        return (int) $code;
    }

    // =======================
    // CHECK TERMS BASE
    // =======================

    /**
     * EN: Ensure base taxonomy terms exist (e.g., Sale/Rent for properties).
     * IT: Verifica e crea i termini di base (es. Vendita/Affitto per immobili).
     *
     * @param string $taxonomy The taxonomy slug / Lo slug della tassonomia.
     * @param array  $terms    Array of terms in 'slug' => 'name' format / Array di termini in formato 'slug' => 'nome'.
     * @return void
     */
    protected function pkinex_check_base_terms(string $taxonomy, array $terms): void
    {
        foreach ($terms as $slug => $name) {
            if (! term_exists($slug, $taxonomy)) {
                wp_insert_term($name, $taxonomy, ['slug' => sanitize_title($slug)]);
            }
        }
    }

    // =======================
    // OPTIONS MANAGEMENT
    // =======================

    /**
     * EN: Update post meta with type-based sanitization and error logging.
     * IT: Aggiorna i meta del post con sanitizzazione basata sul tipo e logging degli errori.
     *
     * @param int    $post_id  EN: Post ID / IT: ID del post
     * @param string $meta_key EN: Meta key to update / IT: Chiave meta da aggiornare
     * @param mixed  $value    EN: Meta value to save / IT: Valore meta da salvare
     * @param string $type     EN: Expected type ('string', 'int', 'bool', 'float') / IT: Tipo atteso ('string', 'int', 'bool', 'float')
     * @return bool            EN: True on success, false on failure / IT: True in caso di successo, false altrimenti
     */
    protected function pkinex_update_post_meta(int $post_id, string $meta_key, $value, string $type = 'string'): bool
    {
        if ($post_id <= 0 || empty($meta_key)) {
            return false;
        }

        switch ($type) {
            case 'string':
                if (! is_string($value)) {

                    return false;
                }
                $value = sanitize_text_field($value);
                break;

            case 'int':
                if (! is_numeric($value)) {
                    return false;
                }
                $value = intval($value);
                break;

            case 'bool':
                $value = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
                if ($value === null) {
                    return false;
                }
                break;

            case 'float':
                if (! is_numeric($value)) {
                    return false;
                }
                $value = floatval($value);
                break;

            default:
                return false;
        }

        return update_post_meta($post_id, $meta_key, $value);
    }

    // =======================
    // TAXONOMY MANAGEMENT
    // =======================

    /**
     * EN: Assigns taxonomy terms to a post. Accepts string (comma separated), array or single string.
     * IT: Assegna termini di tassonomia a un post. Accetta stringa (CSV), array o singolo valore.
     *
     * @param int          $post_id Post ID / ID del post.
     * @param string       $taxonomy Taxonomy name / Nome della tassonomia.
     * @param string|array $terms Terms to assign / Termini da assegnare.
     * @return void
     */
    protected function pkinex_assign_taxonomy_terms(int $post_id, string $taxonomy, $terms): void
    {
        // Normalize terms into array
        if (is_array($terms)) {
            $terms_arr = $terms;
        } else {
            $terms_str = (string) $terms;
            if (strpos($terms_str, ',') !== false) {
                $terms_arr = array_map('trim', explode(',', $terms_str));
            } else {
                $terms_arr = [trim($terms_str)];
            }
        }

        // Filter and sanitize
        $terms_arr = array_values(array_filter(array_map('strval', $terms_arr)));
        if (empty($terms_arr)) {
            return;
        }

        // Ensure terms exist and assign
        foreach ($terms_arr as $term) {
            $term = sanitize_text_field($term);
            if (! term_exists($term, $taxonomy)) {
                wp_insert_term($term, $taxonomy, ['slug' => sanitize_title($term)]);
            }
        }

        wp_set_object_terms($post_id, $terms_arr, $taxonomy, false);
    }

    // =======================
    // ADD IMAGES
    // =======================

    /**
     * EN: Downloads and attaches property images to a given post.
     * IT: Scarica e allega immagini di una proprietà a un post specificato.
     *
     * @param array $images_urls Array of image URLs.
     * @param int   $post_id     Post ID where images will be attached.
     * @return array             Array with 'ids' (attachment IDs) and 'names' (image file names).
     */
    protected function pkinex_attach_images(array $images_urls, int $post_id): array
    {
        if (! function_exists('download_url')) {
            require_once ABSPATH . 'wp-admin/includes/file.php';
        }
        if (! function_exists('media_handle_sideload')) {
            require_once ABSPATH . 'wp-admin/includes/media.php';
        }
        if (! function_exists('wp_generate_attachment_metadata')) {
            require_once ABSPATH . 'wp-admin/includes/image.php';
        }

        $attached_image_ids = [];
        $attached_image_names = [];
        $is_featured_set = has_post_thumbnail($post_id);

        foreach ($images_urls as $foto_url_raw) {
            $foto_url = esc_url_raw((string) $foto_url_raw);
            // normalize double slashes while keeping protocol
            $foto_url = preg_replace('#(?<!:)//+#', '/', $foto_url);

            if (empty($foto_url)) {
                continue;
            }

            $code_or_error = $this->pkinex_check_remote_url($foto_url, 8);
            if (is_wp_error($code_or_error)) {

                continue;
            }

            $http_code = (int) $code_or_error;
            if ($http_code < 200 || $http_code >= 400) {

                continue;
            }

            $tmp = download_url($foto_url);
            if (is_wp_error($tmp)) {

                continue;
            }

            $file_name = basename($foto_url);
            $file_array = [
                'name'     => $file_name,
                'tmp_name' => $tmp,
            ];


            /** 
             * Consente al Pro di modificare il file temporaneo (es. ridurre l'originale)
             * prima di consegnarlo a media_handle_sideload().
             * @param array  $file_array ['name','tmp_name']
             * @param int    $post_id
             * @param string $foto_url
             */
            $file_array = apply_filters('pkinex/pre_media_sideload_file_array', $file_array, $post_id, $foto_url);

            $image_id = media_handle_sideload($file_array, $post_id);

            if (is_wp_error($image_id)) {
                wp_delete_file($tmp);
                continue;
            }

            // 🔴 QUI: Salviamo l’URL sorgente dell’immagine originaria dell'XML
            update_post_meta($image_id, '_pkinex_src_url', $foto_url);

            add_post_meta($post_id, 'fave_property_images', $image_id);
            $attached_image_ids[] = $image_id;
            $attached_image_names[] = $file_name;

            if (! $is_featured_set) {
                set_post_thumbnail($post_id, $image_id);
                $is_featured_set = true;
            }
        }

        return [
            'ids'   => $attached_image_ids,
            'names' => $attached_image_names,
        ];
    }

    // =======================
    // ADD FLOOR PLAN
    // =======================

    /**
     * EN: Adds a floor plan image URL as post meta if reachable.
     * IT: Aggiunge l'URL della planimetria come meta del post se raggiungibile.
     *
     * @param int    $post_id       Post ID where to save the meta.
     * @param string $floorplan_url URL of the floorplan to add.
     * @return bool True if added, false otherwise.
     */
    protected function pkinex_add_floor_plan(int $post_id, string $floorplan_url): bool
    {
        $floorplan_url = esc_url_raw(trim($floorplan_url));
        $floorplan_url = preg_replace('#(?<!:)//+#', '/', $floorplan_url);
        if (empty($floorplan_url)) {
            return false;
        }

        $code_or_error = $this->pkinex_check_remote_url($floorplan_url, 5);
        if (is_wp_error($code_or_error)) {
            return false;
        }

        $http_code = (int) $code_or_error;
        if ($http_code >= 200 && $http_code < 400) {
            $floor_plan = [['fave_plan_image' => $floorplan_url]];
            update_post_meta($post_id, 'floor_plans', $floor_plan);
            return true;
        }
        return false;
    }

    // =======================
    // ADD AGENT
    // =======================

    /**
     * EN: Assigns a related post (e.g., real estate agent) by name or default fallback.
     * IT: Assegna un post correlato (es. agente) tramite nome o fallback.
     */
    protected function pkinex_assign_agent_post_by_name(
        int $post_id,
        string $related_post_type,
        string $search_name,
        string $meta_key,
        int $default_id
    ): bool {
        $search_name = trim($search_name);
        if (empty($search_name)) {
            return false;
        }
        // Normalize the search name: lowercase + remove accents
        $normalized_search = $this->pkinex_normalize_string($search_name);
        // Use a small query and then exact compare titles 
        $args = [
            'post_type'      => $related_post_type,
            'posts_per_page' => 5,
            's'              => $search_name, //qui passo il nome originale che puo essere senza accento
            'post_status'    => 'publish',
        ];

        $query = new WP_Query($args);
        $related_id = null;

        if ($query->have_posts()) {
            foreach ($query->posts as $post) {
                $normalized_title = $this->pkinex_normalize_string($post->post_title);
                if (strcasecmp($normalized_title, $normalized_search) === 0) {
                    $related_id = $post->ID;
                    break;
                }
            }
        }
        wp_reset_postdata();

        if (! $related_id) {
            $related_id = $default_id;
        }

        update_post_meta($post_id, $meta_key, $related_id);
        return true;
    }

    /**
     * Normalize a string by lowercasing and removing accents.
     * Example: "Giuseppe Macrì" → "giuseppe macri"
     *
     * @param string $string
     * @return string
     */
    protected function pkinex_normalize_string(string $string): string
    {
        // Convert to lowercase
        $string = mb_strtolower($string, 'UTF-8');

        // Replace accented characters with plain equivalents
        $string = strtr($string, [
            'à' => 'a',
            'á' => 'a',
            'â' => 'a',
            'ä' => 'a',
            'ã' => 'a',
            'å' => 'a',
            'è' => 'e',
            'é' => 'e',
            'ê' => 'e',
            'ë' => 'e',
            'ì' => 'i',
            'í' => 'i',
            'î' => 'i',
            'ï' => 'i',
            'ò' => 'o',
            'ó' => 'o',
            'ô' => 'o',
            'ö' => 'o',
            'õ' => 'o',
            'ù' => 'u',
            'ú' => 'u',
            'û' => 'u',
            'ü' => 'u',
            'ç' => 'c',
            'ñ' => 'n',
            'ý' => 'y',
            'ÿ' => 'y',
            'œ' => 'oe',
            'æ' => 'ae',
        ]);

        // Remove any leftover non-alphanumeric characters (optional)
        $string = preg_replace('/[^a-z0-9\s]/u', '', $string);

        // Trim extra spaces
        return trim($string);
    }


    /**
     * EN: Find agent CPT by external numeric line_id (postmeta pkinex_line_id) and assign to property.
     * IT: Trova il CPT agente tramite line_id numerico (postmeta pkinex_line_id) e assegnalo alla property.
     */
    protected function pkinex_find_agent_by_line_id(int $post_id, $line_id, int $default_agent_id): void
    {
        $raw_in = (string) $line_id;
        $raw    = preg_replace('/\D+/', '', $raw_in) ?? '';
        $norm   = ltrim($raw, '0');
        if ($norm === '') {
            $norm = '0';
        }


        if ($raw === '') {
            $agent_id = (int) $default_agent_id;
        } else {
            $q = new WP_Query([
                'post_type'        => 'houzez_agent',
                'post_status'      => ['publish', 'draft', 'pending', 'private'],
                'posts_per_page'   => 1,
                'fields'           => 'ids',
                'no_found_rows'    => true,
                'meta_query'       => [
                    'relation' => 'OR',
                    ['key' => 'fave_agent_line_id', 'value' => $raw],
                    ['key' => 'fave_agent_line_id', 'value' => $norm],
                ],
            ]);

            /*  if (defined('SAVEQUERIES') && SAVEQUERIES) {
            } */

            $agent_id = !empty($q->posts) ? (int) $q->posts[0] : (int) $default_agent_id;
            wp_reset_postdata();
        }

        // Allineati a REAL: solo fave_agents (scalare) + display option, e pulizia del vecchio meta singolo
        delete_post_meta($post_id, 'fave_property_agent'); // evita conflitti se scritto in passato

        if ($agent_id > 0) {
            update_post_meta($post_id, 'fave_agents', (string) (int) $agent_id); // SCALARE
            update_post_meta($post_id, 'fave_agent_display_option', 'agent_info');
        } else {
            delete_post_meta($post_id, 'fave_agents');
            delete_post_meta($post_id, 'fave_agent_display_option');
        }
    }





    // =======================
    // ADD FIELD CUSTOM - CAMPI TITOLO VALORE PERSONALIZZATI
    // =======================
    /**
     * EN: Add or update an additional detail (Houzez: fave_additional_features).
     * IT: Aggiunge o aggiorna un dettaglio aggiuntivo (Houzez: fave_additional_features).
     *
     * @param int    $post_id Property post ID
     * @param string $title   Label shown in Additional Details
     * @param string $value   Value shown in Additional Details
     */
    protected function pkinex_add_additional_detail(int $post_id, string $title, string $value): void
    {
        $title = sanitize_text_field($title);
        $value = sanitize_text_field($value);
        if ($post_id <= 0 || $title === '' || $value === '') return;

        // Leggi lo schema Houzez corretto
        $rows = get_post_meta($post_id, 'additional_features', true);
        if (!is_array($rows)) $rows = [];

        // Cerca (case-insensitive) una riga con lo stesso titolo
        $found = false;
        foreach ($rows as &$row) {
            $t = isset($row['fave_additional_feature_title']) ? (string)$row['fave_additional_feature_title'] : '';
            if (mb_strtolower($t) === mb_strtolower($title)) {
                $row['fave_additional_feature_value'] = $value; // aggiorna solo il valore
                $found = true;
                break;
            }
        }
        unset($row);

        // Se non esiste, aggiungi una nuova riga
        if (!$found) {
            $rows[] = [
                'fave_additional_feature_title' => $title,
                'fave_additional_feature_value' => $value,
            ];
        }

        // Salva con la meta key giusta per Houzez
        update_post_meta($post_id, 'additional_features', array_values($rows));
    }


    /**
     * EN: Remove one Additional Detail by its title (Houzez: fave_additional_features).
     * IT: Rimuove un singolo Dettaglio Aggiuntivo dal titolo (Houzez: fave_additional_features).
     */
    protected function pkinex_remove_additional_detail(int $post_id, string $title): void
    {
        $title = sanitize_text_field($title);
        if ($post_id <= 0 || $title === '') return;

        $details = get_post_meta($post_id, 'fave_additional_features', true);
        if (! is_array($details) || empty($details)) return;

        $new = [];
        foreach ($details as $row) {
            $t = isset($row['title']) ? (string) $row['title'] : '';
            if (mb_strtolower($t) !== mb_strtolower($title)) {
                $new[] = $row;
            }
        }

        if (empty($new)) {
            delete_post_meta($post_id, 'fave_additional_features');
        } else {
            update_post_meta($post_id, 'fave_additional_features', $new);
        }
    }

    /**
     * EN: Save a labeled meta field (Title => Value) on the property.
     * IT: Salva un campo meta etichettato (Titolo => Valore) sull’immobile.
     */
    protected function pkinex_save_meta_field(int $post_id, string $label, string $value): void
    {
        $label = sanitize_text_field($label);
        $value = sanitize_text_field($value);
        if ($post_id <= 0 || $label === '' || $value === '') return;

        // Meta key derivata dal titolo (uniforme, no spazi)
        $meta_key = 'pkinex_' . sanitize_key($label); // es: pkinex_scheda_grado
        update_post_meta($post_id, $meta_key, $value);

        // (Facoltativo) salva anche un array aggregato di tutte le specifiche
        $all = get_post_meta($post_id, 'pkinex_specs', true);
        if (!is_array($all)) $all = [];
        $all[$label] = $value;
        update_post_meta($post_id, 'pkinex_specs', $all);
    }


    // =======================
    // ADD FEATURE
    // =======================

    /**
     * EN: Add a single feature term to a post taxonomy.
     * IT: Aggiunge un singolo termine di feature alla tassonomia di un post.
     */
    protected function pkinex_add_single_feature(int $post_id, string $feature_name, string $taxonomy = 'property_feature'): void
    {
        $feature_name = trim($feature_name);
        if (empty($feature_name)) {
            return;
        }

        $term = term_exists($feature_name, $taxonomy);
        if ($term === 0 || $term === null) {
            wp_insert_term($feature_name, $taxonomy);
        }
        wp_set_object_terms($post_id, [$feature_name], $taxonomy, true);
    }

    // =======================
    // ADD VIRTUAL TOUR
    // =======================

    /**
     * EN: Adds a virtual tour iframe to the property post meta.
     * IT: Aggiunge un iframe per il virtual tour come meta del post.
     */
    protected function pkinex_add_virtual_tour(int $post_id, string $virtual_tour_url): void
    {
        $virtual_tour_url = trim($virtual_tour_url);
        if (empty($virtual_tour_url)) {
            return;
        }

        $iframe_html = sprintf(
            '<iframe width="853" height="480" src="%s" frameborder="0" allowfullscreen></iframe>',
            esc_url($virtual_tour_url)
        );

        update_post_meta($post_id, 'fave_virtual_tour', $iframe_html);
    }

    // =======================
    // UPDATE TAXONOMY
    // =======================
    /**
     * EN: Assign taxonomy if changed or missing
     * IT: Assegna la tassonomia se mancante o diversa
     */
    protected function pkinex_update_taxonomy(int $post_id, string $tax, string $term): void
    {
        if (empty($term)) return;
        $current = wp_get_object_terms($post_id, $tax, ['fields' => 'slugs']);
        if (empty($current) || $current[0] !== $term) {
            if (! term_exists($term, $tax)) {
                wp_insert_term($term, $tax);
            }
            wp_set_object_terms($post_id, [$term], $tax, false);
        }
    }

    // =======================
// UPDATE IMAGES / GALLERY
// =======================

    /**
     * EN: Sync gallery using original XML URLs stored in attachment meta
     *     (_pkinex_src_url). Adds only truly new images and removes those no
     *     longer present in the feed, without re-downloading everything.
     *
     * IT: Sincronizza la galleria usando gli URL XML originali salvati nel meta
     *     degli allegati (_pkinex_src_url). Aggiunge solo le immagini davvero
     *     nuove e rimuove quelle non più presenti nel feed, senza riscaricare
     *     tutto ogni volta.
     */
    protected function pkinex_update_gallery(int $post_id, SimpleXMLElement $immobile): void
    {
        // EN: Ensure WP's media/download helpers are loaded (needed in cron/CLI contexts).
        // IT: Assicura che gli helper media/download di WP siano caricati (necessari in cron/CLI).

        // Per download_url()
        if (! function_exists('download_url')) {
            require_once ABSPATH . 'wp-admin/includes/file.php';
        }

        // Per media_handle_sideload() e gestione immagini
        if (! function_exists('media_handle_sideload')) {
            require_once ABSPATH . 'wp-admin/includes/media.php';
            require_once ABSPATH . 'wp-admin/includes/image.php';
        }

        // 1) Raccogli gli URL dal feed XML
        $newUrls = [];
        if (! empty($immobile->ElencoFoto->Foto)) {
            foreach ($immobile->ElencoFoto->Foto as $foto) {
                $url = trim((string) $foto);
                if ($url !== '') {
                    $newUrls[] = esc_url_raw($url);
                }
            }
        }

        // Se il feed non ha immagini, per sicurezza NON tocchiamo la galleria esistente
        if (empty($newUrls)) {
            return;
        }

        // Normalizza: rimuovi duplicati
        $newUrls = array_values(array_unique($newUrls));

        // 2) Leggi gli attachment attuali associati all'immobile
        $existingIds = get_post_meta($post_id, 'fave_property_images', false); // array di ID

        // Mappa URL sorgente XML → ID attachment
        $existingBySrc = []; // [ src_url => attachment_id ]
        foreach ($existingIds as $img_id) {
            $img_id = (int) $img_id;
            if ($img_id <= 0) {
                continue;
            }

            // EN: original XML URL stored at import time
            // IT: URL XML originale salvato in fase di import
            $src = get_post_meta($img_id, '_pkinex_src_url', true);
            $src = is_string($src) ? trim($src) : '';

            if ($src === '') {
                // Caso legacy: nessun meta sorgente → non sappiamo agganciarlo
                // Per ora lo ignoriamo nel matching; potrà essere sostituito da nuove immagini.
                continue;
            }

            $existingBySrc[$src] = $img_id;
        }

        // 3) Quali URL del feed sono già coperti da attachment esistenti?
        $keepIds = [];   // ID da tenere
        $usedSrc = [];   // URL sorgente già abbinati
        foreach ($newUrls as $url) {
            if (isset($existingBySrc[$url])) {
                $keepIds[] = $existingBySrc[$url];
                $usedSrc[] = $url;
            }
        }

        // 4) Determina quali attachment vanno rimossi dalla galleria
        //    (quelli che NON sono in $keepIds)
        $keepIdsMap = array_fill_keys(array_map('intval', $keepIds), true);
        $toRemove   = [];

        foreach ($existingIds as $img_id) {
            $img_id = (int) $img_id;
            if ($img_id <= 0) {
                continue;
            }

            if (! isset($keepIdsMap[$img_id])) {
                // EN: Remove only from meta, do not delete the physical file (safety).
                // IT: Rimuove solo dal meta, non cancella il file fisico (per sicurezza).
                $toRemove[] = $img_id;
            }
        }

        foreach ($toRemove as $img_id) {
            delete_post_meta($post_id, 'fave_property_images', $img_id);
        }

        // 5) Determina quali URL del feed sono veramente "nuovi"
        $usedSrcMap = array_fill_keys($usedSrc, true);
        $toAddUrls  = [];

        foreach ($newUrls as $url) {
            if (! isset($usedSrcMap[$url])) {
                $toAddUrls[] = $url;
            }
        }

        // Se non ci sono URL nuovi, abbiamo finito: niente download, niente lavoro pesante
        if (empty($toAddUrls)) {
            return;
        }

        // 6) Scarica solo le nuove immagini mancanti
        foreach ($toAddUrls as $url) {
            $code_or_error = $this->pkinex_check_remote_url($url, 8);
            if (is_wp_error($code_or_error)) {
                continue;
            }

            $code = (int) $code_or_error;
            if ($code < 200 || $code >= 400) {
                continue;
            }

            $tmp = download_url($url);
            if (is_wp_error($tmp)) {
                continue;
            }

            // Hook Pro per eventuali ottimizzazioni / resize
            $tmp = apply_filters('pkinex_before_image_save', $tmp, $url, $post_id);

            $url_parts = wp_parse_url($url);
            $path      = $url_parts['path'] ?? '';

            $filename = basename($path !== '' ? $path : $url);

            $file = [
                'name'     => sanitize_file_name($filename),
                'tmp_name' => $tmp,
            ];


            $att = media_handle_sideload($file, $post_id);
            if (is_wp_error($att)) {
                wp_delete_file($tmp);
                continue;
            }

            // Collega l'immagine al post
            add_post_meta($post_id, 'fave_property_images', $att);

            // 🔑 Salva l'URL sorgente XML sull'attachment, per futuri confronti leggeri
            update_post_meta($att, '_pkinex_src_url', $url);

            // Imposta l'immagine in evidenza se non c'è già
            if (! has_post_thumbnail($post_id)) {
                set_post_thumbnail($post_id, $att);
            }
        }
    }


    
    // =======================
    // UPDATE IMG GENERIC - GENERICO PER TUTTI I GESTIONALI PASSARE ANCHE REALSMART IN FUTURO SU QUESTA FUNZIONE
    // =======================

    /**
     * EN: Generic smart gallery updater based on a normalized photos array.
     * IT: Update generico “intelligente” della galleria basato su un array normalizzato di foto.
     *
     * $photos[] = array(
     *   'url'         => (string) full image URL,
     *   'role'        => 'image' | 'plan' | '360',
     *   'order'       => (int) sort index (0,1,2...),
     *   'is_featured' => (bool) true if this should be the featured image
     * );
     */
    protected function pkinex_update_gallery_generic(int $post_id, array $photos): void
    {
        if (empty($photos)) {
            return;
        }

        // EN: Ensure media helpers are loaded (safe for cron/CLI).
        // IT: Assicura che gli helper media siano caricati (sicuro per cron/CLI).
        if (! function_exists('download_url')) {
            require_once ABSPATH . 'wp-admin/includes/file.php';
        }
        if (! function_exists('media_handle_sideload')) {
            require_once ABSPATH . 'wp-admin/includes/media.php';
        }
        if (! function_exists('wp_generate_attachment_metadata')) {
            require_once ABSPATH . 'wp-admin/includes/image.php';
        }

        // 1) Filter only “image” role and sort by order
        // 1) Filtra solo le immagini “image” e ordina per order
        $imageItems = array();
        foreach ($photos as $item) {
            if (! is_array($item)) {
                continue;
            }
            $role = isset($item['role']) ? strtolower((string) $item['role']) : 'image';
            if ($role !== 'image') {
                continue;
            }

            $url = isset($item['url']) ? trim((string) $item['url']) : '';
            if ($url === '') {
                continue;
            }

            $imageItems[] = array(
                'url'         => esc_url_raw($url),
                'order'       => isset($item['order']) ? (int) $item['order'] : 0,
                'is_featured' => ! empty($item['is_featured']),
            );
        }

        if (empty($imageItems)) {
            // Nessuna immagine “image” → non tocchiamo la galleria
            return;
        }

        usort($imageItems, static function ($a, $b) {
            return $a['order'] <=> $b['order'];
        });

        // 2) Build ordered URL list
        // 2) Costruisci la lista ordinata di URL
        $newUrls = array();
        $featuredCandidateUrl = null;

        foreach ($imageItems as $item) {
            $newUrls[] = $item['url'];

            if ($item['is_featured'] && $featuredCandidateUrl === null) {
                $featuredCandidateUrl = $item['url'];
            }
        }

        $newUrls = array_values(array_unique($newUrls));

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

        // Se nessuna marcata come featured, candidiamo la prima.
        if ($featuredCandidateUrl === null) {
            $featuredCandidateUrl = $newUrls[0];
        }

        // 3) Read existing attachments
        // 3) Leggi gli attachment esistenti
        $existingIds = get_post_meta($post_id, 'fave_property_images', false);

        $existingBySrc = array(); // [ src_url => attachment_id ]
        foreach ($existingIds as $img_id) {
            $img_id = (int) $img_id;
            if ($img_id <= 0) {
                continue;
            }

            $src = get_post_meta($img_id, '_pkinex_src_url', true);
            $src = is_string($src) ? trim($src) : '';

            if ($src === '') {
                continue;
            }

            $existingBySrc[$src] = $img_id;
        }

        // 4) Determine which attachments to keep
        // 4) Determina quali attachment tenere
        $keepIds = array();
        $usedSrc = array();

        foreach ($newUrls as $url) {
            if (isset($existingBySrc[$url])) {
                $keepIds[] = (int) $existingBySrc[$url];
                $usedSrc[] = $url;
            }
        }

        $keepIdsMap = array_fill_keys($keepIds, true);
        $toRemove   = array();

        foreach ($existingIds as $img_id) {
            $img_id = (int) $img_id;
            if ($img_id <= 0) {
                continue;
            }

            if (! isset($keepIdsMap[$img_id])) {
                $toRemove[] = $img_id;
            }
        }

        foreach ($toRemove as $img_id) {
            delete_post_meta($post_id, 'fave_property_images', $img_id);
        }

        // 5) Which URLs are really new?
        // 5) Quali URL sono davvero nuovi?
        $usedSrcMap = array_fill_keys($usedSrc, true);
        $toAddUrls  = array();

        foreach ($newUrls as $url) {
            if (! isset($usedSrcMap[$url])) {
                $toAddUrls[] = $url;
            }
        }

        // 6) Download only new URLs
        //    Keep map for featured calculation
        // 6) Scarica solo i nuovi URL
        $createdBySrc = array();

        foreach ($toAddUrls as $url) {
            $code_or_error = $this->pkinex_check_remote_url($url, 8);
            if (is_wp_error($code_or_error)) {
                continue;
            }

            $code = (int) $code_or_error;
            if ($code < 200 || $code >= 400) {
                continue;
            }

            $tmp = download_url($url);
            if (is_wp_error($tmp)) {
                continue;
            }

            $tmp = apply_filters('pkinex_before_image_save', $tmp, $url, $post_id);

            $url_parts = wp_parse_url($url);

            $path = '';
            if (is_array($url_parts) && ! empty($url_parts['path'])) {
                $path = $url_parts['path'];
            }

            $name = basename($path !== '' ? $path : $url);
            $name = sanitize_file_name($name);

            $file = array(
                'name'     => $name,
                'tmp_name' => $tmp,
            );


            $att = media_handle_sideload($file, $post_id);
            if (is_wp_error($att)) {
                wp_delete_file($tmp);
                continue;
            }

            add_post_meta($post_id, 'fave_property_images', $att);
            update_post_meta($att, '_pkinex_src_url', $url);

            $createdBySrc[$url] = (int) $att;
        }

        // 7) Sync featured image with featuredCandidateUrl
        // 7) Allinea la featured image con featuredCandidateUrl
        $firstAttId = 0;

        if (isset($existingBySrc[$featuredCandidateUrl])) {
            $firstAttId = (int) $existingBySrc[$featuredCandidateUrl];
        } elseif (isset($createdBySrc[$featuredCandidateUrl])) {
            $firstAttId = (int) $createdBySrc[$featuredCandidateUrl];
        }

        if ($firstAttId > 0) {
            $currentThumb = (int) get_post_thumbnail_id($post_id);
            if ($currentThumb !== $firstAttId) {
                set_post_thumbnail($post_id, $firstAttId);
            }
        }
    }


    // =======================
    // UPDATE FLOOR PLAN
    // =======================

    /**
     * EN: Update floorplan image if different
     * IT: Aggiorna l'immagine della planimetria se differente
     */
    protected function pkinex_update_plan(int $post_id, string $newUrl): void
    {
        $newUrl = esc_url_raw($newUrl);
        if (empty($newUrl)) return;

        $code_or_error = $this->pkinex_check_remote_url($newUrl, 8);
        if (is_wp_error($code_or_error)) {

            return;
        }

        $code = (int) $code_or_error;
        if ($code < 200 || $code >= 400) {

            return;
        }

        $old = get_post_meta($post_id, 'floor_plans', true);
        $oldUrl = ! empty($old[0]['fave_plan_image']) ? $old[0]['fave_plan_image'] : '';

        if ($newUrl !== $oldUrl) {
            $data = [['fave_plan_image' => $newUrl]];
            update_post_meta($post_id, 'floor_plans', $data);
        }
    }

  // =======================
    // GET MAP - CREA UNA MAPPA DEGLI ID DEI POST ABBINATI AL CODICE IMMOBILE
    // =======================
    /**
     * EN: Build a map "external code (meta_value) → post_id" for a given post type and meta key.
     * IT: Crea una mappa "codice esterno (meta_value) → post_id" per uno specifico post type e meta key.
     *
     * @param string $post_type Post type to search (e.g. 'property')
     * @param string $meta_key  Meta key that stores the external ID (e.g. 'pkinex_codice')
     * @return array            Map like [ 'CODICE1' => 123, 'CODICE2' => 456 ]
     */
    protected function pkinex_build_postid_map_by_meta(string $post_type, string $meta_key): array
    {
        global $wpdb;

        // EN: Sanitize meta key and keep safe length.
        // IT: Sanitizza la meta key e limita la lunghezza.
        $meta_key = trim(sanitize_text_field($meta_key));
        $meta_key = substr($meta_key, 0, 191);

        // EN: Build a cache key for this post_type + meta_key combination.
        // IT: Costruisce una chiave di cache per la combinazione post_type + meta_key.
        $cache_key = 'pkinex_postid_map_' . md5($post_type . '|' . $meta_key);
        $cached    = wp_cache_get($cache_key, 'pkinex');

        if (false !== $cached && is_array($cached)) {
            // EN: Return cached map if available.
            // IT: Restituisce la mappa dalla cache se disponibile.
            return $cached;
        }

        // EN: Prepare SQL to get all (meta_value, post_id) pairs.
        // IT: Prepara la query SQL per ottenere tutte le coppie (meta_value, post_id).
        $sql = $wpdb->prepare(
            "
        SELECT pm.meta_value, pm.post_id
        FROM {$wpdb->postmeta} AS pm
        INNER JOIN {$wpdb->posts} AS p
            ON p.ID = pm.post_id
        WHERE p.post_type   = %s
          AND p.post_status = 'publish'
          AND pm.meta_key   = %s
          AND pm.meta_value <> ''
        ",
            $post_type,
            $meta_key
        );

        // EN: Direct DB query is intentional here for performance
        //     (building a map for the importer) and is safely prepared + cached.
        // IT: La query diretta è intenzionale per motivi di performance
        //     (costruzione della mappa per l'importer) ed è preparata + messa in cache.
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
        $rows = $wpdb->get_results($sql, ARRAY_A);

        $map = [];

        if (! empty($rows)) {
            foreach ($rows as $row) {
                // EN: Keep codes as strings to avoid int/string mismatch.
                // IT: Mantiene i codici come stringhe per evitare mismatch int/string.
                $code   = trim((string) $row['meta_value']);
                $postId = (int) $row['post_id'];

                if ($code === '' || $postId <= 0) {
                    continue;
                }

                $map[$code] = $postId;
            }
        }

        // EN: Store result in object cache for reuse within the request / persistent cache.
        // IT: Salva il risultato nella object cache per riuso nella richiesta / cache persistente.
        wp_cache_set($cache_key, $map, 'pkinex', MINUTE_IN_SECONDS * 10);

        return $map;
    }

    // =======================
    // GET META
    // =======================

    /**
     * EN: Get post ID by meta key/value
     * IT: Recupera l’ID del post tramite chiave/valore meta
     */
    protected function pkinex_get_postid_bymeta(string $meta_key, string $meta_value)
    {
        $q = new WP_Query([
            'post_type'      => 'property',
            'posts_per_page' => 1,
            'meta_query'     => [[
                'key'   => $meta_key,
                'value' => $meta_value,
            ]]
        ]);

        if ($q->have_posts()) {
            $id = $q->posts[0]->ID;
            wp_reset_postdata();
            return $id;
        }

        return false;
    }

    // =======================
    // UPDATE META
    // =======================

    /**
     * EN: Update meta if new value differs
     * IT: Aggiorna il meta solo se il valore è cambiato
     */
    protected function pkinex_update_meta(int $post_id, string $meta_key, $new_value): void
    {
        $old = get_post_meta($post_id, $meta_key, true);
        if ($old != $new_value) {
            update_post_meta($post_id, $meta_key, $new_value);
        }
    }

    // =======================
    // DELETE OPERATIONS
    // =======================

    /**
     * EN: Delete posts by given IDs. Loops through provided list of codes and trashes matching posts.
     * IT: Elimina i post tramite gli ID forniti. Scorre la lista di codici e sposta nel cestino i post corrispondenti.
     *
     * @param string $post_type
     * @param string $meta_key
     * @param array  $list_id_delete
     * @return int Number of trashed posts
     */
    protected function pkinex_delete_old_posts(string $post_type, string $id_name, $list_id_delete): int
    {
        $deleted_count = 0;
        // Ensure we iterate values (not array keys) and sanitize each code
        foreach ((array) $list_id_delete as $codice) {
            $codice = trim((string) $codice);
            if ($codice === '') {
                continue;
            }

            $posts = get_posts([
                'post_type'      => $post_type,
                'posts_per_page' => -1,
                'fields'         => 'ids',
                'meta_query'     => [[
                    'key'   => $id_name,
                    'value' => $codice,
                ]],
            ]);

            if (empty($posts)) {
                continue;
            }

            foreach ($posts as $post_id) {
                if ('trash' !== get_post_status($post_id)) {
                    wp_trash_post($post_id);
                    $deleted_count++;
                }
            }
        }

        return $deleted_count;
    }
}
