<?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-preparativi.php
 * FROM →: PK_Inexpress plugin (original)
 * TO →: PK_Inexpress plugin (annotated)
 * DESCRIPTION: English-only file header template followed by the class that prepares XML data
 *
 * Note: This header is intentionally in English only as requested.
 */

/* EN: Base class for preparing property import data from an XML source.
 * EN: It provides XML loading/parsing utilities, extraction of unique property IDs,
 * EN: and helper getters that compute which items to import, update, or delete.
 *
 * IT: Classe base per preparare i dati di importazione degli immobili da una sorgente XML.
 * IT: Fornisce utility per il caricamento/parsing dell'XML, l'estrazione degli ID univoci degli immobili,
 * IT: e metodi helper che calcolano quali elementi importare, aggiornare o eliminare.
 */
abstract class PKINEX_Preparer
{

    // EN: The raw source of XML data. It can be a URL or the raw XML string.
    // IT: Sorgente grezza dei dati XML. Può essere un URL o la stringa XML raw.
    /** @var string Contenuto XML (URL o raw) */
    protected $source_data;

    // EN: The loaded SimpleXMLElement instance after parsing the XML source.
    // IT: L'istanza SimpleXMLElement caricata dopo il parsing della sorgente XML.
    /** @var SimpleXMLElement|null XML caricato */
    protected $data;

    // EN: The name of the XML field/tag used as the unique identifier for properties.
    // IT: Nome del campo/tag XML usato come identificatore univoco per gli immobili.
    /** @var string Nome del campo XML da usare come ID univoco */
    protected $id_name;

    // EN: List of IDs found in the XML source (strings).
    // IT: Lista degli ID trovati nel file XML (stringhe).
    /** @var string[] ID trovati nell'XML */
    protected $list_id_data = [];

    // EN: List of IDs already existing in WordPress (strings).
    // IT: Lista degli ID già presenti in WordPress (stringhe).
    /** @var string[] ID già presenti in WP */
    protected $list_id_existing = [];

    // EN: Computed list of IDs that should be imported (present in XML, not in WP).
    // IT: Lista calcolata degli ID da importare (presenti nell'XML, non in WP).
    // lista degli id presenti in wp quindi già caricati
    protected $list_id_import = [];

    // EN: Computed list of IDs that should be updated (present in both XML and WP).
    // IT: Lista calcolata degli ID da aggiornare (presenti sia nell'XML che in WP).
    // lista degli id presenti in wp quindi già caricati
    protected $list_id_update = [];

    // EN: Computed list of IDs that should be deleted (present in WP but not in XML).
    // IT: Lista calcolata degli ID da eliminare (presenti in WP ma non nell'XML).
    // lista degli id presenti in wp quindi già caricati
    protected $list_id_delete = [];

    // EN: Source type indicator: 'url' (default) or 'text' for raw XML.
    // IT: Indicatore del tipo di sorgente: 'url' (default) o 'text' per XML raw.
    protected $source_type;


    /**
     * EN: Constructor — sets up source, id field and source type.
     * EN: It also immediately attempts to load and parse the XML and populate ID lists.
     * EN: Note: performing IO (HTTP & DB) inside a constructor is convenient but affects testability;
     * EN: consider refactoring to a separate prepare() method if desired.
     *
     * IT: Costruttore — imposta la sorgente, il campo ID e il tipo di sorgente.
     * IT: Tenta inoltre di caricare e parsare immediatamente l'XML e popolare le liste di ID.
     * IT: Nota: eseguire IO (HTTP & DB) nel costruttore è comodo ma riduce la testabilità;
     * IT: valuta di spostare queste operazioni in un metodo prepare() separato se necessario.
     *
     * @param string $source_data URL or raw XML content
     * @param string $id_name name of the XML field used as unique id
     * @param string $source_type 'url' or 'text' (default 'url')
     */
    public function __construct(string $source_data, string $id_name, string $source_type = 'url')
    {
        $this->source_data = $source_data;
        $this->id_name     = $id_name;
        $this->source_type = $source_type;

        libxml_use_internal_errors(true);

        try {
            $this->pkinex_load_data();
            $this->pkinex_get_id_data();

            $this->pkinex_get_id_wppost();
        } finally {
            libxml_clear_errors();
            libxml_use_internal_errors(false);
        }
    }


