<?php
/**
 * Main plugin class
 *
 * @since  1.0.0
 *
 * @category  WordPress_Plugin
 * @package   QrCdr
 * @author    Nicola Franchini
 * @link      https://veno.es
 */

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

use Veno\QrCdr\Qrcdr_Generator;

if ( ! class_exists( 'QrCdr_Plugin', false ) ) {
	/**
	 * QrCdr_Plugin class
	 */
	class QrCdr_Plugin {
		/**
		 * Plugin name
		 *
		 * @var string
		 */
		public $slug = 'qrcdr';
		/**
		 * Attachments taxonomy
		 *
		 * @var string
		 */
		public $taxonomy = 'qrcdr_media_origin';
		/**
		 * Holds an instance of the object
		 *
		 * @var instance
		 */
		protected static $instance = null;

		/**
		 * Counts generators on page
		 *
		 * @var number
		 */
		public static $gencounter = 0;

		/**
		 * Flag per la modalità di debug.
		 * Impostato nel costruttore.
		 *
		 * @var bool
		 */
		private static $qrcdr_debug = false;

		/**
		 * Custom upload folder
		 *
		 * @var string
		 */
		private $custom_upload_folder = null;

		/**
		 * Holds an instance of the QRcdrFn utility class.
		 *
		 * @var QRcdrFn
		 */
		public $utils = null;

		/**
		 * Returns the running object
		 *
		 * @return QrCdr_Plugin
		 */
		public static function get_instance() {
			if ( null === self::$instance ) {
				self::$instance = new self();
				self::$instance->hooks();
			}
			return self::$instance;
		}

		/**
		 * Init plugin
		 */
		private function __construct() {

			self::$qrcdr_debug = defined( 'WP_DEBUG' ) && WP_DEBUG;

			require_once dirname( __DIR__ ) . '/lib/class-qrcdr-fn.php';
			require_once dirname( __DIR__ ) . '/lib/class-qrcdr.php';

			$this->utils = QRcdrFn::getInstance();

			if ( ! function_exists( 'qrcdr' ) ) {
				/**
				 * Helper function for QRcdrFn.
				 *
				 * @return QRcdrFn
				 */
				function qrcdr() {
					return QRcdrFn::getInstance();
				}
			}
		}

		/**
		 * Initiate hooks
		 */
		public function hooks() {

			add_action( 'init', array( $this, 'register_tax' ) );
			add_action( 'init', array( $this, 'set_countrycodes' ) );
			add_action( 'init', array( $this, 'register_all_assets' ) );
			add_action( 'wp_enqueue_scripts', array( $this, 'localize_main_assets' ), 20 );

			add_action( 'init', array( $this, 'register_block' ) );
			add_action( 'enqueue_block_editor_assets', array( $this, 'init_gutenberg' ) );
			add_action( 'wp_print_styles', array( $this, 'maybe_add_qrcdr_inline_style' ) );

			// Process data.
			add_action( 'wp_ajax_qrcdr_process', array( $this, 'ajax_process' ), 10 );
			add_action( 'wp_ajax_nopriv_qrcdr_process', array( $this, 'ajax_process' ), 10 );
			// Save SVG.
			add_action( 'wp_ajax_qrcdr_svg', array( $this, 'create_svg' ) );
			add_action( 'wp_ajax_nopriv_qrcdr_svg', array( $this, 'create_svg' ) );
			// Save PNG.
			add_action( 'wp_ajax_qrcdr_png', array( $this, 'ajax_png' ) );
			add_action( 'wp_ajax_nopriv_qrcdr_png', array( $this, 'ajax_png' ) );

			add_shortcode( 'qrcdr', array( $this, 'do_shortcode' ) );

			add_action( 'pre_get_posts', array( $this, 'hide_qrcdr_media_from_library' ) );
			add_action( 'restrict_manage_posts', array( $this, 'add_media_taxonomy_filter' ) );

			// Download PDF.
			add_action( 'init', array( $this, 'register_download_rewrite_rule' ) );
			add_filter( 'query_vars', array( $this, 'register_download_query_var' ) );
			add_action( 'wp_ajax_qrcdr_download_pdf', array( $this, 'handle_pdf' ) );
			add_action( 'wp_ajax_nopriv_qrcdr_download_pdf', array( $this, 'handle_pdf' ) );

			add_action( 'qrcdr_cron_hook', array( $this, 'delete_old_qrcodes' ) );

			// Controllo di sicurezza per aggiornamenti o se il cron salta per qualche motivo.
			if ( is_admin() && ! wp_next_scheduled( 'qrcdr_cron_hook' ) ) {
				wp_schedule_event( time(), 'daily', 'qrcdr_cron_hook' );
			}

			if ( ! is_admin() ) {
				add_action( 'template_redirect', array( $this, 'intercept_pdf_download' ) );
			}
		}

		/**
		 * Register assets
		 */
		public function register_all_assets() {

			$plugin_root_url = plugin_dir_url( __DIR__ );

			$is_dev_mode = defined( 'WP_DEBUG' ) && WP_DEBUG;

			// --- BUNDLE (Frontend) ---
			$script_path = $is_dev_mode ? 'assets/js/qrcdr-plugin.dev.js' : 'assets/js/qrcdr-plugin.min.js';
			$style_path  = $is_dev_mode ? 'assets/css/qrcdr.css' : 'assets/css/qrcdr.min.css';

			wp_register_script(
				'qrcdr',
				$plugin_root_url . $script_path,
				array(), // Nessuna dipendenza esterna, è tutto nel bundle!
				QRCDR_VERSION,
				true
			);

			wp_register_style(
				'qrcdr',
				$plugin_root_url . $style_path,
				array(), // Nessuna dipendenza esterna.
				QRCDR_VERSION
			);

			// --- BUNDLE OPENLAYERS (On-Demand) ---
			$ol_script_path    = $is_dev_mode ? 'assets/js/ol-qrcdr.dev.js' : 'assets/js/ol-qrcdr.min.js';

			wp_register_script(
				'ol-qrcdr',
				$plugin_root_url . $ol_script_path,
				array(),
				QRCDR_VERSION,
				true
			);
			// NOTA: lo stile di OpenLayers (ol.css) è ora INCLUSO nel bundle 'qrcdr-main' via SASS.
		}

		/**
		 * Localizza lo script principale in modo che sia disponibile
		 * anche se richiamato come dipendenza.
		 */
		public function localize_main_assets() {
			// Verifichiamo se lo script è stato registrato (evita errori se chiamato a sproposito).
			if ( wp_script_is( 'qrcdr', 'registered' ) ) {
				// 1. Get main color and calculate variants once.
				$main_color_setting = qrcdr_settings()->get_option( 'primary_color', '#671AFF' );
				$get_colors = $this->get_color_variants( $main_color_setting );

				$localizevars = array(
					'ajax_url'        => admin_url( 'admin-ajax.php' ),
					'nonce'           => wp_create_nonce( 'qrcdr-ajax-nonce' ),
					'colors'          => array(
						'primary' => $get_colors['primary'],
						'text'    => $get_colors['text'],
					),
					'icons'           => array(
						'svg'   => qrcdr_icons()->get_the_icon( 'ui', 'file_svg' ),
						'png'   => qrcdr_icons()->get_the_icon( 'ui', 'file_png' ),
						'pdf'   => qrcdr_icons()->get_the_icon( 'ui', 'file_pdf' ),
						'print' => qrcdr_icons()->get_the_icon( 'ui', 'print' ),
					),
					'placeholder_url' => qrcdr_settings()->get_option( 'placeholder', plugin_dir_url( __DIR__ ) . 'images/placeholder.svg' ),
				);
				$final_localize_vars = apply_filters( 'qrcdr_localize_script_data', $localizevars );

				wp_localize_script( 'qrcdr', 'QRCDR', $final_localize_vars );
			}
		}

		/**
		 * Add custom CSS
		 */
		public function maybe_add_qrcdr_inline_style() {
			if ( wp_style_is( 'qrcdr', 'enqueued' ) ) {
				$custom_css = $this->generate_custom_css();

				if ( ! empty( $custom_css ) ) {
					wp_add_inline_style( 'qrcdr', $custom_css );
				}
			}
		}

		/**
		 * Generate custom CSS.
		 *
		 * @return string.
		 */
		private function generate_custom_css() {

			$default_main_color = '#671AFF';

			// 1. Border Radius.
			$get_border_radius = qrcdr_settings()->get_option( 'border_radius', 0 );
			$border_radius_value = floatval( $get_border_radius ); // Converte a numero, scartando qualsiasi altra cosa.
			$border_radius = $border_radius_value . 'px';

			// 2. Colors.
			$maincolor = qrcdr_settings()->get_option( 'primary_color', $default_main_color );
			$maincolor = sanitize_hex_color( $maincolor );
			if ( empty( $maincolor ) ) {
				$maincolor = $default_main_color;
			}

			$get_colors = $this->get_color_variants( $maincolor );
			$c_primary = sanitize_hex_color( $get_colors['primary'] );
			$c_primary_rgb = sanitize_text_field( $get_colors['primary-rgb'] );
			if ( ! preg_match( '/^\d{1,3},\d{1,3},\d{1,3}$/', $c_primary_rgb ) ) {
				$c_primary_rgb = '0,0,0';
			}

			$c_dark    = sanitize_hex_color( $get_colors['primary-dark'] );
			$c_text    = sanitize_hex_color( $get_colors['text'] );
			$c_link    = sanitize_hex_color( $get_colors['link'] );

			$custom_css = ":root{ --qrcdr-primary: {$c_primary}; --qrcdr-primary-rgb: {$c_primary_rgb};  --qrcdr-primary-dark: {$c_dark}; --qrcdr-text: {$c_text}; --qrcdr-link: {$c_link}; --qrcdr-border-radius: {$border_radius}; }";

			return $custom_css;
		}

		/**
		 * Operazioni da eseguire alla disattivazione (pulizia)
		 */
		public static function deactivate() {
			$timestamp = wp_next_scheduled( 'qrcdr_cron_hook' );
			if ( $timestamp ) {
				wp_unschedule_event( $timestamp, 'qrcdr_cron_hook' );
			}
		}

		/**
		 * Delete old qrcodes
		 */
		public function delete_old_qrcodes() {
			$lifetime = qrcdr_settings()->get_option( 'delete_old', '1 day ago' );

			if ( empty( $lifetime ) ) {
				$lifetime = '1 day ago';
			}

			$batch_size = 200; // Elimina 200 allegati per ciclo. Un valore sicuro.

			$args = array(
				'post_type'      => 'attachment',
				'post_status'    => 'inherit',
				'post_parent'    => 0,
				'date_query'     => array(
					'before' => $lifetime,
				),
				'posts_per_page' => $batch_size,
				// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query 
				'tax_query'      => array(
					array(
						'taxonomy' => $this->taxonomy,
						'field'    => 'slug',
						'terms'    => array( 'qrcdr-download' ),
					),
				),
				'fields' => 'ids',
			);

			// Continua a ciclare finché get_posts() trova allegati da cancellare.
			while ( ( $old_attachments = get_posts( $args ) ) ) {
				foreach ( $old_attachments as $attachment_id ) {
					wp_delete_attachment( $attachment_id, true );
				}

				// Previene loop infiniti in caso di problemi.
				if ( count( $old_attachments ) < $batch_size ) {
					break;
				}
			}
		}

		/**
		 * Set country codes
		 */
		public function set_countrycodes() {
			if ( ! get_option( 'qrcdr_country_codes', false ) ) {
				include_once dirname( __DIR__ ) . '/lib/countrycodes.php';
				update_option( 'qrcdr_country_codes', $qrcdr_countries );
			}
		}

		/**
		 * Register block
		 */
		public function register_block() {

			// 1. Registra il tipo di blocco usando il file block.json.
			register_block_type( plugin_dir_path( __DIR__ ) . 'block' );

			// --- Script dell'Editor ---
			wp_register_script(
				'qrcdr-block', // Corrisponde a "editorScript" in block.json.
				plugin_dir_url( __DIR__ ) . 'block/block.js',
				array( 'wp-blocks', 'wp-element', 'wp-block-editor', 'wp-components', 'wp-i18n' ),
				file_exists( plugin_dir_path( __DIR__ ) . 'block/block.js' ) ? filemtime( plugin_dir_path( __DIR__ ) . 'block/block.js' ) : QRCDR_VERSION,
				true
			);

			// --- Stile dell'Editor ---
			$is_dev_mode = defined( 'WP_DEBUG' ) && WP_DEBUG;
			$editor_style_path = $is_dev_mode ? 'assets/css/editor.css' : 'assets/css/editor.min.css';
			wp_register_style(
				'qrcdr-block-editor-styles', // Corrisponde a "editorStyle" in block.json.
				plugin_dir_url( __DIR__ ) . $editor_style_path,
				array( 'wp-edit-blocks' ),
				$is_dev_mode && file_exists( plugin_dir_path( __DIR__ ) . $editor_style_path ) ? filemtime( plugin_dir_path( __DIR__ ) . $editor_style_path ) : QRCDR_VERSION
			);
		}

		/**
		 * Register block editor assets
		 */
		public function init_gutenberg() {
			$tabs = qrcdr_blueprint()->get_tabs();
			$features = qrcdr_blueprint()->get_features();

			$simplify = function ( $items ) {
				return array_map(
					function ( $item ) {
						return array(
							'label'     => $item['label'] ?? '',
							'className' => $item['className'] ?? '',
						);
					},
					$items
				);
			};

			$atts = array(
				'tabs'     => (object) $simplify( $tabs ),
				'features' => (object) $simplify( $features ),
				'main_color' => qrcdr_settings()->get_option( 'primary_color', '#671AFF' ),
				'border_radius' => qrcdr_settings()->get_option( 'border_radius', 0 ),
				'i18n' => array(
					'features_accordion' => __( 'Features accordion', 'qrcdr' ),
					'services' => __( 'Services', 'qrcdr' ),
					'features' => __( 'Features', 'qrcdr' ),
					'sidebar' => __( 'Sidebar', 'qrcdr' ),
					'primary_color' => __( 'Primary color', 'qrcdr' ),
					'background_color' => __( 'Background color', 'qrcdr' ),
					'collapse_options' => __( 'Collapse options', 'qrcdr' ),
					'single_option_open' => __( 'Single option open', 'qrcdr' ),
					'value' => __( 'Value (px)', 'qrcdr' ),
					'border_radius' => __( 'Border radius', 'qrcdr' ),
				),
			);

			// Attacchiamo i dati all'handle dello script del nostro blocco.
			wp_localize_script( 'qrcdr-block', 'QRCDRblock', $atts );
		}

		/**
		 * Register qrcdr_template_tag taxonomy for attachments
		 */
		public function register_tax() {

			$labels = array(
				'name' => _x( 'QR Codes', 'taxonomy general name', 'qrcdr' ),
				'singular_name' => _x( 'QR code', 'taxonomy singular name', 'qrcdr' ),
				'all_items' => __( 'All QR codes', 'qrcdr' ),
			);

			$qrcdr_tag_args = array(
				'hierarchical' => false, // Tags and not categories.
				'labels' => $labels,
				'show_ui' => false,
				'show_admin_column' => true,
				'show_in_menu' => false,
				'publicly_queryable' => false,
				// 'public' => false,
				'update_count_callback' => '_update_generic_term_count',
			);

			register_taxonomy( $this->taxonomy, array( 'attachment' ), $qrcdr_tag_args );
			$this->set_term_name( 'qrcdr-download', __( 'Generated Downloads', 'qrcdr' ) );
		}

		/**
		 * Public Helper to register/update term names safely.
		 * Used by extensions (Templates, Pro) to register their own terms.
		 *
		 * @param string $slug  The term slug (e.g. 'qrcdr-templates').
		 * @param string $label The nice name (e.g. 'Templates').
		 */
		public function set_term_name( $slug, $label ) {

			if ( ! taxonomy_exists( $this->taxonomy ) ) {
				return;
			}

			$term = term_exists( $slug, $this->taxonomy );

			if ( ! $term ) {
				// CASO 1: Non esiste -> Creiamo.
				wp_insert_term(
					$label,
					$this->taxonomy,
					array( 'slug' => $slug )
				);
			} else {
				// CASO 2: Esiste -> Controlliamo se ha il nome "brutto" (uguale allo slug).
				$term_id = is_array( $term ) ? $term['term_id'] : $term;
				$term_obj = get_term( $term_id, $this->taxonomy );

				if ( $term_obj && ! is_wp_error( $term_obj ) && $term_obj->name === $slug ) {
					// Aggiorniamo con il Titolo bello.
					wp_update_term(
						$term_id,
						$this->taxonomy,
						array( 'name' => $label )
					);
				}
			}
		}

		/**
		 * Set /qrcdr/ upload directory.
		 *
		 * @param array $upload the default upload_dir array.
		 */
		public function custom_upload_dir( $upload ) {
			$dirname = $this->custom_upload_folder ? $this->custom_upload_folder : $this->slug;
			$upload['subdir'] = '/' . $dirname;
			$upload['path']   = $upload['basedir'] . $upload['subdir'];
			$upload['url']    = $upload['baseurl'] . $upload['subdir'];
			return $upload;
		}

		/**
		 * PNG Generator
		 *
		 * @param string $imgdata       Image data.
		 * @param string $filename      File name.
		 * @param string $taxonomy_slug Taxonomy slug.
		 * @param string $custom_subdir Custom upload dir.
		 */
		public function save_png( $imgdata = false, $filename = false, $taxonomy_slug = false, $custom_subdir = false ) {

			$response = array();

			if ( $imgdata && $filename ) {

				if ( $custom_subdir ) {
					$this->custom_upload_folder = $custom_subdir;
				}
				// Imposta la directory di upload personalizzata.
				add_filter( 'upload_dir', array( $this, 'custom_upload_dir' ) );

				$upload_dir  = wp_get_upload_dir();
				$qrcodes_dir = $upload_dir['url'];

				// Verifica esistenza file precedente per sovrascrittura.
				$final_file_url = $qrcodes_dir . '/' . $filename;
				$old_image_id   = attachment_url_to_postid( $final_file_url );

				if ( $old_image_id ) {
					wp_delete_attachment( $old_image_id, true );
				}

				$taxonomy_slug = $taxonomy_slug ? $taxonomy_slug : $this->slug;
				$content       = false;

				// 1. GESTIONE BASE64
				if ( strpos( $imgdata, 'data:' ) === 0 ) {
					$b64_data = explode( ',', $imgdata );
					if ( isset( $b64_data[1] ) ) {
						$base64_clean = str_replace( ' ', '+', $b64_data[1] );
						$content      = base64_decode( $base64_clean );
					}
				} elseif ( filter_var( $imgdata, FILTER_VALIDATE_URL ) ) {
					// 2. GESTIONE URL (HTTP/HTTPS) -> Usa wp_remote_get
					$remote_response = wp_remote_get( $imgdata, array( 'timeout' => 15 ) );

					if ( ! is_wp_error( $remote_response ) && wp_remote_retrieve_response_code( $remote_response ) === 200 ) {
						$content = wp_remote_retrieve_body( $remote_response );
					} else {
						// Logghiamo l'errore per debug se necessario.
						$response['error'] = __( 'Failed to fetch image from URL', 'qrcdr' );
					}
				} else {
					// 3. GESTIONE PATH LOCALE -> Usa WP_Filesystem
					global $wp_filesystem;
					if ( empty( $wp_filesystem ) ) {
						require_once ABSPATH . 'wp-admin/includes/file.php';
						WP_Filesystem();
					}
					// Se il file esiste localmente.
					if ( $wp_filesystem->exists( $imgdata ) || file_exists( $imgdata ) ) {
						$content = $wp_filesystem->get_contents( $imgdata );
					}
				}

				// Se non abbiamo ottenuto il contenuto, usciamo.
				if ( ! $content ) {
					if ( empty( $response['error'] ) ) {
						$response['error'] = __( 'File retrieval failed (invalid data or path)', 'qrcdr' );
					}
					remove_filter( 'upload_dir', array( $this, 'custom_upload_dir' ) );
					return $response;
				}

				// Scrittura fisica del file nella cartella uploads.
				$upload = wp_upload_bits( $filename, null, $content );

				if ( isset( $upload['error'] ) && $upload['error'] ) {
					$response['error'] = $upload['error'];
					remove_filter( 'upload_dir', array( $this, 'custom_upload_dir' ) );
					return $response;
				}

				// Determinazione tipo MIME.
				$type = '';
				if ( ! empty( $upload['type'] ) ) {
					$type = $upload['type'];
				} else {
					$mime = wp_check_filetype( $upload['file'] );
					if ( $mime ) {
						$type = $mime['type'];
					}
				}

				// Creazione Attachment nel DB.
				$attachment = array(
					'post_title'     => $filename,
					'post_type'      => 'attachment',
					'post_mime_type' => $type,
					'guid'           => $upload['url'],
				);

				$id = wp_insert_attachment( $attachment, $upload['file'] );

				if ( ! is_wp_error( $id ) ) {
					require_once ABSPATH . 'wp-admin/includes/image.php';
					$attach_data = wp_generate_attachment_metadata( $id, $upload['file'] );
					wp_update_attachment_metadata( $id, $attach_data );

					wp_set_object_terms( $id, $taxonomy_slug, $this->taxonomy );
					$response['id'] = $id;
				} else {
					$response['error'] = $id->get_error_message();
				}

				remove_filter( 'upload_dir', array( $this, 'custom_upload_dir' ) );

			} else {
				$response['error'] = __( 'Missing image data or filename', 'qrcdr' );
			}
			return $response;
		}

		/**
		 * Save PNG inside /uploads/qrcdr/ dir
		 */
		public function ajax_png() {

			// 1. Check nonce.
			check_ajax_referer( 'qrcdr-ajax-nonce', 'qrcdr_nonce' );

			// 2. Check input.
			if ( empty( $_POST['imgdata'] ) || empty( $_POST['filename'] ) ) {
				wp_send_json_error( array( 'message' => esc_html__( 'Missing data.', 'qrcdr' ) ) );
			}

			// 3. Sanitize and Validate
			// Rimuoviamo il commento phpcs:ignore perché ora eseguiamo la validazione qui.
			$imgdata_sanitized = sanitize_text_field( wp_unslash( $_POST['imgdata'] ) );
			// $imgdata_raw = wp_unslash( $_POST['imgdata'] );
			$filename    = sanitize_file_name( wp_unslash( $_POST['filename'] ) );
			// filepath is optional and informative for the JS.
			$filepath    = isset( $_POST['filepath'] ) ? sanitize_text_field( wp_unslash( $_POST['filepath'] ) ) : '';

			// Rimuoviamo l'header 'data:image/...' per validare solo la stringa base64.
			$base64_string = preg_replace( '#^data:image/\w+;base64,#i', '', $imgdata_sanitized );

			// Usiamo base64_decode in modalità strict ('true') per verificare che la stringa
			// contenga solo caratteri validi per il base64.
			if ( base64_decode( $base64_string, true ) === false ) {
				// Se la decodifica fallisce, l'input non è un base64 valido.
				wp_send_json_error( array( 'message' => esc_html__( 'Invalid image data.', 'qrcdr' ) ) );
			}

			// A questo punto, $imgdata_sanitized è considerato un input valido.
			// Lo passiamo alla funzione di salvataggio.
			$response = $this->save_png( $imgdata_sanitized, $filename, 'qrcdr-download' );

			// Add filepath to response.
			if ( ! isset( $response['error'] ) ) {
				$response['file'] = $filepath . '/' . $filename;
			}

			// Usiamo wp_send_json_success o wp_send_json_error per coerenza.
			if ( isset( $response['error'] ) ) {
				wp_send_json_error( $response );
			} else {
				wp_send_json_success( $response );
			}
		}

		/**
		 * Enable svg upload
		 *
		 * @param string $mimes mimes type.
		 */
		public function allow_svg( $mimes ) {
			$mimes['svg'] = 'image/svg+xml';
			return $mimes;
		}

		/**
		 * Disable svg upload
		 *
		 * @param string $mimes mimes type.
		 */
		public function disallow_svg( $mimes ) {
			unset( $mimes['svg'] );
			return $mimes;
		}

		/**
		 * Save SVG via AJAX
		 * Orchestratore principale: riceve input, sanitizza, salva e risponde.
		 */
		public function create_svg() {

			check_ajax_referer( 'qrcdr-ajax-nonce', 'qrcdr_nonce' );

			// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized via $this->sanitize_svg_xml() immediately below.
			$raw_content = isset( $_POST['content'] ) ? wp_unslash( $_POST['content'] ) : '';
			$basename    = isset( $_POST['basename'] ) ? sanitize_file_name( wp_unslash( $_POST['basename'] ) ) : '';

			if ( empty( $raw_content ) || empty( $basename ) ) {
				wp_send_json_error( array( 'message' => esc_html__( 'Required data is missing.', 'qrcdr' ) ) );
			}

			// Inizializza il Filesystem di WP.
			global $wp_filesystem;
			if ( empty( $wp_filesystem ) ) {
				require_once ABSPATH . 'wp-admin/includes/file.php';
				WP_Filesystem();
			}

			/**
			 * Upload Helper
			 */
			$process_upload = function ( $file_key ) use ( $wp_filesystem ) {

				// phpcs:ignore WordPress.Security.NonceVerification.Missing
				if ( ! isset( $_FILES[ $file_key ] ) || ! is_array( $_FILES[ $file_key ] ) ) {
					return false;
				}
				// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Missing
				$file = $_FILES[ $file_key ];

				if ( ! empty( $file['error'] ) || empty( $file['tmp_name'] ) || empty( $file['name'] ) ) {
					return false;
				}

				$raw_tmp_name = $file['tmp_name'];
				$raw_name     = $file['name'];

				if ( ! is_uploaded_file( $raw_tmp_name ) ) {
					return false;
				}

				// 3. Sanifica il nome del file e ottieni il tipo MIME reale.
				$filename = sanitize_file_name( $raw_name );
				$file_info = wp_check_filetype_and_ext( $raw_tmp_name, $filename );
				$real_mime = $file_info['type'];

				if ( ! $real_mime && '.svg' === substr( $filename, -4 ) ) {
					$content_check = $wp_filesystem->get_contents( $raw_tmp_name );
					if ( $content_check && strpos( $content_check, '<svg' ) !== false ) {
						$real_mime = 'image/svg+xml';
					}
				}

				// Media whitelist.
				$allowed_mime_types = array(
					'image/jpeg',
					'image/png',
					'image/gif',
					'image/svg+xml',
				);

				if ( ! in_array( $real_mime, $allowed_mime_types, true ) ) {
					return false; // Non è un tipo di immagine permesso.
				}

				$file_data = $wp_filesystem->get_contents( $raw_tmp_name );

				if ( $file_data ) {
					return 'data:' . $real_mime . ';base64,' . base64_encode( $file_data );
				}

				return false;
			};

			// 1. Background Image
			$bg_base64 = $process_upload( 'qrcdr_bg_image' );
			if ( $bg_base64 ) {
				$raw_content = str_replace( '__FILE_BG_UPLOAD__', $bg_base64, $raw_content );
			}

			// 2. Custom Logo
			$logo_base64 = $process_upload( 'qrcdr_custom_logo' );
			if ( $logo_base64 ) {
				$raw_content = str_replace( '__FILE_LOGO_UPLOAD__', $logo_base64, $raw_content );
			}

			// 1. Sanitize SVG
			$clean_content = $this->sanitize_svg_xml( $raw_content );

			// 2. Save File
			$response = $this->save_svg( $clean_content, $basename, 'qrcdr-download' );

			if ( isset( $response['error'] ) ) {
				wp_send_json_error( $response );
			}

			// 3. Nonce PDF
			// $response['basename'] è già stato definito dalla funzione save_svg().
			$base_url = home_url( '/download/pdf/' . $response['basename'] . '/' );

			// 2. Crea il nonce separatamente.
			$nonce = wp_create_nonce( 'qrcdr-pdf-download-nonce-action' );

			// 3. Aggiungi entrambi alla risposta JSON.
			$response['pdf_base_url'] = $base_url;
			$response['pdf_nonce'] = $nonce;

			wp_send_json_success( $response );
		}

		/**
		 * Definisce i tag e gli attributi SVG permessi per le icone di Bootstrap.
		 *
		 * @return array Array di tag e attributi consentiti.
		 */
		public function get_allowed_svg_tags() {
			return array(
				'svg'     => array(
					'width'    => true,
					'height'   => true,
					'viewbox'  => true,
					'fill'     => true,
					'xmlns'    => true,
					'points'   => true,
					'class' => true,
					'focusable' => true,
					'aria-hidden' => true,
				),
				'path'    => array(
					'd'    => true,
					'fill' => true,
				),
				'rect'    => array(
					'x'      => true,
					'y'      => true,
					'width'  => true,
					'height' => true,
					'fill'   => true,
				),
				'polygon' => array(
					'points' => true,
					'fill'   => true,
				),
				'circle'  => array(
					'cx' => true,
					'cy'   => true,
					'r'   => true,
					'fill'   => true,
				),
			);
		}

		/**
		 * Sanitize SVG with DOMDocument to preserve Case-Sensitivity (XML).
		 * wp_kses breaks camelCase attributes (viewBox).
		 *
		 * @param string $svg_content SVG content.
		 * @return string Clean SVG (without XML header).
		 */
		private function sanitize_svg_xml( $svg_content ) {
			// 1. Controllo base UTF-8 e presenza tag SVG.
			$svg_content = wp_check_invalid_utf8( trim( $svg_content ) );
			if ( false === stripos( $svg_content, '<svg' ) ) {
				return '';
			}

			// 2. Soppressione errori XML standard per gestione interna.
			$internal_errors = libxml_use_internal_errors( true );

			// 3. Caricamento XML.
			$dom = new DOMDocument();

			// Disabilitiamo check WPCS per proprietà native PHP in camelCase.
			// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
			$dom->preserveWhiteSpace = false;

			// LIBXML_NONET: Vitale per bloccare attacchi XXE (External Entity).
			// LIBXML_NOBLANKS: Pulisce spazi inutili.
			$loaded = $dom->loadXML( $svg_content, LIBXML_NONET | LIBXML_NOBLANKS );

			if ( ! $loaded ) {
				libxml_clear_errors();
				libxml_use_internal_errors( $internal_errors );
				return '';
			}

			// 4. Whitelist Tag & Attributi.
			$allowed_tags = array( 'svg', 'g', 'defs', 'style', 'title', 'desc', 'path', 'rect', 'circle', 'ellipse', 'line', 'polyline', 'polygon', 'linearGradient', 'radialGradient', 'stop', 'mask', 'clipPath', 'use', 'image', 'symbol', 'marker', 'pattern' );

			$allowed_attr = array( 'id', 'class', 'style', 'width', 'height', 'viewBox', 'transform', 'd', 'points', 'fill', 'stroke', 'stroke-width', 'opacity', 'fill-opacity', 'stroke-opacity', 'fill-rule', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-dasharray', 'stroke-dashoffset', 'x', 'y', 'x1', 'y1', 'x2', 'y2', 'cx', 'cy', 'r', 'rx', 'ry', 'offset', 'stop-color', 'stop-opacity', 'gradientUnits', 'gradientTransform', 'spreadMethod', 'mask', 'maskUnits', 'patternUnits', 'patternContentUnits', 'href', 'xlink:href', 'preserveAspectRatio', 'xmlns', 'xml:space', 'version' );

			$xpath = new DOMXPath( $dom );
			$nodes = $xpath->query( '//*' );

			// Iterazione inversa per rimuovere nodi in sicurezza.
			for ( $i = $nodes->length - 1; $i >= 0; $i-- ) {
				$node = $nodes->item( $i );

				// A. Check Tag.
				if ( ! in_array( $node->nodeName, $allowed_tags, true ) ) {
					$node->parentNode->removeChild( $node );
					continue;
				}

				// B. Check Attributi.
				if ( $node->hasAttributes() ) {
					$attributes = iterator_to_array( $node->attributes );

					foreach ( $attributes as $attr ) {
						$attr_name = $attr->nodeName;
						$attr_val  = $attr->nodeValue;

						// Rimuovi attributi non permessi.
						if ( ! in_array( $attr_name, $allowed_attr, true ) ) {
							$node->removeAttribute( $attr_name );
							continue;
						}

						// Sanitizzazione XSS base (javascript/vbscript).
						if ( stripos( $attr_val, 'javascript:' ) !== false || stripos( $attr_val, 'vbscript:' ) !== false ) {
							$node->removeAttribute( $attr_name );
						}
					}
				}
			}

			$result = $dom->saveXML( $dom->documentElement );

			// phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase

			// 5. Ripristino errori.
			libxml_clear_errors();
			libxml_use_internal_errors( $internal_errors );

			return $result;
		}

		/**
		 * Save SVG to uploads directory and create attachment.
		 *
		 * @param string $data     Image data (Sanitized XML string without header).
		 * @param string $basename File name without extension.
		 * @param string $taxonomy_slug Media taxonomy slug.
		 */
		public function save_svg( $data = false, $basename = false, $taxonomy_slug = false ) {

			$response = array();

			if ( $data && $basename ) {

				// Set custom upload directory.
				add_filter( 'upload_dir', array( $this, 'custom_upload_dir' ) );

				$upload_dir  = wp_get_upload_dir();
				$qrcodes_dir = $upload_dir['url'];
				$file_path   = $upload_dir['path'] . '/' . $basename . '.svg';

				// Cleanup old file via ID lookup (better than file_exists checks for attachments).
				$final_file_url = $qrcodes_dir . '/' . $basename . '.svg';
				$old_image_id   = attachment_url_to_postid( $final_file_url );

				if ( $old_image_id ) {
					wp_delete_attachment( $old_image_id, true );
				}

				// Re-inseriamo l'header XML standard (sanitize_svg_xml lo rimuove).
				$svgheader = '<?xml version="1.0" encoding="utf-8" standalone="yes"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">';

				$content    = $svgheader . $data;
				$image_name = $basename . '.svg';

				// Temporaneamente permettiamo l'upload di SVG (per sicurezza WP di default lo blocca).
				add_filter( 'upload_mimes', array( $this, 'allow_svg' ) );

				$upload = wp_upload_bits( $image_name, null, $content );

				// Ripristiniamo immediatamente i permessi standard.
				remove_filter( 'upload_mimes', array( $this, 'allow_svg' ) );

				if ( isset( $upload['error'] ) && $upload['error'] ) {
					$response['error'] = $upload['error'];
					remove_filter( 'upload_dir', array( $this, 'custom_upload_dir' ) );
					return $response;
				}

				// Determinazione tipo MIME.
				$type = '';
				if ( ! empty( $upload['type'] ) ) {
					$type = $upload['type'];
				} else {
					$mime = wp_check_filetype( $upload['file'] );
					if ( $mime ) {
						$type = $mime['type'];
					}
				}

				$attachment = array(
					'post_title'     => basename( $upload['file'], '.svg' ),
					'post_content'   => '',
					'post_type'      => 'attachment',
					'post_mime_type' => $type,
					'guid'           => $upload['url'],
				);

				$id = wp_insert_attachment( $attachment, $upload['file'] );

				// Generazione Metadati (Dimensioni).
				$width  = 0;
				$height = 0;

				// Usiamo libxml per sopprimere errori in caso di SVG malformato alla lettura.
				$internal_errors = libxml_use_internal_errors( true );
				$xml = simplexml_load_file( $upload['file'] );

				if ( $xml ) {
					$attributes = $xml->attributes();

					// 1. ViewBox è la fonte più affidabile.
					// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
					if ( isset( $attributes->viewBox ) ) {
						$viewbox = (string) $attributes->viewBox;
						$parts = preg_split( '/[\s,]+/', trim( $viewbox ) );

						if ( count( $parts ) === 4 ) {
							$width  = intval( $parts[2] );
							$height = intval( $parts[3] );
						}
					}
					// phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase

					// 2. Fallback su width/height attributes
					if ( ( ! $width || ! $height ) && isset( $attributes->width ) && isset( $attributes->height ) ) {
						$width  = intval( (string) $attributes->width );
						$height = intval( (string) $attributes->height );
					}
				}

				libxml_clear_errors();
				libxml_use_internal_errors( $internal_errors );

				// Defaults per evitare 0.
				$width  = $width ? $width : 100;
				$height = $height ? $height : 100;

				$metadata = array(
					'width'  => $width,
					'height' => $height,
					'file'   => _wp_relative_upload_path( $upload['file'] ),
					'sizes'  => array(),
				);

				wp_update_attachment_metadata( $id, $metadata );

				$term_slug = $taxonomy_slug ? $taxonomy_slug : $this->slug;
				wp_set_object_terms( $id, $term_slug, $this->taxonomy );

				remove_filter( 'upload_dir', array( $this, 'custom_upload_dir' ) );

				$svg_url = wp_get_attachment_url( $id );

				$response['basename'] = $basename;
				$response['filedir']  = $qrcodes_dir;
				$response['url']      = $svg_url;

			} else {
				$response['error'] = __( 'Missing content or filename', 'qrcdr' );
			}

			return $response;
		}

		/**
		 * Process static qr codes via ajax
		 */
		public function ajax_process() {

			// Il controllo nonce è corretto.
			if ( empty( $_POST ) || ! isset( $_POST['qrcdr_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['qrcdr_nonce'] ) ), 'qrcdr-ajax-nonce' ) ) {
				wp_send_json_error( array( 'message' => esc_html__( 'Security check failed.', 'qrcdr' ) ) );
			}

			$sanitized_content_json = isset( $_POST['content'] ) ? wp_kses_post( wp_unslash( $_POST['content'] ) ) : '[]';
			$sanitized_style_json   = isset( $_POST['style'] ) ? wp_kses_post( wp_unslash( $_POST['style'] ) ) : '[]';

			// 2. Decodifica le stringhe JSON ora sicure.
			$get_content = json_decode( $sanitized_content_json, true );
			$get_data    = json_decode( $sanitized_style_json, true );

			// 3. Valida che la decodifica abbia prodotto degli array.
			// json_decode restituisce null se il JSON non è valido.
			if ( ! is_array( $get_content ) || ! is_array( $get_data ) ) {
				$placeholder = qrcdr_settings()->get_option( 'placeholder', plugin_dir_url( __DIR__ ) . 'images/placeholder.svg' );
				wp_send_json_error(
					array(
						'message' => esc_html__( 'Invalid data format.', 'qrcdr' ),
						'placeholder' => $placeholder,
					),
				);
			}

			// A questo punto, non è più necessaria la sanificazione ricorsiva,
			// perché la potenziale minaccia (codice HTML/JS) è già stata rimossa dalla stringa JSON.
			// L'integrità dei dati all'interno del JSON è un problema di validazione, non di sanificazione XSS.
			$result = $this->generate_qr_code( $get_content, $get_data );

			if ( isset( $result['error'] ) ) {
				wp_send_json_error( $result );
			} else {
				wp_send_json_success( $result );
			}
		}

		/**
		 * Main QR generator
		 *
		 * @param array        $get_content Content data, JSON decoded.
		 * @param array        $get_data Style data, JSON decoded.
		 * @param string|false $data_override If available, overrides provided data.
		 * @return array Un array con l'output o un errore.
		 */
		public function generate_qr_code( $get_content, $get_data, $data_override = false ) {

			$output_data = false;

			if ( false !== $data_override ) {
				$output_data = $data_override;
			} else {

				$getsection = isset( $get_content['section'] ) ? $get_content['section'] : false;

				switch ( $getsection ) {

					case 'text':
						$output_data = isset( $get_content['data'] ) ? sanitize_textarea_field( $get_content['data'] ) : false;
						break;

					case 'link':
						$link = isset( $get_content['link'] ) ? esc_url_raw( $get_content['link'] ) : false;
						$output_data = $link ? qrcdr()->validateUrl( $link ) : false;
						break;

					case 'email':
						$datamailto = isset( $get_content['mailto'] ) ? is_email( $get_content['mailto'] ) : false;

						if ( $datamailto ) {
							$postsubject = isset( $get_content['subject'] ) ? sanitize_text_field( $get_content['subject'] ) : '';
							$postbody = isset( $get_content['body'] ) ? sanitize_textarea_field( $get_content['body'] ) : '';
							$datamailsubj = $postsubject ? html_entity_decode( $postsubject ) : '';
							$datamailbody = $postbody ? html_entity_decode( $postbody ) : '';
							$output_data = 'mailto:' . $datamailto . '?subject=' . rawurlencode( $datamailsubj ) . '&body=' . rawurlencode( $datamailbody );
						}
						break;

					case 'tel':
						$countrycode = isset( $get_content['countrycodetel'] ) ? '+' . sanitize_text_field( $get_content['countrycodetel'] ) : '';
						$number = isset( $get_content['tel'] ) ? sanitize_text_field( $get_content['tel'] ) : false;

						if ( $number ) {
							$output_data = 'tel:' . $countrycode . $number;
						}
						break;

					case 'sms':
						$countrycode = isset( $get_content['countrycodesms'] ) ? '+' . sanitize_text_field( $get_content['countrycodesms'] ) : '';
						$number = isset( $get_content['sms'] ) ? sanitize_text_field( $get_content['sms'] ) : false;
						$bodysms = isset( $get_content['bodysms'] ) ? sanitize_textarea_field( $get_content['bodysms'] ) : false;

						if ( $number ) {
							$output_data = 'SMSTO:' . $countrycode . $number . ':';
							if ( $bodysms ) {
								$output_data .= $bodysms;
							}
						}
						break;

					case 'whatsapp':
						$countrycode = isset( $get_content['wapp_countrycode'] ) ? sanitize_text_field( $get_content['wapp_countrycode'] ) : '';
						$number = isset( $get_content['wapp_number'] ) ? sanitize_text_field( $get_content['wapp_number'] ) : false;
						$message = isset( $get_content['wapp_message'] ) ? sanitize_textarea_field( $get_content['wapp_message'] ) : false;

						if ( $number ) {
							$output_data = 'https://wa.me/' . $countrycode . $number;
							if ( $message ) {
								$output_data .= '?text=' . urlencode( $message );
							}
						}
						break;

					case 'skype':
						$skype = isset( $get_content['skype'] ) ? sanitize_text_field( $get_content['skype'] ) : false;
						$skypetype = isset( $get_content['skype_type'] ) ? sanitize_text_field( $get_content['skype_type'] ) : 'chat';

						if ( $skype ) {
							$output_data = 'skype:' . urlencode( $skype ) . '?' . $skypetype;
						}
						break;

					case 'zoom':
						$zoom_id = isset( $get_content['zoom_id'] ) ? sanitize_text_field( $get_content['zoom_id'] ) : false;
						$zoom_pwd = isset( $get_content['zoom_pwd'] ) ? sanitize_text_field( $get_content['zoom_pwd'] ) : false;

						if ( $zoom_id && $zoom_pwd ) {
							$zoom_id = str_replace( ' ', '', $zoom_id );
							$output_data = 'https://zoom.us/j/' . $zoom_id . '?pwd=' . $zoom_pwd;
						}
						break;

					case 'wifi':
						$ssid = isset( $get_content['ssid'] ) ? sanitize_text_field( $get_content['ssid'] ) : false;
						$wifipass = isset( $get_content['wifipass'] ) ? sanitize_text_field( $get_content['wifipass'] ) : false;
						$networktype = isset( $get_content['networktype'] ) ? sanitize_text_field( $get_content['networktype'] ) : 'WPA';
						$wifihidden = isset( $get_content['wifihidden'] ) && $get_content['wifihidden'] ? 'true' : false;

						if ( $ssid ) {
							$output_data = 'WIFI:S:' . $ssid . ';';
							$output_data .= 'T:' . $networktype . ';';
							if ( $wifipass ) {
								$output_data .= 'P:' . $wifipass . ';';
							}
							if ( $wifihidden ) {
								$output_data .= 'H:true;';
							}
							$output_data .= ';';
						}
						break;

					case 'vcard':
						$vname = isset( $get_content['vname'] ) ? sanitize_text_field( $get_content['vname'] ) : false;
						if ( $vname ) {
							$vversion = isset( $get_content['vversion'] ) ? sanitize_text_field( $get_content['vversion'] ) : '3.0';
							$vnametitle = isset( $get_content['vnametitle'] ) ? sanitize_text_field( $get_content['vnametitle'] ) : '';
							$vlast = isset( $get_content['vlast'] ) ? sanitize_text_field( $get_content['vlast'] ) : '';

							$sort_name = $vlast . ';' . $vname;
							$sort_name = $vnametitle ? $sort_name . ';;' . $vnametitle : $sort_name;
							if ( '2.1' !== $vversion ) {
								$sort_name .= ';';
							}
							$fn = $vname . ' ' . $vlast;
							$fn = $vnametitle ? $vnametitle . ' ' . $fn : $fn;

							$output_data  = 'BEGIN:VCARD' . "\n";
							$output_data .= 'VERSION:' . $vversion . "\n";
							$output_data .= 'N;CHARSET=UTF-8:' . $sort_name . "\n";
							$output_data .= 'FN;CHARSET=UTF-8:' . $fn . "\n";

							// Contacts.
							$countrymobile = isset( $get_content['countrycodevmobile'] ) ? '+' . sanitize_text_field( $get_content['countrycodevmobile'] ) : '';
							$phone_cell = isset( $get_content['vmobile'] ) ? sanitize_text_field( $get_content['vmobile'] ) : false;
							if ( $phone_cell ) {
								$output_data .= 'TEL;TYPE=CELL:' . $countrymobile . $phone_cell . "\n";
							}

							$countryphone = isset( $get_content['countrycodevphone'] ) ? '+' . sanitize_text_field( $get_content['countrycodevphone'] ) : '';
							$phone = isset( $get_content['vphone'] ) ? sanitize_text_field( $get_content['vphone'] ) : false;
							if ( $phone ) {
								$output_data .= 'TEL;TYPE=HOME,VOICE:' . $countryphone . $phone . "\n";
							}

							$email = isset( $get_content['vemail'] ) ? is_email( $get_content['vemail'] ) : false;
							if ( $email ) {
								$output_data .= 'EMAIL:' . $email . "\n";
							}

							// Company.
							$org_name = isset( $get_content['vcompany'] ) ? sanitize_text_field( $get_content['vcompany'] ) : false;
							if ( $org_name ) {
								$output_data .= 'ORG;CHARSET=UTF-8:' . $org_name . "\n";
							}
							$org_title = isset( $get_content['vtitle'] ) ? sanitize_text_field( $get_content['vtitle'] ) : false;
							if ( $org_title ) {
								$output_data .= 'TITLE;CHARSET=UTF-8:' . $org_title . "\n";
							}

							// Address.
							$address = isset( $get_content['vaddress'] ) ? sanitize_text_field( $get_content['vaddress'] ) : false;
							$address_town = isset( $get_content['vcity'] ) ? sanitize_text_field( $get_content['vcity'] ) : false;
							$address_state = isset( $get_content['vstate'] ) ? sanitize_text_field( $get_content['vstate'] ) : false;
							$address_post_code = isset( $get_content['vcap'] ) ? sanitize_text_field( $get_content['vcap'] ) : false;
							$address_country = isset( $get_content['vcountry'] ) ? sanitize_text_field( $get_content['vcountry'] ) : false;

							if ( $address || $address_town || $address_state || $address_country ) {
								$output_data .= 'ADR;CHARSET=UTF-8;TYPE=WORK,PREF:;';
								if ( $address ) {
									$output_data .= ';' . str_replace( array( ';', ',' ), array( '\;', '\,' ), $address );
								}
								$output_data .= ';' . $address_town . ';' . $address_state . ';' . $address_post_code . ';' . $address_country . "\n";
							}

							$vurl = isset( $get_content['vurl'] ) ? esc_url_raw( $get_content['vurl'] ) : false;
							if ( $vurl ) {
								$output_data .= 'URL:' . $vurl . "\n";
							}

							$output_data .= 'END:VCARD';
						}
						break;

					case 'location':
						$lat = isset( $get_content['lat'] ) ? esc_attr( $get_content['lat'] ) : false;
						$lng = isset( $get_content['lng'] ) ? esc_attr( $get_content['lng'] ) : false;

						if ( $lat && $lng ) {
							$output_data = 'geo:' . $lat . ',' . $lng . '?q=' . $lat . ',' . $lng;
						}
						break;

					case 'event':
						$title = isset( $get_content['eventtitle'] ) ? sanitize_text_field( $get_content['eventtitle'] ) : false;
						$starttime = isset( $get_content['eventstarttime'] ) ? esc_attr( $get_content['eventstarttime'] ) : false;

						if ( $title && $starttime ) {
							$location = isset( $get_content['eventlocation'] ) ? sanitize_text_field( $get_content['eventlocation'] ) : false;
							$endtime = isset( $get_content['eventendtime'] ) ? esc_attr( $get_content['eventendtime'] ) : false;
							$reminder = isset( $get_content['eventreminder'] ) ? esc_attr( $get_content['eventreminder'] ) : false;
							$eventurl = isset( $get_content['eventlink'] ) ? esc_url_raw( $get_content['eventlink'] ) : false;
							$eventnote = isset( $get_content['eventnote'] ) ? sanitize_textarea_field( $get_content['eventnote'] ) : false;

							$output_data = 'BEGIN:VCALENDAR' . "\n";
							$output_data .= 'VERSION:2.0' . "\n";
							$output_data .= 'PRODID:-//QRcdr//QRcdr 4.0//EN' . "\n";
							$output_data .= 'BEGIN:VEVENT' . "\n";
							$output_data .= 'SUMMARY:' . $title . "\n";

							if ( $location ) {
								$output_data .= 'LOCATION:' . str_replace( array( ';', ',' ), array( '\;', '\,' ), $location ) . "\n";
							}
							$output_data .= 'DTSTART:' . gmdate( 'Ymd\THis\Z', $starttime ) . "\n";
							if ( $endtime ) {
								$output_data .= 'DTEND:' . gmdate( 'Ymd\THis\Z', $endtime ) . "\n";
							}
							if ( $eventnote ) {
								$output_data .= 'DESCRIPTION:' . str_replace( "\r\n", "\\n", $eventnote ) . "\n";
							}
							if ( $eventurl ) {
								$output_data .= 'URL:' . $eventurl . "\n";
							}
							if ( $reminder ) {
								$output_data .= 'BEGIN:VALARM' . "\n";
								$output_data .= 'TRIGGER:' . $reminder . "\n";
								$output_data .= 'ACTION:DISPLAY' . "\n";
								$output_data .= 'DESCRIPTION:Reminder' . "\n";
								$output_data .= 'END:VALARM' . "\n";
							}
							$output_data .= 'END:VEVENT' . "\n";
							$output_data .= 'END:VCALENDAR';
						}
						break;

					case 'paypal':
						$email = isset( $get_content['pp_email'] ) ? is_email( $get_content['pp_email'] ) : false;
						$name = isset( $get_content['pp_name'] ) ? sanitize_text_field( $get_content['pp_name'] ) : false;

						if ( $email && $name ) {
							$type = isset( $get_content['pp_type'] ) ? sanitize_text_field( $get_content['pp_type'] ) : '_donations';
							$price = isset( $get_content['pp_price'] ) ? sanitize_text_field( $get_content['pp_price'] ) : '';
							$currency = isset( $get_content['pp_currency'] ) ? sanitize_text_field( $get_content['pp_currency'] ) : 'USD';
							$shipping = isset( $get_content['pp_shipping'] ) ? sanitize_text_field( $get_content['pp_shipping'] ) : '';
							$tax = isset( $get_content['pp_tax'] ) ? sanitize_text_field( $get_content['pp_tax'] ) : '';

							$output_data  = 'https://www.paypal.com/cgi-bin/webscr';
							$output_data .= '?cmd=' . $type;
							$output_data .= '&business=' . urlencode( $email );
							$output_data .= '&item_name=' . urlencode( $name );
							$output_data .= '&amount=' . urlencode( $price );
							$output_data .= '&currency_code=' . $currency;
							if ( $shipping ) {
								$output_data .= '&shipping=' . urlencode( $shipping );
							}
							if ( $tax ) {
								$output_data .= '&tax_rate=' . urlencode( $tax );
							}
							if ( '_xclick' === $type ) {
								$output_data .= '&button_subtype=services&bn=' . urlencode( 'PP-BuyNowBF:btn_buynow_LG.gif:NonHostedGuest' );
							} elseif ( '_cart' === $type ) {
								$output_data .= '&button_subtype=products&add=1&bn=' . urlencode( 'PP-ShopCartBF:btn_cart_LG.gif:NonHostedGuest' );
							} else {
								$output_data .= '&bn=' . urlencode( 'PP-DonationsBF:btn_donate_LG.gif:NonHostedGuest' );
							}
							$output_data .= '&lc=US&no_note=0';
						}
						break;

					case 'bitcoin':
						$btc_account = isset( $get_content['btc_account'] ) ? sanitize_text_field( $get_content['btc_account'] ) : false;

						if ( $btc_account ) {
							$btc_amount = isset( $get_content['btc_amount'] ) ? sanitize_text_field( $get_content['btc_amount'] ) : false;
							$btc_label = isset( $get_content['btc_label'] ) ? sanitize_text_field( $get_content['btc_label'] ) : false;
							$btc_message = isset( $get_content['btc_message'] ) ? sanitize_textarea_field( $get_content['btc_message'] ) : false;

							$output_data = 'bitcoin:' . $btc_account;
							$params = array();
							if ( $btc_amount ) {
								$params['amount'] = $btc_amount;
							}
							if ( $btc_label ) {
								$params['label'] = $btc_label;
							}
							if ( $btc_message ) {
								$params['message'] = $btc_message;
							}
							if ( ! empty( $params ) ) {
								$output_data .= '?' . http_build_query( $params );
							}
						}
						break;
				}
			}

			if ( ! $output_data ) {
				$placeholder = qrcdr_settings()->get_option( 'placeholder', plugin_dir_url( __DIR__ ) . 'images/placeholder.svg' );
				return array(
					'error'       => esc_html__( 'Please provide more data', 'qrcdr' ),
					'placeholder' => $placeholder,
				);
			}

			$setbackcolor = isset( $get_data['backcolor'] ) ? $get_data['backcolor'] : false;
			$stringbackcolor = $setbackcolor ? $setbackcolor : qrcdr_settings()->get_option( 'qr_bgcolor', '#ffffff' );

			$setfrontcolor = isset( $get_data['frontcolor'] ) ? $get_data['frontcolor'] : false;
			$stringfrontcolor = $setfrontcolor ? $setfrontcolor : qrcdr_settings()->get_option( 'qr_color', '#000000' );

			$optionlogo = isset( $get_data['optionlogo'] ) ? $get_data['optionlogo'] : false;

			$qrcdr_patterns_lib = qrcdr_patterns()->get_patterns();
			$qrcdr_patterns = $qrcdr_patterns_lib['patterns'];
			$qrcdr_markers = $qrcdr_patterns_lib['markers'];

			$pattern = isset( $get_data['pattern'] ) ? $get_data['pattern'] : false;
			$marker_out = isset( $get_data['marker_out'] ) ? $get_data['marker_out'] : false;
			$marker_in = isset( $get_data['marker_in'] ) ? $get_data['marker_in'] : false;

			$pattern_path = $pattern && isset( $qrcdr_patterns[ $pattern ] ) ? $qrcdr_patterns[ $pattern ] : ( isset( $qrcdr_patterns['default'] ) ? $qrcdr_patterns['default'] : array() );
			$marker_out_path = $marker_out && isset( $qrcdr_markers[ $marker_out ] ) ? $qrcdr_markers[ $marker_out ] : ( isset( $qrcdr_markers['default'] ) ? $qrcdr_markers['default'] : array() );
			$marker_in_path = $marker_in && isset( $qrcdr_patterns[ $marker_in ] ) ? $qrcdr_patterns[ $marker_in ] : ( isset( $qrcdr_patterns['default'] ) ? $qrcdr_patterns['default'] : array() );

			$marker_out_color = isset( $get_data['marker_out_color'] ) ? $get_data['marker_out_color'] : false;
			$marker_in_color = isset( $get_data['marker_in_color'] ) ? $get_data['marker_in_color'] : false;
			$gradient_color = isset( $get_data['gradient_color'] ) ? $get_data['gradient_color'] : false;

			$markers_color = isset( $get_data['markers_color'] ) ? boolval( $get_data['markers_color'] ) : false;

			$radial = isset( $get_data['radial'] ) ? boolval( $get_data['radial'] ) : false;
			$no_logo_bg = isset( $get_data['no_logo_bg'] ) ? boolval( $get_data['no_logo_bg'] ) : false;
			$different_markers_color = isset( $get_data['different_markers_color'] ) ? boolval( $get_data['different_markers_color'] ) : false;

			$transparent_code = isset( $get_data['transparent_code'] ) ? boolval( $get_data['transparent_code'] ) : false;

			$negative_qr = $transparent_code && isset( $get_data['negative_qr'] ) ? boolval( $get_data['negative_qr'] ) : false;
			$gradient = isset( $get_data['gradient'] ) && ! $negative_qr ? boolval( $get_data['gradient'] ) : false;
			$custom_frame_color = isset( $get_data['custom_frame_color'] ) ? boolval( $get_data['custom_frame_color'] ) : false;

			$nopdf = ( $gradient || $negative_qr );

			if ( $different_markers_color ) {
				$marker_top_right_outline = isset( $get_data['marker_top_right_outline'] ) ? $get_data['marker_top_right_outline'] : false;
				$marker_top_right_center = isset( $get_data['marker_top_right_center'] ) ? $get_data['marker_top_right_center'] : false;
				$marker_bottom_left_outline = isset( $get_data['marker_bottom_left_outline'] ) ? $get_data['marker_bottom_left_outline'] : false;
				$marker_bottom_left_center = isset( $get_data['marker_bottom_left_center'] ) ? $get_data['marker_bottom_left_center'] : false;
			} else {
				$marker_top_right_outline = $marker_out_color;
				$marker_bottom_left_outline = $marker_out_color;
				$marker_top_right_center = $marker_in_color;
				$marker_bottom_left_center = $marker_in_color;
			}

			$outerframe = isset( $get_data['outer_frame'] ) ? $get_data['outer_frame'] : 'none';

			$qrcdr_frames = qrcdr_patterns()->get_frames();
			$frame_data = isset( $qrcdr_frames[ $outerframe ] ) ? $qrcdr_frames[ $outerframe ] : array();

			$framecolor = isset( $get_data['framecolor'] ) ? $get_data['framecolor'] : false;
			$framelabel = isset( $get_data['framelabel'] ) ? $get_data['framelabel'] : false;
			$label_font = isset( $get_data['label_font'] ) ? $get_data['label_font'] : false;
			$logo_size = isset( $get_data['logo_size'] ) ? $get_data['logo_size'] : false;
			$label_text_size = isset( $get_data['label_text_size'] ) ? $get_data['label_text_size'] : false;

			$watermark_base64 = false;

			if ( ! empty( $optionlogo ) && 'none' !== $optionlogo ) {
				$input_logo = trim( (string) $optionlogo );
				$basemark   = 'data:image/';

				// 1: input is already base64 or placeholder.
				if (
					substr( $input_logo, 0, strlen( $basemark ) ) === $basemark ||
					strpos( $input_logo, '__LOGO_PLACEHOLDER__' ) !== false ||
					strpos( $input_logo, '__FILE_LOGO_UPLOAD__' ) !== false
				) {
					$watermark_base64 = $input_logo;

				} else {
					// 2: The input is an attachment ID di allegato or URL.
					$path     = '';
					$mimetype = '';

					// --- CONVERSION TO RELIABLE PATH ---
					if ( is_numeric( $input_logo ) ) {
						// È un ID di allegato.
						$path = get_attached_file( (int) $input_logo );

					} else if ( filter_var( $input_logo, FILTER_VALIDATE_URL ) ) {
						// È un URL. Convertiamolo in un percorso fisico in modo affidabile.
						$upload_dir_info = wp_upload_dir();
						$base_url = $upload_dir_info['baseurl'];
						$base_dir = $upload_dir_info['basedir'];

						// Check baseurl.
						if ( strpos( $input_logo, $base_url ) === 0 ) {
							$path = str_replace( $base_url, $base_dir, $input_logo );
						} else {
							// The URL is not a local file inside uploads, we can't handle it.
							$path = false;
						}
					} else {
						// Ignore direct paths.
						$path = false;
					}

					// --- BASE64 conversion ---
					if ( $path && file_exists( $path ) ) {
						global $wp_filesystem;
						if ( empty( $wp_filesystem ) ) {
							require_once ABSPATH . 'wp-admin/includes/file.php';
							WP_Filesystem();
						}

						$data = $wp_filesystem->get_contents( $path );

						if ( $data ) {
							$mimetype = mime_content_type( $path );
							if ( $mimetype ) {
								$type = ( 'image/svg' === $mimetype ) ? 'image/svg+xml' : $mimetype;
								$watermark_base64 = 'data:' . $type . ';base64,' . base64_encode( $data );
							}
						}
					}
				}
			}

			$watermark = $watermark_base64;

			$labeltext_color = isset( $get_data['labelcolor'] ) ? $get_data['labelcolor'] : false;
			$labeltext_color = $labeltext_color ? $labeltext_color : '#FFFFFF';

			$bg_image = isset( $get_data['bg_image'] ) && $transparent_code ? $get_data['bg_image'] : false;
			$bg_image = $bg_image && 'no' !== $bg_image ? $bg_image : 'none';

			$optionstyle = array(
				'optionlogo' => $watermark,
				'pattern' => $pattern,
				'pattern_path' => $pattern_path,
				'marker_out' => $marker_out,
				'marker_out_path' => $marker_out_path,
				'marker_in' => $marker_in,
				'marker_in_path' => $marker_in_path,
				'marker_out_color' => $marker_out_color,
				'marker_in_color' => $marker_in_color,
				'marker_top_right_outline' => $marker_top_right_outline,
				'marker_top_right_center' => $marker_top_right_center,
				'marker_bottom_left_outline' => $marker_bottom_left_outline,
				'marker_bottom_left_center' => $marker_bottom_left_center,
				'gradient' => $gradient,
				'gradient_color' => $gradient_color,
				'markers_color' => $markers_color,
				'radial' => $radial,
				'no_logo_bg' => $no_logo_bg,
				'frame' => $outerframe,
				'frame_data' => $frame_data,
				'custom_frame_color' => $custom_frame_color,
				'framecolor' => $framecolor,
				'framelabel' => $framelabel,
				'label_font' => $label_font,
				'labeltext_color' => $labeltext_color,
				'logo_size' => $logo_size,
				'label_text_size' => $label_text_size,
				'transparent_code' => $transparent_code,
				'bg_image' => $bg_image,
				'negative' => $negative_qr,
			);

			$backcolor = qrcdr()->hexdecColor( $stringbackcolor, '#FFFFFF' );
			$frontcolor = qrcdr()->hexdecColor( $stringfrontcolor, '#000000' );

			$level = isset( $get_data['level'] ) ? esc_attr( $get_data['level'] ) : false;
			$level = $level ? $level : qrcdr_settings()->get_option( 'precision', 'Q' );

			if ( in_array( $level, array( 'L', 'M', 'Q', 'H' ) ) ) {
				$error_correction_level = $level;
				// if ( 'none' !== $optionlogo && 'L' == $error_correction_level ) {
				// 	$error_correction_level = 'M';
				// }
			}
			$size = isset( $get_data['size'] ) ? esc_attr( $get_data['size'] ) : false;
			$size = $size ? $size : 16;
			$matrix_point_size = min( max( (int) $size, 4 ), 32 );

			require_once dirname( __DIR__ ) . '/lib/frames.php';

			$backcolor = isset( $get_data['transparent'] ) ? 'transparent' : $backcolor;

			$filename = md5( $output_data . '|' . $error_correction_level . '|' . $matrix_point_size . time() );

			$filenamesvg = $filename . '.svg';
			$basename = basename( $filenamesvg, '.svg' );

			$codemargin = 'none' !== $outerframe ? $qrcdr_frames[ $outerframe ]['frame_border'] * 2 + 1 : 2;

			$optionstyle['backcolor'] = $setbackcolor;
			$optionstyle['frontcolor'] = $setfrontcolor;

			$content = Qrcdr_Generator::svg( $output_data, $filenamesvg, $error_correction_level, $matrix_point_size, $codemargin, false, $backcolor, $frontcolor, $optionstyle );

			$result = array(
				'basename' => $basename,
				'content' => $content,
				'nopdf' => $nopdf,
			);
			return $result;
		}

		/**
		 * Register PDF rewrite.
		 * URL: /download/pdf/{qr_code_id}/
		 */
		public function register_download_rewrite_rule() {
			add_rewrite_rule(
				'^download/pdf/([^/]+)/?$', // Regex: get QR code id.
				'index.php?qrpdf_id=$matches[1]', // Map new query var.
				'top' // Priority.
			);
		}

		/**
		 * Add download query.
		 *
		 * @param array $vars Le query var esistenti.
		 * @return array Le query var aggiornate.
		 */
		public function register_download_query_var( $vars ) {
			$vars[] = 'qrpdf_id'; // Il nome della nostra variabile.
			return $vars;
		}

		/**
		 * Generate pdf
		 */
		public function intercept_pdf_download() {

			if ( is_admin() ) {
				return;
			}

			$qr_filename = get_query_var( 'qrpdf_id' );

			if ( ! $qr_filename ) {
				return;
			}

			if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'qrcdr-pdf-download-nonce-action' ) ) {
				wp_die( 'Invalid security token.', 'Forbidden', array( 'response' => 403 ) );
			}

			$qr_filename = sanitize_key( $qr_filename );
			add_filter( 'upload_dir', array( $this, 'custom_upload_dir' ) );
			$upload_dir = wp_upload_dir();
			$filepath = trailingslashit( $upload_dir['path'] ) . $qr_filename . '.svg';
			remove_filter( 'upload_dir', array( $this, 'custom_upload_dir' ) );

			global $wp_filesystem;
			if ( empty( $wp_filesystem ) ) {
				require_once ABSPATH . '/wp-admin/includes/file.php';
				WP_Filesystem();
			}
			if ( ! $wp_filesystem->exists( $filepath ) ) {
				wp_die(
					esc_html__( 'The requested file was not found.', 'qrcdr' ),
					esc_html__( 'File Not Found', 'qrcdr' ),
					array( 'response' => 404 )
				);
			}

			$svg_content = $wp_filesystem->get_contents( $filepath );

			if ( false === $svg_content ) {
				wp_die(
					esc_html__( 'Could not read the SVG file.', 'qrcdr' ),
					esc_html__( 'Server Error', 'qrcdr' ),
					array( 'response' => 500 )
				);
			}

			// 6. Generazione PDF
			$mpdf_base    = dirname( __DIR__ ) . '/lib/mpdf/vendor';
			$vendor_autoload = $mpdf_base . '/autoload.php';

			if ( ! file_exists( $vendor_autoload ) || ! file_exists( $mpdf_base . '/myclabs/deep-copy/src/DeepCopy/deep_copy.php' ) ) {
				if ( self::$qrcdr_debug ) {
					wp_die(
						esc_html__( 'The PDF library (mPDF) is not installed correctly. Run <code>composer install</code> inside <code>lib/mpdf/</code> or redeploy the vendor directory.', 'qrcdr' ),
						esc_html__( 'PDF Library Missing', 'qrcdr' ),
						array( 'response' => 500 )
					);
				}
				wp_die(
					esc_html__( 'The PDF library is not installed correctly.', 'qrcdr' ),
					esc_html__( 'PDF Error', 'qrcdr' ),
					array( 'response' => 500 )
				);
			}

			require_once $vendor_autoload;

			try {
				$mpdf = new \Mpdf\Mpdf( array( 'mode' => 'c' ) );
				// phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
				$mpdf->imageVars['qrcdr_svg'] = $svg_content;
				$mpdf->SetTitle( 'QR Code' );

				// escaping not needed for mPDF internal html.
				$image_html = '<div style="width:100%; text-align: center;"><img style="height: auto; max-width:100%; margin:0 auto;" src="var:qrcdr_svg" /></div>';

				$mpdf->WriteHTML( $image_html );
				$mpdf->Output( 'qrcode.pdf', 'I' );

			} catch ( \Mpdf\MpdfException $e ) {
				wp_die(
					esc_html__( 'An error occurred while generating the PDF: ', 'qrcdr' ) . esc_html( $e->getMessage() ),
					esc_html__( 'PDF Error', 'qrcdr' ),
					array( 'response' => 500 )
				);
			}
			exit;
		}

		/**
		 * Get color variants
		 *
		 * @param string $maincolor Main color.
		 */
		public function get_color_variants( $maincolor ) {

			$maincolor_dark = qrcdr()->adjustBrightness( $maincolor, -0.3 );

			$rgb_array = qrcdr()->HTMLToRGB( $maincolor );

			$primary_rgb_string = implode( ',', $rgb_array );

			$brightness = ( $rgb_array['r'] * 299 + $rgb_array['g'] * 587 + $rgb_array['b'] * 114 ) / 1000;

			$maintext = ( $brightness > 128 ) ? '#1E1E1E' : '#F6F6F6';
			// $linkcolor = ( $brightness > 128 ) ? '#1E1E1E' : $maincolor;
			$linkcolor = $maincolor;

			return array(
				'primary'      => $maincolor,
				'primary-rgb'  => $primary_rgb_string,
				'primary-dark' => $maincolor_dark,
				'text'         => $maintext,
				'link'         => $linkcolor,
			);
		}

		/**
		 * Output Generator
		 *
		 * @param array $atts Array of shortcode attributes.
		 */
		public function do_shortcode( $atts = array() ) {

			self::$gencounter++;
			$form_id = self::$gencounter;

			wp_enqueue_style( 'qrcdr' );

			$args = shortcode_atts(
				array(
					'layout' => 'classic', // classic || vertical.
					'sidebar' => 'right', // left || right.
					'tabs' => '', // 'link,text,email,location,tel,sms,whatsapp,skype,zoom,wifi,vcard,event,paypal,bitcoin'.
					'features' => '', // 'color,design,logo,frame,options'.
					'color' => qrcdr_settings()->get_option( 'primary_color', '#671AFF' ),
					'collapse' => 1,
					'single' => 0,
					'border_radius' => qrcdr_settings()->get_option( 'border_radius', '0' ) . 'px',
				),
				$atts
			);

			$border_radius = esc_html( $args['border_radius'] );
			$qrcdr_sidebar = esc_attr( $args['sidebar'] );
			$qrcdr_tabs = array_filter( explode( ',', esc_attr( $args['tabs'] ) ) );
			$qrcdr_features = array_filter( explode( ',', esc_attr( $args['features'] ) ) );
			$collapse = wp_validate_boolean( $args['collapse'] );
			$single = wp_validate_boolean( $args['single'] );

			if ( empty( $qrcdr_tabs ) ) {
				return __( 'Please select at least one service for the QR code generator', 'qrcdr' );
			}

			// First section as default.
			$getsection = '.qrcdr-' . $qrcdr_tabs[0];

			$wrapper_class = ' qrcdr-dynamic';
			$accordion_parent = $single ? '#QRCDRcollapseSettings-' . $form_id : '';
			$collapsed = $collapse ? '' : ' show';

			$maincolor  = esc_attr( $args['color'] );
			$get_colors = $this->get_color_variants( $maincolor );

			$wrapper = '<div class="qrcdr-wrapper' . $wrapper_class . '" 
			data-qrcdr-features="' . esc_attr( $args['features'] ) . '" 
			data-qrcdr-collapse="' . $collapse . '" 
			data-qrcdr-single="' . $single . '" 
			data-qrcdr-formid="' . $form_id . '" ';
			$wrapper .= 'style="--qrcdr-primary: ' . $get_colors['primary'] . '; --qrcdr-primary-rgb: ' . $get_colors['primary-rgb'] . '; --qrcdr-primary-dark: ' . $get_colors['primary-dark'] . '; --qrcdr-text: ' . $get_colors['text'] . '; --qrcdr-link: ' . $get_colors['link'] . '; --qrcdr-border-radius: ' . $border_radius . ';"';
			$wrapper .= '>';

			/**
			 * Hook to add serivces
			 */
			$hook_data = array(
				'form_id' => $form_id,
				'getsection' => $getsection,
				'qrcdr_tabs' => $qrcdr_tabs,
				'get_colors' => $get_colors,
			);

			ob_start();

			do_action( 'qrcdr_before_generator_wrapper', $hook_data );

			$template_path = dirname( __DIR__ ) . '/template-parts/generator.php';

			if ( file_exists( $template_path ) ) {
				include $template_path;
			}

			do_action( 'qrcdr_after_generator_wrapper', $hook_data );

			$output = ob_get_clean();

			$wrapper .= $output;
			$wrapper .= '</div>';

			if ( in_array( 'location', $qrcdr_tabs ) ) {
				wp_enqueue_script( 'ol-qrcdr' );
				if ( self::$qrcdr_debug ) {
					wp_enqueue_style( 'ol-qrcdr' );
				}
			}

			wp_enqueue_script( 'qrcdr' );

			return $wrapper;
		}

		/**
		 * Get country codes list
		 */
		public function country_code_options() {
			$output = '<option value="">--</option>';
			$countries = get_option( 'qrcdr_country_codes', false );
			if ( $countries ) {
				$output = '';
				foreach ( $countries as $i => $row ) {
					$output .= '<option value="' . esc_attr( $row['code'] ) . '">' . esc_attr( $row['name'] ) . '</option>';
				}
			}
			return $output;
		}

		/**
		 * Universal Hide Function.
		 * Hides ANY attachment that belongs to the 'qrcdr_media_origin' taxonomy
		 * from the main Media Library view, unless explicitly filtered.
		 *
		 * @param WP_Query $query The current query.
		 */
		public function hide_qrcdr_media_from_library( $query ) {
			// 1. Controlli standard (Admin, Main Query, Attachment)
			if ( ! is_admin() || ! $query->is_main_query() || 'attachment' !== $query->get( 'post_type' ) ) {
				return;
			}

			// 2. Controlla lo screen corrente
			$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
			if ( ! $screen || 'upload' !== $screen->id ) {
				return;
			}

			// 3. ESCAPE HATCH: Se l'utente sta filtrando per questa tassonomia, NON nascondere nulla.
			// Se $_REQUEST['qrcdr_media_origin'] ha un valore (es. 'qrcdr-templates'), usciamo
			// e lasciamo che WordPress mostri i file di quel termine.
			// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Filtro admin, nessun nonce necessario.
			if ( ! empty( $_REQUEST[ $this->taxonomy ] ) ) {
				return;
			}

			// 4. LOGICA UNIVERSALE: "NOT EXISTS"
			// Nascondi tutti gli attachment che hanno QUALSIASI termine associato a questa tassonomia.
			$tax_query = $query->get( 'tax_query' );
			if ( ! is_array( $tax_query ) ) {
				$tax_query = array();
			}

			$tax_query[] = array(
				'taxonomy' => $this->taxonomy, // 'qrcdr_media_origin'
				'operator' => 'NOT EXISTS',    // Esclude se ha una relazione con questa tax.
			);

			$query->set( 'tax_query', $tax_query );
		}

		/**
		 * Add dropdown filter to Media Library
		 */
		public function add_media_taxonomy_filter() {

			$screen = get_current_screen();
			if ( 'upload' === $screen->id ) {
				if ( taxonomy_exists( $this->taxonomy ) ) {

					wp_dropdown_categories(
						array(
							// 'show_option_all' => __( 'QR Codes', 'qrcdr' ),
							'show_option_none'  => __( 'Filter by QR Codes', 'qrcdr' ),
							'option_none_value' => '',
							'taxonomy'        => $this->taxonomy,
							'name'            => $this->taxonomy,
							'orderby'         => 'name',
							// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Filtro admin, nessun nonce necessario.
							'selected'          => ( isset( $_GET[ $this->taxonomy ] ) ? sanitize_text_field( wp_unslash( $_GET[ $this->taxonomy ] ) ) : '' ),
							'hierarchical'    => true,
							'value_field'     => 'slug',
							'show_count'      => true,
							'hide_empty'      => false,
						)
					);
				}
			}
		}
	}
}