    /**
     * EN: Load and parse XML from the configured source (URL or raw text).
     * EN: Throws an Exception if the source is empty or parsing fails.
     *
     * IT: Carica e parsifica l'XML dalla sorgente configurata (URL o testo).
     * IT: Lancia un'Exception se la sorgente è vuota o il parsing fallisce.
     *
     * @throws Exception if source is empty or parsing fails
     */
    private function pkinex_load_data(): void
    {
        // EN: Quick validation: ensure the source_data property is not empty.
        // IT: Validazione rapida: assicurati che la proprietà source_data non sia vuota.
        if (empty($this->source_data)) {
            throw new Exception('Fonte XML non valida o vuota.');
        }

        // EN: Decide which loader to use based on source_type ('text' uses raw string loader).
        // IT: Decide quale loader usare in base a source_type ('text' usa il loader da stringa).
        if ($this->source_type === 'text') {
            $this->pkinex_load_text_data();
        } else {
            $this->pkinex_load_url_data();
        }

        // EN: Validate that parsing produced a SimpleXMLElement instance.
        // IT: Verifica che il parsing abbia prodotto un'istanza SimpleXMLElement valida.
        if (! $this->data instanceof SimpleXMLElement) {
            throw new Exception('Parsing XML fallito.');
        }
    }


    /**
     * EN: Load and parse XML from a remote URL using the WP HTTP API.
     * EN: LIBXML_NONET is used to mitigate XXE (disables network entity loading).
     *
     * IT: Carica e parsifica l'XML da un URL remoto usando la WP HTTP API.
     * IT: LIBXML_NONET è usato per mitigare XXE (disabilita il caricamento di entità di rete).
     *
     * @throws Exception on HTTP or parsing failure
     */
    private function pkinex_load_url_data(): void
    {
        // EN: Basic guard: if source_data is empty refuse immediately.
        // IT: Controllo di base: se source_data è vuoto rifiuta immediatamente.
        if (empty($this->source_data)) {
            throw new Exception('Nessuna fonte XML fornita (né URL né contenuto).');
        }

        // EN: Perform HTTP GET with reasonable timeouts and headers.
        // IT: Esegue una richiesta HTTP GET con timeout e header ragionevoli.
        $response = wp_remote_get($this->source_data, [
            'timeout'      => 30,
            'redirection'  => 5,
            'httpversion'  => '1.1',
            'headers'      => ['Accept' => 'application/xml, text/xml'],
        ]);

        // EN: If WP returns an error object, convert to exception with message.
        // IT: Se WP restituisce un oggetto errore, convertilo in eccezione con il messaggio.
        if (is_wp_error($response)) {
            throw new Exception(
                esc_html__(
                    'HTTP error: ',
                    'pk-inexpress'
                ) . esc_html($response->get_error_message())
            );
        }


        // EN: Check for a 200 HTTP status; otherwise raise.
        // IT: Controlla lo status HTTP 200; altrimenti lancia eccezione.
        // IT: Controlla lo status HTTP 200; altrimenti lancia eccezione.
        $status = wp_remote_retrieve_response_code($response);
        if (200 !== intval($status)) {
            throw new Exception(
                sprintf(
                    /* translators: %d: HTTP status code */
                    esc_html__('Error: HTTP response %d from XML server.', 'pk-inexpress'),
                    intval($status)
                )
            );
        }

        // EN: Retrieve the response body and ensure it's not empty.
        // IT: Recupera il corpo della risposta e verifica che non sia vuoto.
        $xml_body = wp_remote_retrieve_body($response);
        if (empty($xml_body)) {
            throw new Exception(
                esc_html__('Empty or failed XML download.', 'pk-inexpress')
            );
        }


        // EN: Use simplexml_load_string with LIBXML_NONET to parse XML safely.
        // IT: Usa simplexml_load_string con LIBXML_NONET per parsare l'XML in modo sicuro.
        $this->data = simplexml_load_string($xml_body, 'SimpleXMLElement', LIBXML_NONET);

        if (false === $this->data) {
            // EN: Collect all libxml errors
            // IT: Recupera tutti gli errori di libxml
            $errors = libxml_get_errors();
            libxml_clear_errors();

            // EN: Build a plain error message
            // IT: Costruisce un messaggio di errore pulito
            $error_messages = array_map(function ($e) {
                return $e->message;
            }, $errors);

            // EN: Throw exception with all error messages escape.
            // IT: Lancia eccezione con tutti i messaggi di errore escapati
            $error_messages_sanitized = array_map('esc_html', $error_messages);
            $message = 'Parsing XML fallito: ' . implode('; ', $error_messages_sanitized);

            // Optionally, wrap for output later with wp_die() or esc_html() when displaying
            // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
            throw new Exception($message);
        }
    }

    /**
     * EN: Load and parse XML provided as a raw string (for example from a textarea).
     * EN: LIBXML_NONET is always used to avoid XXE attacks.
     *
     * IT: Carica e parsifica XML fornito come stringa (es. da textarea).
     * IT: LIBXML_NONET è sempre usato per evitare attacchi XXE.
     *
     * @return bool True on success, false on failure. | True se ok, false in caso di errore.
     */
    private function pkinex_load_text_data(): bool
    {

        if (empty($this->source_data)) {
            throw new Exception('Nessuna fonte XML fornita.');
        }

        if (strpos(trim($this->source_data), '<') === false) {
            throw new Exception('Il testo fornito non sembra contenere XML valido.');
        }
        // EN: Guard against empty source_data.
        // IT: Controllo contro source_data vuoto.
        if (empty($this->source_data)) {
            $this->show_admin_notice('Nessuna fonte XML fornita (né URL né contenuto).');
            return false;
        }

        // EN: Quick sanity check: raw content should contain at least a '<' character for XML.
        // IT: Controllo di sanità: il contenuto dovrebbe contenere almeno un '<' per essere XML.
        if (strpos(trim($this->source_data), '<') === false) {
            $this->show_admin_notice('Il testo fornito non sembra contenere XML valido.');
            return false;
        }

        // EN: Enable libxml internal error handling for safe parsing.
        // IT: Abilita la gestione interna degli errori libxml per il parsing sicuro.
        libxml_use_internal_errors(true);

        // EN: Parse the provided XML string with LIBXML_NONET.
        // IT: Parsea la stringa XML fornita con LIBXML_NONET.
        $this->data = simplexml_load_string($this->source_data, 'SimpleXMLElement', LIBXML_NONET);

        // EN: On failure, collect and show all errors to the admin.
        // IT: In caso di fallimento, raccoglie e mostra tutti gli errori all'admin.
        if ($this->data === false) {
            $errors = libxml_get_errors();
            libxml_clear_errors();

            $error_messages = [];
            foreach ($errors as $error) {
                $error_messages[] = trim($error->message);
            }

            set_transient(
                'pkinex_xml_text_error',
                __('ERROR MESSAGE. The data is formatted incorrectly. Check the XML text for missing fields or other errors.', 'pk-inexpress'),
                30
            );


            libxml_use_internal_errors(false);
            return false;
        }

        // EN: Restore libxml error handling to default.
        // IT: Ripristina la gestione errori di libxml al comportamento predefinito.
        libxml_use_internal_errors(false);

        return true;
    }
    /**
     * EN: Return the parsed SimpleXMLElement instance or null if none loaded.
     * IT: Restituisce l'istanza SimpleXMLElement parsata o null se non caricata.
     *
     * @return SimpleXMLElement|null
     */
    public function pkinex_get_data_ready(): ?SimpleXMLElement
    {
        // EN: Simple getter to expose the parsed XML to external callers.
        // IT: Getter semplice per esporre l'XML parsato a chiamanti esterni.
        return $this->data;
    }

    /**
     * EN: Extract unique property IDs from the loaded XML.
     * EN: This method currently expects either a root <immobili><immobile>... structure
     * EN: or the older <immobile>... structure at the top level. Override in subclasses if needed.
     *
     * IT: Estrae gli ID univoci degli immobili dall'XML caricato.
     * IT: Questo metodo si aspetta attualmente o una struttura radice <immobili><immobile>...
     * IT: o la vecchia struttura <immobile>... a livello top. Sovrascrivere nelle sottoclassi se necessario.
     *
     * @return array List of IDs extracted from XML
     */
    private function pkinex_get_id_data(): array
    {
        // EN: Prepare an empty list to collect IDs.
        // IT: Prepara una lista vuota per raccogliere gli ID.
        $list_id = [];
        // EN: If no XML data is loaded, return an empty array immediately.
        // IT: Se non è stato caricato alcun XML, ritorna subito array vuoto.
        if (!$this->data) {
            return $list_id;
        }

        // EN: If XML has <immobili><immobile> nodes, iterate that path.
        // IT: Se l'XML ha nodi <immobili><immobile>, iterali via quel percorso.
        if (isset($this->data->immobili)) {
            foreach ($this->data->immobili->immobile as $immobile) {
                // EN: Cast value to string to ensure consistent types in arrays.
                // IT: Cast a string per garantire tipi coerenti negli array.
                $list_id[] = (string) $immobile->{$this->id_name};
            }
        } else if (isset($this->data->Annuncio)) {
            foreach ($this->data->Annuncio as $annuncio) {
                // EN: Cast value to string to ensure consistent types in arrays.
                // IT: Cast a string per garantire tipi coerenti negli array.
                $list_id[] = (string) $annuncio->{$this->id_name};
            }
        } else {
            // EN: Fallback to older top-level <immobile> nodes if present.
            // IT: Fallback a vecchio percorso top-level <immobile> se presente.
            foreach ($this->data->immobile as $immobile) {
                $list_id[] = (string) $immobile->{$this->id_name};
            }
        }

        // EN: Store the discovered IDs into the instance property for later use.
        // IT: Memorizza gli ID scoperti nella proprietà dell'istanza per uso successivo.
        $this->list_id_data = $list_id;
        return $this->list_id_data;
    }

    /**
     * EN: Populate $this->list_id_existing with all meta values for the configured meta key.
     * EN: It queries the WP database for posts of type 'property' and status 'publish'.
     *
     * IT: Popola $this->list_id_existing con tutti i valori meta per la chiave configurata.
     * IT: Interroga il DB di WP per i post di tipo 'property' e status 'publish'.
     *
     * Notes:
     * - sanitize meta_key, use transient cache to reduce DB load,
     * - exclude empty meta_value rows, normalize/trim results and remove duplicates.
     *
     * @return array List of existing IDs from WP postmeta
     */
    private function pkinex_get_id_wppost(): array
    {
        // --- EN: Retrieve existing property 'codice' values using WP functions (no direct DB queries, no transient).
        // --- IT: Recupera i valori esistenti del campo 'codice' degli immobili usando funzioni WP (senza query dirette al DB, senza transient).

        global $wpdb;

        // --- EN: Normalize and sanitize the meta_key before using it.
        // --- IT: Normalizza e sanitizza la meta_key prima di usarla.
        $meta_key = trim(sanitize_text_field($this->id_name));
        $meta_key = substr($meta_key, 0, 191); // safe length for meta_key

        // --- EN: Get all published property IDs that have this meta key.
        // --- IT: Recupera tutti gli ID dei post 'property' pubblicati che hanno questo meta key.
        $args = [
            'post_type'      => 'property',
            'post_status'    => 'publish',
            'posts_per_page' => -1,
            'fields'         => 'ids', // retrieve only IDs
            'meta_key'       => $meta_key,
            'meta_value'     => '',     // optional: exclude empty
            'meta_compare'   => '!=',   // ensure meta_value is not empty
        ];
        $post_ids = get_posts($args);

        // --- EN: Loop through IDs to get the 'codice' meta values.
        // --- IT: Cicla sugli ID per recuperare i valori del campo 'codice'.
        $codici = [];
        foreach ($post_ids as $post_id) {
            $val = get_post_meta($post_id, $meta_key, true);
            $val = trim((string) $val);
            if ($val !== '') {
                $codici[] = $val;
            }
        }

        // --- EN: Remove duplicates and reindex array.
        // --- IT: Rimuove duplicati e reindicizza l'array.
        $codici = array_values(array_unique($codici));

        // --- EN: Store the results in the instance.
        // --- IT: Salva il risultato nell'istanza.
        $this->list_id_existing = $codici;

        return $this->list_id_existing;
    }


    /**
     * EN: Return IDs to import (present in XML but not in WP) and store them in the object.
     *
     * IT: Restituisce gli ID da importare (presenti nell'XML ma non in WP) e li memorizza nell'oggetto.
     *
     * @return string[] List of IDs to import
     */
    public function pkinex_get_id_import(): array
    {
        // EN: Normalize both source arrays: cast to array and remove duplicates, then reindex.
        // IT: Normalizza entrambi gli array: cast ad array, rimuovi duplicati e reindicizza.
        $xmlIds = array_values(array_unique((array) $this->list_id_data));
        $wpIds  = array_values(array_unique((array) $this->list_id_existing));

        // EN: Compute the difference (XML minus WP) — these are new items to import.
        // IT: Calcola la differenza (XML meno WP) — questi sono i nuovi elementi da importare.
        $this->list_id_import = array_values(array_diff($xmlIds, $wpIds));


        // EN: Return the computed list and keep it stored in the object for later use.
        // IT: Restituisce la lista calcolata e la conserva nell'oggetto per uso successivo.
        return $this->list_id_import;
    }

    /**
     * EN: Return IDs to update (present both in XML and in WP) and store them in the object.
     *
     * IT: Restituisce gli ID da aggiornare (presenti sia nell'XML che in WP) e li memorizza nell'oggetto.
     *quello che fa e confrontare gli id presenti in wp e quelli ancora presenti nel xml quindi prende solo gli id presenti in entrambi le liste (non fa xml + wp)
     * @return string[] List of IDs to update
     */
    public function pkinex_get_id_update(): array
    {
        // EN: Normalize arrays to avoid issues with nulls or duplicates.
        // IT: Normalizza gli array per evitare problemi con null o duplicati.
        $xmlIds = array_values(array_unique((array) $this->list_id_data));
        $wpIds  = array_values(array_unique((array) $this->list_id_existing));

        // EN: Intersection yields IDs present in both sets — these should be updated.
        // IT: L'intersezione fornisce gli ID presenti in entrambi gli insiemi — questi vanno aggiornati.
        $this->list_id_update = array_values(array_intersect($xmlIds, $wpIds));


        // DEBUG LOG (temporaneo)
        /*  error_log(
            '[PKINEX FREE] GET_ID_UPDATE: ' .
                'xml=' . count($xmlIds) .
                ' wp=' . count($wpIds) .
                ' update=' . count($this->list_id_update) .
                ' update_ids=' . json_encode($this->list_id_update)
        ); */
        return $this->list_id_update;
    }

    /**
     * EN: Return IDs to delete (present in WP but not in XML) and store them in the object.
     *
     * IT: Restituisce gli ID da eliminare (presenti in WP ma non nell'XML) e li memorizza nell'oggetto.
     *
     * @return string[] List of IDs to delete
     */
    public function pkinex_get_id_delete(): array
    {
        // EN: Normalize arrays to avoid issues with nulls or duplicates.
        // IT: Normalizza gli array per evitare problemi con null o duplicati.
        $xmlIds = array_values(array_unique((array) $this->list_id_data));
        $wpIds  = array_values(array_unique((array) $this->list_id_existing));

        // EN: Difference (WP minus XML) yields IDs that exist in WP but are not present in the latest XML.
        // IT: La differenza (WP meno XML) fornisce gli ID che esistono in WP ma non nel XML più recente.
        $this->list_id_delete = array_values(array_diff($wpIds, $xmlIds));

        return $this->list_id_delete;
    }


    /**
  
     * @throws Exception se ci sono errori durante il caricamento XML o nelle fasi successive
     */
}
