<?php

/*
Plugin Name: Automotive Inventory Importer – Sync Car Dealer Feeds
Plugin URI: https://www.ibexoft.com/product/automotive-feed-import/
Description: Automatically update your car inventory on your website. No manual entry needed. Stop wasting hours uploading cars one by one.
Version: 3.0.1
Author: Muhammad Jawaid Shamshad - Ibexoft
Author URI: https://ibexoft.com
License: GNU Public License
Requires PHP: 8.0
Text Domain: automotive-feed-import
Domain Path: /languages
*/
if ( !defined( 'ABSPATH' ) ) {
    exit;
    // Exit if accessed directly
}
if ( !defined( 'AFI_VERSION' ) ) {
    define( 'AFI_VERSION', '3.0.1' );
}
if ( function_exists( 'afi_fs' ) ) {
    afi_fs()->set_basename( false, __FILE__ );
} else {
    /**
     * DO NOT REMOVE THIS IF, IT IS ESSENTIAL FOR THE
     * `function_exists` CALL ABOVE TO PROPERLY WORK.
     */
    if ( !function_exists( 'afi_fs' ) ) {
        // Create a helper function for easy SDK access.
        function afi_fs() {
            global $afi_fs;
            if ( !isset( $afi_fs ) ) {
                // Include Freemius SDK.
                require_once dirname( __FILE__ ) . '/vendor/freemius/start.php';
                $afi_fs = fs_dynamic_init( array(
                    'id'               => '29664',
                    'slug'             => 'automotive-feed-import',
                    'type'             => 'plugin',
                    'public_key'       => 'pk_319dd97c8222d0d569532fe5df783',
                    'is_premium'       => false,
                    'premium_suffix'   => 'Pro',
                    'has_addons'       => false,
                    'has_paid_plans'   => true,
                    'is_org_compliant' => true,
                    'menu'             => array(
                        'slug'    => 'automotive-feed-import',
                        'support' => false,
                    ),
                    'is_live'          => true,
                ) );
            }
            return $afi_fs;
        }

        // Init Freemius.
        afi_fs();
        // Signal that SDK was initiated.
        do_action( 'afi_fs_loaded' );
    }
    // Not like register_uninstall_hook(), you do NOT have to use a static function.
    afi_fs()->add_action( 'after_uninstall', 'afi_fs_uninstall_cleanup' );
    function afi_fs_uninstall_cleanup() {
        $slug = 'automotive-feed-import';
        $options = array(
            'xml_file_path',
            'import_frequency',
            'post_title_format',
            'post_content_format',
            'currency_symbol',
            'removed_vehicle_action',
            'migration_done',
            'placeholder_attachment_id',
            'field_mappings',
            'last_feed_url',
            'target_post_type',
            'marker_backfill_done',
            'foreign_cpt_disclaimer_accepted'
        );
        foreach ( $options as $option ) {
            delete_option( $slug . '_' . $option );
        }
        delete_option( 'afi_total_imports' );
        delete_option( 'afi_first_import_time' );
        delete_option( 'afi_last_sync' );
        wp_clear_scheduled_hook( 'update_xml_event' );
        delete_transient( 'afi_activation_notice' );
        delete_transient( 'afi_pro_upsell_gate' );
    }

    /**
     * A class to import vehicle data periodically and display in vehicles edit screen.
     * @author Muhammad Jawaid Shamshad
     *
     */
    class AutomotiveFeedImport {
        private $xml_file;

        private $log_file;

        private $plugin_slug = 'automotive-feed-import';

        private $survey_url = 'https://forms.gle/qEneb8ZeBxnFXuV78';

        // Configurable survey URL
        private $rendering_vehicle_card = false;

        private $settings_hook = '';

        // Captured admin page hook suffix for asset enqueue
        /**
         * Constructor - PHP 8 compatible
         */
        public function __construct() {
            // Get settings or use defaults
            $this->xml_file = $this->get_option( 'xml_file_path' );
            // Set up log file
            $upload_dir = wp_upload_dir();
            $log_dir = $upload_dir['basedir'] . '/automotive-feed-import';
            if ( !file_exists( $log_dir ) ) {
                wp_mkdir_p( $log_dir );
            }
            $this->log_file = $log_dir . '/import-log.txt';
        }

        /**
         * Helper to get plugin options with default fallback
         */
        private function get_option( $key, $default = '' ) {
            return get_option( $this->plugin_slug . '_' . $key, $default );
        }

        /**
         * Helper to update plugin options
         */
        private function update_option( $key, $value ) {
            return update_option( $this->plugin_slug . '_' . $key, $value );
        }

        /**
         * Log errors and messages
         */
        private function log( $message, $is_error = false ) {
            $timestamp = current_time( 'mysql' );
            $level = ( $is_error ? 'ERROR' : 'INFO' );
            $log_entry = '[' . $timestamp . '] [' . $level . '] ' . $message . PHP_EOL;
            // Write to log file
            file_put_contents( $this->log_file, $log_entry, FILE_APPEND );
            // Show admin notice for errors
            if ( $is_error ) {
                add_action( 'admin_notices', function () use($message) {
                    echo '<div class="notice notice-error is-dismissible"><p><strong>' . esc_html__( 'Automotive Inventory Importer Error:', 'automotive-feed-import' ) . '</strong> ' . esc_html( $message ) . '</p></div>';
                } );
            }
        }

        /**
         * Load the plugin text domain for translations.
         *
         * Hooked on `init` (never earlier): WordPress 6.7+ triggers a
         * _doing_it_wrong notice if a text domain is loaded before init.
         *
         * @since 2.7.0
         * @return void
         */
        public function load_textdomain() {
            load_plugin_textdomain( 'automotive-feed-import', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' );
        }

        /**
         * Register custom post type 'vehicles'
         */
        public function register_vehicles_post_type() {
            $labels = array(
                'name'               => __( 'Vehicles', 'automotive-feed-import' ),
                'singular_name'      => __( 'Vehicle', 'automotive-feed-import' ),
                'menu_name'          => __( 'Vehicles', 'automotive-feed-import' ),
                'add_new'            => __( 'Add New', 'automotive-feed-import' ),
                'add_new_item'       => __( 'Add New Vehicle', 'automotive-feed-import' ),
                'edit_item'          => __( 'Edit Vehicle', 'automotive-feed-import' ),
                'new_item'           => __( 'New Vehicle', 'automotive-feed-import' ),
                'view_item'          => __( 'View Vehicle', 'automotive-feed-import' ),
                'search_items'       => __( 'Search Vehicles', 'automotive-feed-import' ),
                'not_found'          => __( 'No vehicles found', 'automotive-feed-import' ),
                'not_found_in_trash' => __( 'No vehicles found in Trash', 'automotive-feed-import' ),
            );
            // When the user is importing into a different post type (one from
            // their theme or another plugin), hide our own Vehicles section
            // from the admin so they don't see two competing inventory areas.
            // We keep it registered and public so any previously imported
            // vehicles still resolve on the frontend rather than 404 — the
            // user just manages inventory in their chosen post type now.
            $importing_elsewhere = 'vehicles' !== $this->get_target_post_type();
            $args = array(
                'labels'             => $labels,
                'public'             => true,
                'publicly_queryable' => true,
                'show_ui'            => !$importing_elsewhere,
                'show_in_menu'       => ( $importing_elsewhere ? false : $this->plugin_slug ),
                'show_in_admin_bar'  => !$importing_elsewhere,
                'query_var'          => true,
                'rewrite'            => array(
                    'slug' => 'vehicles',
                ),
                'capability_type'    => 'post',
                'has_archive'        => true,
                'hierarchical'       => false,
                'supports'           => array(
                    'title',
                    'editor',
                    'thumbnail',
                    'custom-fields'
                ),
            );
            register_post_type( 'vehicles', $args );
        }

        /**
         * Migrate existing 'listing' posts to 'vehicles' post type
         */
        public function migrate_listings_to_vehicles() {
            // Check if migration already done
            if ( $this->get_option( 'migration_done' ) ) {
                return;
            }
            global $wpdb;
            // Update post type from 'listing' to 'vehicles'
            $updated = $wpdb->update(
                $wpdb->posts,
                array(
                    'post_type' => 'vehicles',
                ),
                array(
                    'post_type' => 'listing',
                ),
                array('%s'),
                array('%s')
            );
            if ( $updated !== false ) {
                $this->update_option( 'migration_done', true );
                $this->log( "Migrated {$updated} listings to vehicles post type" );
            }
        }

        /**
         * Backfill the _ais_imported marker on legacy posts.
         *
         * Pre-2.5.0 imports lived in the 'vehicles' CPT without an explicit
         * "this post was created by the importer" marker. Stale handling and
         * the existence check both now require that marker as a hard safety
         * gate. To keep legacy customer setups working, we run a single SQL
         * UPDATE on plugin upgrade that marks every existing 'vehicles' post
         * (the only place imports could have lived before 2.5.0). Idempotent;
         * stops after the first successful run via the migration flag.
         *
         * @since 2.5.0
         */
        public function backfill_imported_marker() {
            if ( $this->get_option( 'marker_backfill_done' ) ) {
                return;
            }
            global $wpdb;
            $updated = $wpdb->query( "INSERT INTO {$wpdb->postmeta} (post_id, meta_key, meta_value)\n\t\t\t\t SELECT p.ID, '_ais_imported', '1'\n\t\t\t\t FROM {$wpdb->posts} p\n\t\t\t\t LEFT JOIN {$wpdb->postmeta} pm\n\t\t\t\t   ON pm.post_id = p.ID AND pm.meta_key = '_ais_imported'\n\t\t\t\t WHERE p.post_type = 'vehicles'\n\t\t\t\t   AND pm.meta_id IS NULL" );
            if ( $updated !== false ) {
                $this->update_option( 'marker_backfill_done', true );
                $this->log( "Backfilled _ais_imported marker on {$updated} legacy vehicle posts" );
            }
        }

        /**
         * Get the post type the importer should write into.
         *
         * Defaults to our own 'vehicles' CPT. The setting lets users target a CPT
         * registered by another plugin or theme so feed data flows into whatever
         * inventory structure their site already uses.
         *
         * @since 2.5.0
         * @return string Post type slug.
         */
        public function get_target_post_type() {
            /* @fs_premium_only */
            $target = $this->get_option( 'target_post_type', 'vehicles' );
            $target = ( is_string( $target ) ? trim( $target ) : '' );
            if ( '' !== $target && post_type_exists( $target ) ) {
                return $target;
            }
            /* end fs_premium_only */
            return 'vehicles';
        }

        /**
         * Detect post types that can serve as import destinations.
         *
         * Returns our own 'vehicles' CPT first, then every other public
         * non-builtin post type registered on the site (by plugins or themes).
         * Each entry includes the slug, human label, and a hint about the
         * source — used to render the destination dropdown on the settings
         * page and to surface known dealership plugins/themes for presets.
         *
         * @since 2.5.0
         * @return array<string, array{slug:string,label:string,source:string,is_own:bool}>
         */
        public function get_target_cpt_choices() {
            $choices = array();
            // Always offer our own CPT first, even if not yet registered (early call).
            $choices['vehicles'] = array(
                'slug'   => 'vehicles',
                'label'  => __( 'Vehicles (built-in)', 'automotive-feed-import' ),
                'source' => __( 'This plugin', 'automotive-feed-import' ),
                'is_own' => true,
            );
            $post_types = get_post_types( array(
                'public'   => true,
                '_builtin' => false,
            ), 'objects' );
            // Best-effort source hints for known dealership plugins/themes. Used to
            // label the dropdown entries and to drive Pro preset detection later.
            $known_sources = array(
                'inventory_vehicle'             => 'Inventory Presser',
                'listings'                      => 'Motors (StyleMix)',
                'mvl_vehicle'                   => 'Motors (StyleMix)',
                'stm_listings'                  => 'Motors (StyleMix) classic',
                'dealertrend_vehicle_inventory' => 'CarDealerPress / DealerTrend',
                'car'                           => __( 'Generic dealership theme', 'automotive-feed-import' ),
                'vehicle'                       => __( 'Generic dealership theme', 'automotive-feed-import' ),
                'auto-listing'                  => 'Auto Listings',
                'cardemon_vehicle'              => 'Car Demon',
            );
            foreach ( $post_types as $slug => $obj ) {
                if ( 'vehicles' === $slug ) {
                    // Already added above.
                    continue;
                }
                $label = ( isset( $obj->labels->singular_name ) && $obj->labels->singular_name ? $obj->labels->singular_name : $obj->label );
                $source = ( isset( $known_sources[$slug] ) ? $known_sources[$slug] : sprintf( __( 'Custom post type (%s)', 'automotive-feed-import' ), $slug ) );
                $choices[$slug] = array(
                    'slug'   => $slug,
                    'label'  => $label,
                    'source' => $source,
                    'is_own' => false,
                );
            }
            return $choices;
        }

        /**
         * Initialize plugin
         */
        public function init() {
            // Register post type
            $this->register_vehicles_post_type();
            // Migrate existing listings
            $this->migrate_listings_to_vehicles();
            // One-time backfill of the _ais_imported marker on legacy
            // 'vehicles' posts so stale handling and existence checks
            // (which now require the marker) keep working for upgraders.
            $this->backfill_imported_marker();
            // schedule an event
            $this->schedule();
            // Flush rewrite rules
            flush_rewrite_rules();
            // Set activation notice flag
            set_transient( 'afi_activation_notice', true, 60 * 60 * 24 );
            // 24 hours
        }

        /**
         * Get every Pro preset registered for the current target CPT.
         *
         * Free builds have no /pro/ folder, so the registry is empty and
         * this always returns []. Pro builds with /pro/ shipped return
         * whichever presets match the target — typically zero or one.
         *
         * @since 2.5.0
         * @return array<string, AFI_Preset_Base>
         */
        public function get_presets_for_current_target() {
            if ( !function_exists( 'afi_get_preset_registry' ) ) {
                return array();
            }
            return afi_get_preset_registry()->for_cpt( $this->get_target_post_type() );
        }

        /**
         * Is the current site licensed to use Pro features?
         *
         * Wraps Freemius's runtime check with a safe default so the call
         * site doesn't need to repeat the function_exists guards.
         *
         * @since 2.5.0
         * @return bool
         */
        public function is_pro_active() {
            return function_exists( 'afi_fs' ) && afi_fs()->can_use_premium_code();
        }

        /**
         * Has the third-party post type disclaimer been accepted for the given CPT?
         *
         * Acceptance is stored site-wide (option, not user meta) so that cron
         * imports — which have no logged-in user context — can check the flag
         * before running. Required for every CPT that isn't our own 'vehicles'.
         *
         * @since 2.5.0
         * @param string $cpt Target post type slug.
         * @return bool
         */
        public function has_accepted_foreign_cpt_disclaimer( $cpt ) {
            $accepted = $this->get_option( 'foreign_cpt_disclaimer_accepted', array() );
            if ( !is_array( $accepted ) ) {
                $accepted = array();
            }
            return in_array( $cpt, $accepted, true );
        }

        /**
         * Record acceptance of the third-party post type disclaimer for a given CPT.
         *
         * @since 2.5.0
         * @param string $cpt Target post type slug.
         */
        public function mark_foreign_cpt_disclaimer_accepted( $cpt ) {
            $accepted = $this->get_option( 'foreign_cpt_disclaimer_accepted', array() );
            if ( !is_array( $accepted ) ) {
                $accepted = array();
            }
            if ( !in_array( $cpt, $accepted, true ) ) {
                $accepted[] = $cpt;
                $this->update_option( 'foreign_cpt_disclaimer_accepted', $accepted );
            }
        }

        /**
         * Render the blocking disclaimer notice when targeting a third-party CPT
         * that the current admin has not yet acknowledged.
         *
         * The notice carries a checkbox + Accept form posted to admin-post.php.
         * While unaccepted, the import hard-gate in update_data() refuses to run,
         * so the site owner physically cannot lose data by accident.
         *
         * @since 2.5.0
         */
        public function render_foreign_cpt_disclaimer_notice() {
            if ( !current_user_can( 'manage_options' ) ) {
                return;
            }
            $target = $this->get_target_post_type();
            if ( 'vehicles' === $target || $this->has_accepted_foreign_cpt_disclaimer( $target ) ) {
                return;
            }
            $choices = $this->get_target_cpt_choices();
            $label = ( isset( $choices[$target]['label'] ) ? $choices[$target]['label'] : $target );
            $source = ( isset( $choices[$target]['source'] ) ? $choices[$target]['source'] : '' );
            ?>
			<div class="notice notice-info" style="border-left-color:#2271b1;">
				<h3 style="margin-top:12px;"><?php 
            esc_html_e( 'One quick step before your first import', 'automotive-feed-import' );
            ?></h3>
				<p>
					<?php 
            printf( 
                /* translators: 1: post type label, 2: source plugin/theme */
                esc_html__( 'Great choice — your inventory will be sent into %1$s (%2$s), and that theme or plugin will display your vehicles using its own design. Just confirm below and you\'re ready to go.', 'automotive-feed-import' ),
                '<strong>' . esc_html( $label ) . '</strong>',
                esc_html( $source )
             );
            ?>
				</p>
				<p style="margin-bottom:6px;"><?php 
            esc_html_e( 'A couple of good habits before importing into any system:', 'automotive-feed-import' );
            ?></p>
				<ul style="list-style:disc;padding-left:22px;margin-top:0;">
					<li><?php 
            esc_html_e( 'Keep a recent backup of your site — quick to do and always worth it.', 'automotive-feed-import' );
            ?></li>
					<li><?php 
            esc_html_e( 'If you can, try it on a staging copy first to see how your vehicles look.', 'automotive-feed-import' );
            ?></li>
				</ul>
				<p style="color:#646970;font-size:13px;">
					<?php 
            esc_html_e( 'Good to know: we only ever update vehicles this plugin imported, so anything you added by hand stays untouched. As with any import tool, please make sure you have a backup — we can\'t take responsibility for issues caused by another plugin or theme.', 'automotive-feed-import' );
            ?>
				</p>
				<form method="post" action="<?php 
            echo esc_url( admin_url( 'admin-post.php' ) );
            ?>" style="margin:14px 0;">
					<input type="hidden" name="action" value="afi_accept_foreign_cpt_disclaimer" />
					<input type="hidden" name="target_cpt" value="<?php 
            echo esc_attr( $target );
            ?>" />
					<?php 
            wp_nonce_field( 'afi_accept_foreign_cpt_disclaimer', 'afi_disclaimer_nonce' );
            ?>
					<label style="display:block;margin-bottom:10px;">
						<input type="checkbox" name="afi_accept_confirmed" value="1" required />
						<?php 
            printf( 
                /* translators: %s: post type label */
                esc_html__( 'I have a backup and I\'m ready to import into %s.', 'automotive-feed-import' ),
                esc_html( $label )
             );
            ?>
					</label>
					<button type="submit" class="button button-primary">
						<?php 
            esc_html_e( 'Confirm and start importing', 'automotive-feed-import' );
            ?>
					</button>
					<a href="<?php 
            echo esc_url( $this->settings_page_url() );
            ?>" class="button">
						<?php 
            esc_html_e( 'Use the built-in Vehicles section instead', 'automotive-feed-import' );
            ?>
					</a>
				</form>
			</div>
			<?php 
        }

        /**
         * Render the mapping-assist block shown above the field-mapping table
         * for third-party target CPTs.
         *
         * Pro: one "Auto-map fields" button that applies a hand-tuned preset
         * when one exists for the target, or the generic best-guess mapper.
         * Free: no auto button — the user maps by hand, with an upgrade prompt.
         * Uses a GET link with a nonce rather than a nested <form>, which is
         * invalid inside the mapping form.
         *
         * @since 2.5.0
         */
        public function render_mapping_assist_block() {
            $target = $this->get_target_post_type();
            if ( 'vehicles' === $target ) {
                return;
            }
            $choices = $this->get_target_cpt_choices();
            $label = ( isset( $choices[$target]['label'] ) ? $choices[$target]['label'] : $target );
            echo '<div style="background:#eef7ff;border-left:4px solid #2271b1;padding:14px 16px;margin:16px 0;max-width:820px;">';
            if ( $this->is_pro_active() ) {
                $auto_url = wp_nonce_url( admin_url( 'admin-post.php?action=afi_auto_map' ), 'afi_auto_map' );
                $presets = $this->get_presets_for_current_target();
                $preset = ( !empty( $presets ) ? reset( $presets ) : null );
                if ( $preset ) {
                    $ready = $preset->detect();
                    echo '<p style="margin:0 0 6px 0;font-size:14px;"><strong>';
                    printf( 
                        /* translators: %s: preset label */
                        esc_html__( 'One-click preset: %s', 'automotive-feed-import' ),
                        esc_html( $preset->get_label() )
                     );
                    echo '</strong></p>';
                    echo '<p style="margin:0 0 10px 0;">';
                    esc_html_e( 'A hand-tuned mapping built specifically for this plugin\'s fields and categories — the most accurate option when it\'s available.', 'automotive-feed-import' );
                    echo '</p>';
                    echo '<a href="' . esc_url( $auto_url ) . '" class="button button-primary"';
                    if ( !$ready ) {
                        echo ' onclick="return confirm(\'' . esc_attr__( 'The host plugin doesn\'t appear to be active yet. Apply the preset anyway?', 'automotive-feed-import' ) . '\');"';
                    }
                    echo '>';
                    printf( 
                        /* translators: %s: preset label */
                        esc_html__( 'Apply %s preset', 'automotive-feed-import' ),
                        esc_html( $preset->get_label() )
                     );
                    echo '</a>';
                    if ( !$ready ) {
                        echo ' <em style="margin-left:8px;color:#996800;">' . esc_html__( '(host plugin not detected)', 'automotive-feed-import' ) . '</em>';
                    }
                } else {
                    echo '<p style="margin:0 0 6px 0;font-size:14px;"><strong>' . esc_html__( 'Match fields automatically', 'automotive-feed-import' ) . '</strong></p>';
                    echo '<p style="margin:0 0 10px 0;">';
                    printf( 
                        /* translators: %s: post type label */
                        esc_html__( 'Let the importer line your feed fields up with the fields and categories that %s already uses, so you don\'t have to map them by hand.', 'automotive-feed-import' ),
                        '<strong>' . esc_html( $label ) . '</strong>'
                     );
                    echo '</p>';
                    echo '<a href="' . esc_url( $auto_url ) . '" class="button button-primary">' . esc_html__( 'Auto-map fields', 'automotive-feed-import' ) . '</a>';
                }
            } else {
                // Free: no auto-mapping. Guide the user to map by hand, with an upsell.
                echo '<p style="margin:0 0 6px 0;font-size:14px;"><strong>' . esc_html__( 'Map your fields below', 'automotive-feed-import' ) . '</strong></p>';
                echo '<p style="margin:0 0 10px 0;">';
                printf( 
                    /* translators: %s: post type label */
                    esc_html__( 'Match each feed field to the right %s field in the table below. Want it done for you? Pro maps your fields automatically — and includes ready-made setups for popular dealer plugins like Inventory Presser and Motors.', 'automotive-feed-import' ),
                    '<strong>' . esc_html( $label ) . '</strong>'
                 );
                echo '</p>';
                echo '<span style="display:inline-block;background:#ffeaa7;color:#996800;padding:2px 8px;border-radius:3px;font-size:12px;font-weight:600;margin-right:8px;">PRO</span>';
                if ( function_exists( 'afi_fs' ) ) {
                    echo '<a href="' . esc_url( afi_fs()->get_upgrade_url() ) . '" class="button button-secondary">' . esc_html__( 'Upgrade to Pro', 'automotive-feed-import' ) . '</a>';
                }
            }
            echo '</div>';
        }

        /**
         * Handle the disclaimer acceptance form post.
         *
         * @since 2.5.0
         */
        public function handle_foreign_cpt_disclaimer_accept() {
            if ( !isset( $_POST['afi_disclaimer_nonce'] ) || !wp_verify_nonce( $_POST['afi_disclaimer_nonce'], 'afi_accept_foreign_cpt_disclaimer' ) ) {
                wp_die( esc_html__( 'Security check failed.', 'automotive-feed-import' ) );
            }
            if ( !current_user_can( 'manage_options' ) ) {
                wp_die( esc_html__( 'Unauthorized.', 'automotive-feed-import' ) );
            }
            if ( empty( $_POST['afi_accept_confirmed'] ) || empty( $_POST['target_cpt'] ) ) {
                wp_safe_redirect( $this->settings_page_url() );
                exit;
            }
            $target = sanitize_key( wp_unslash( $_POST['target_cpt'] ) );
            if ( '' === $target || 'vehicles' === $target || !post_type_exists( $target ) ) {
                wp_safe_redirect( $this->settings_page_url() );
                exit;
            }
            $this->mark_foreign_cpt_disclaimer_accepted( $target );
            set_transient( 'afi_admin_notice', array(
                'type'    => 'success',
                'message' => sprintf( 
                    /* translators: %s: post type slug */
                    __( 'Disclaimer accepted for "%s". Imports into this post type are now enabled.', 'automotive-feed-import' ),
                    $target
                 ),
            ), 30 );
            wp_safe_redirect( $this->settings_page_url() );
            exit;
        }

        /**
         * Display activation notice after plugin is activated
         */
        public function display_activation_notice() {
            // Only show if activation flag is set
            if ( !get_transient( 'afi_activation_notice' ) ) {
                return;
            }
            // Only show to users who can manage options
            if ( !current_user_can( 'manage_options' ) ) {
                return;
            }
            // If this user already dismissed the notice, do not show again
            if ( get_user_meta( get_current_user_id(), 'afi_activation_notice_dismissed', true ) ) {
                return;
            }
            ?>
			<div class="notice notice-info is-dismissible" data-dismiss-type="activation">
				<p style="font-size: 16px;">
					<strong><?php 
            esc_html_e( 'Automotive Inventory Importer is ready!', 'automotive-feed-import' );
            ?></strong>
					<?php 
            esc_html_e( 'Ready to see your cars on your site?', 'automotive-feed-import' );
            ?>
					<a href="<?php 
            echo esc_url( $this->settings_page_url() );
            ?>"
						class="button button-primary" style="margin-left: 10px;">
						<?php 
            esc_html_e( 'Start Your First Import', 'automotive-feed-import' );
            ?>
					</a>
				</p>
			</div>
		<?php 
        }

        /**
         * Display transient admin notices (used after redirects)
         */
        public function display_transient_notices() {
            $notice = get_transient( 'afi_admin_notice' );
            if ( $notice ) {
                $type = ( isset( $notice['type'] ) ? $notice['type'] : 'info' );
                $message = ( isset( $notice['message'] ) ? $notice['message'] : '' );
                if ( $message ) {
                    echo '<div class="notice notice-' . esc_attr( $type ) . ' is-dismissible"><p>' . wp_kses_post( $message ) . '</p></div>';
                }
                // Delete transient so it only shows once
                delete_transient( 'afi_admin_notice' );
            }
        }

        /**
         * Uninitialize plugin
         */
        public function uninit() {
            // clear all events
            $this->unschedule();
        }

        /**
         * Clear schedule the plugin
         */
        public function unschedule() {
            wp_clear_scheduled_hook( 'update_xml_event' );
        }

        /**
         * Reschedule the cron event with the current frequency setting
         */
        public function reschedule() {
            $this->unschedule();
            $this->schedule();
        }

        /**
         * Schedule the plugin
         */
        public function schedule() {
            // Get frequency from settings (default 10 minutes)
            $frequency = $this->get_option( 'import_frequency', 'tenminute' );
            // Gate C: sub-hourly frequencies are Pro-only. Clamp free users to
            // twice-daily even if a sub-hourly value is stored (e.g., a pre-gating
            // install or a tampered free build). This pairs with the build-marker
            // stripping of the sub-hourly interval definitions from the free package.
            if ( !$this->is_pro_active() ) {
                $free_allowed = array('twicedaily', 'daily');
                if ( !in_array( $frequency, $free_allowed, true ) ) {
                    $frequency = 'twicedaily';
                }
                // Correct an already-running sub-hourly cron (e.g., user downgraded
                // from Pro). wp_next_scheduled() would leave it running unchanged, so
                // we explicitly unschedule it here so the event gets re-created below
                // at the allowed frequency.
                $current_schedule = wp_get_schedule( 'update_xml_event' );
                if ( $current_schedule && !in_array( $current_schedule, $free_allowed, true ) ) {
                    $this->unschedule();
                }
            }
            // check if event is not defined, then schedule one
            if ( !wp_next_scheduled( 'update_xml_event' ) ) {
                wp_schedule_event( time(), $frequency, 'update_xml_event' );
            }
        }

        /**
         * Defines custom intervals including 10 minute default
         * @return Interval array  
         */
        public function define_interval( $schedules ) {
            /* @fs_premium_only */
            $schedules['tenminute'] = array(
                'interval' => 60 * 10,
                'display'  => __( 'Once Every 10 Minutes', 'automotive-feed-import' ),
            );
            $schedules['fiveminute'] = array(
                'interval' => 60 * 5,
                'display'  => __( 'Once Every 5 Minutes', 'automotive-feed-import' ),
            );
            $schedules['fifteenminute'] = array(
                'interval' => 60 * 15,
                'display'  => __( 'Once Every 15 Minutes', 'automotive-feed-import' ),
            );
            $schedules['thirtyminute'] = array(
                'interval' => 60 * 30,
                'display'  => __( 'Once Every 30 Minutes', 'automotive-feed-import' ),
            );
            /* end fs_premium_only */
            return $schedules;
        }

        /**
         * Load the data from xml file and return as an array
         * @return Data array on success, false on failure
         */
        public function load_xml() {
            // Handle remote URL (http/https)
            if ( preg_match( '#^https?://#i', $this->xml_file ) ) {
                $response = wp_remote_get( $this->xml_file, array(
                    'timeout' => 30,
                ) );
                if ( is_wp_error( $response ) ) {
                    $this->log( "Failed to fetch remote XML: " . $response->get_error_message(), true );
                    return false;
                }
                $code = wp_remote_retrieve_response_code( $response );
                if ( $code < 200 || $code >= 300 ) {
                    $this->log( "Remote XML returned error code: {$code}", true );
                    return false;
                }
                $body = wp_remote_retrieve_body( $response );
                if ( empty( $body ) ) {
                    $this->log( "Remote XML file is empty: {$this->xml_file}", true );
                    return false;
                }
                libxml_use_internal_errors( true );
                $xml = simplexml_load_string( $body );
                if ( !$xml ) {
                    $error_msg = "Failed loading XML from {$this->xml_file}";
                    $this->log( $error_msg, true );
                    foreach ( libxml_get_errors() as $error ) {
                        $this->log( "XML Error: " . $error->message, true );
                    }
                    libxml_clear_errors();
                    return false;
                }
            } else {
                // Handle local file path
                if ( !file_exists( $this->xml_file ) ) {
                    $this->log( "XML file not found: {$this->xml_file}", true );
                    return false;
                }
                // load the xml file
                $xml = simplexml_load_file( $this->xml_file );
                if ( !$xml ) {
                    $error_msg = "Failed loading XML from {$this->xml_file}";
                    $this->log( $error_msg, true );
                    foreach ( libxml_get_errors() as $error ) {
                        $this->log( "XML Error: " . $error->message, true );
                    }
                    return false;
                }
            }
            // First pass: parse every direct child of the root into a
            // name + fields pair, and tally how often each tag name appears
            // among children that actually contain sub-fields.
            //
            // We can't assume every child of the root is a vehicle. Real feeds
            // (DealerCenter is one) place non-vehicle metadata siblings next to
            // the vehicle records — an export date, a record count, a dealer-info
            // block — often as the very first child. Treating those as vehicles
            // polluted field detection (which sampled the first child) and spammed
            // the import log with "missing stock_number". So we detect the actual
            // repeating vehicle element instead of trusting position.
            $children = array();
            $name_counts = array();
            foreach ( $xml->children() as $child ) {
                $fields = array();
                foreach ( $child as $grand_child ) {
                    $fields[$grand_child->getName()] = strip_tags( $grand_child->asXML() );
                }
                $name = $child->getName();
                $children[] = array($name, $fields);
                if ( !empty( $fields ) ) {
                    $name_counts[$name] = ( isset( $name_counts[$name] ) ? $name_counts[$name] + 1 : 1 );
                }
            }
            // The vehicle record is the most frequently repeated element that
            // carries sub-fields — a feed has many vehicles and at most a handful
            // of metadata siblings.
            $record_name = '';
            if ( !empty( $name_counts ) ) {
                arsort( $name_counts );
                $record_name = array_key_first( $name_counts );
            }
            // Second pass: keep only the vehicle records; skip metadata siblings.
            // Fallback: if no child had sub-fields (a degenerate feed we don't
            // recognise), keep the original behaviour so simple feeds never regress.
            $units = array();
            foreach ( $children as $entry ) {
                list( $name, $fields ) = $entry;
                if ( '' !== $record_name ) {
                    if ( $name === $record_name ) {
                        array_push( $units, $fields );
                    }
                } else {
                    array_push( $units, $fields );
                }
            }
            $this->log( "Successfully loaded " . count( $units ) . " units from XML" );
            return $units;
        }

        /**
         * Detect whether the configured feed is a CSV file
         */
        private function is_csv_feed() {
            return (bool) preg_match( '/\\.csv(\\?.*)?$/i', $this->xml_file );
        }

        /**
         * Load data from a CSV feed (remote URL or local path) and return as an array.
         * The first row must be a header row whose values become the field names.
         * @return array|false
         */
        public function load_csv() {
            $raw = '';
            if ( preg_match( '#^https?://#i', $this->xml_file ) ) {
                $response = wp_remote_get( $this->xml_file, array(
                    'timeout' => 30,
                ) );
                if ( is_wp_error( $response ) ) {
                    $this->log( "Failed to fetch remote CSV: " . $response->get_error_message(), true );
                    return false;
                }
                $code = wp_remote_retrieve_response_code( $response );
                if ( $code < 200 || $code >= 300 ) {
                    $this->log( "Remote CSV returned error code: {$code}", true );
                    return false;
                }
                $raw = wp_remote_retrieve_body( $response );
                if ( empty( $raw ) ) {
                    $this->log( "Remote CSV file is empty: {$this->xml_file}", true );
                    return false;
                }
            } else {
                if ( !file_exists( $this->xml_file ) ) {
                    $this->log( "CSV file not found: {$this->xml_file}", true );
                    return false;
                }
                $raw = file_get_contents( $this->xml_file );
                if ( $raw === false ) {
                    $this->log( "Could not read CSV file: {$this->xml_file}", true );
                    return false;
                }
            }
            // Normalise line endings
            $raw = str_replace( "\r\n", "\n", str_replace( "\r", "\n", $raw ) );
            // Strip UTF-8 BOM if present
            if ( str_starts_with( $raw, "﻿" ) ) {
                $raw = substr( $raw, 3 );
            }
            $lines = explode( "\n", trim( $raw ) );
            if ( count( $lines ) < 2 ) {
                $this->log( "CSV file has no data rows: {$this->xml_file}", true );
                return false;
            }
            $headers = str_getcsv( array_shift( $lines ) );
            $headers = array_map( 'trim', $headers );
            $units = array();
            foreach ( $lines as $line ) {
                if ( trim( $line ) === '' ) {
                    continue;
                }
                $values = str_getcsv( $line );
                // Pad or truncate to match header count
                $values = array_slice( array_pad( $values, count( $headers ), '' ), 0, count( $headers ) );
                $unit = array();
                foreach ( $headers as $index => $header ) {
                    if ( $header !== '' ) {
                        $unit[$header] = trim( $values[$index] );
                    }
                }
                $units[] = $unit;
            }
            $this->log( "Successfully loaded " . count( $units ) . " units from CSV" );
            return $units;
        }

        /**
         * Collapse the legacy "brand" field into "manufacturer".
         *
         * For cars "brand" and "manufacturer" are the same thing (the make), so
         * we keep a single field. If the mapped row carries a "brand" value and
         * "manufacturer" is empty, promote it; then drop "brand" so it never
         * reaches post meta or a template token as a separate key. This also
         * covers any saved mapping that still targets the retired "brand" field.
         *
         * @since 2.5.0
         * @param array $unit Mapped feed row.
         * @return array Row with "brand" folded into "manufacturer".
         */
        private function consolidate_make_field( $unit ) {
            if ( isset( $unit['brand'] ) ) {
                $brand = trim( (string) $unit['brand'] );
                if ( '' !== $brand && (empty( $unit['manufacturer'] ) || '' === trim( (string) $unit['manufacturer'] )) ) {
                    $unit['manufacturer'] = $brand;
                }
                unset($unit['brand']);
            }
            return $unit;
        }

        /**
         * Parse post format template with tokens
         */
        private function parse_template( $template, $unit ) {
            // Replace tokens like {manufacturer}, {model}, etc.
            $replacements = array();
            foreach ( $unit as $key => $value ) {
                $replacements['{' . $key . '}'] = $value;
            }
            $result = str_replace( array_keys( $replacements ), array_values( $replacements ), $template );
            // Drop any tokens the feed didn't map (e.g. {style} when the feed has
            // no trim column) so we never leave a literal "{token}" in the output,
            // then collapse the whitespace left behind and trim.
            $result = preg_replace( '/\\{[^}]+\\}/', '', $result );
            $result = trim( preg_replace( '/\\s+/', ' ', $result ) );
            return $result;
        }

        /**
         * Deduce a post title and description for a listing imported into a
         * third-party CPT, where the vehicle token templates don't apply.
         *
         * Title, first non-empty wins:
         *   1. An explicit title-like feed field (title, name, headline, ...).
         *   2. Composed "Year Make Model Trim" from common feed fields.
         *   3. The unique ID (stock_number / VIN).
         *   4. A generic "Imported listing" fallback.
         * Description: a feed description-like field if present, else empty.
         *
         * Self-contained and free-build safe — must not call any Pro-only
         * (__premium_only) mapping/concept helpers, since universal CPT
         * targeting ships in the free version.
         *
         * @since 2.5.0
         * @param array $raw_unit    Pre-mapping feed row, keyed by feed field names.
         * @param array $mapped_unit Post-mapping row (used for stock_number fallback).
         * @return array{0: string, 1: string} [ $title, $content ]
         */
        private function deduce_listing_title_content( $raw_unit, $mapped_unit ) {
            // Normalised lookup of the raw feed row: "Model Year" => "modelyear".
            $lookup = array();
            if ( is_array( $raw_unit ) ) {
                foreach ( $raw_unit as $key => $value ) {
                    if ( !is_scalar( $value ) ) {
                        continue;
                    }
                    $norm = preg_replace( '/[^a-z0-9]/', '', strtolower( (string) $key ) );
                    if ( '' !== $norm && !isset( $lookup[$norm] ) ) {
                        $lookup[$norm] = trim( (string) $value );
                    }
                }
            }
            $pick = static function ( array $names ) use($lookup) {
                foreach ( $names as $name ) {
                    if ( isset( $lookup[$name] ) && '' !== $lookup[$name] ) {
                        return $lookup[$name];
                    }
                }
                return '';
            };
            // 1) Explicit title-like field.
            $title = $pick( array(
                'title',
                'name',
                'headline',
                'heading',
                'vehicletitle',
                'listingtitle',
                'adtitle'
            ) );
            // 2) Compose "Year Make Model Trim".
            if ( '' === $title ) {
                $year = $pick( array('year', 'modelyear', 'yr') );
                $make = $pick( array('make', 'manufacturer', 'brand') );
                $model = $pick( array('model', 'modelname') );
                $trim = $pick( array('trim', 'series', 'edition') );
                $parts = array();
                if ( '' !== $year ) {
                    $parts[] = $year;
                    // numeric — leave as-is
                }
                foreach ( array($make, $model, $trim) as $part ) {
                    if ( '' !== $part ) {
                        $parts[] = $this->format_display_value( $part );
                    }
                }
                $title = trim( implode( ' ', $parts ) );
            }
            // 3) Unique ID / VIN.
            if ( '' === $title ) {
                if ( isset( $mapped_unit['stock_number'] ) && '' !== trim( (string) $mapped_unit['stock_number'] ) ) {
                    $title = trim( (string) $mapped_unit['stock_number'] );
                } else {
                    $title = $pick( array(
                        'vin',
                        'stocknumber',
                        'stockno',
                        'stockid'
                    ) );
                }
            }
            // 4) Generic fallback.
            if ( '' === $title ) {
                $title = __( 'Imported listing', 'automotive-feed-import' );
            }
            // Description: a feed description-like field, if any.
            $content = $pick( array(
                'description',
                'comments',
                'notes',
                'longdescription',
                'vehicledescription',
                'remarks',
                'details'
            ) );
            return array($title, $content);
        }

        /**
         * Inserts data into database
         * @param Associative array containing data to be inserted in database
         * @return boolean: Post Id on success, false on failure
         */
        public function add_listing( $unit, $raw_unit = null ) {
            // check for valid unit
            if ( !isset( $unit ) ) {
                $this->log( "Invalid unit data provided", true );
                return false;
            }
            $target = $this->get_target_post_type();
            if ( 'vehicles' === $target ) {
                // Native type: build title/description from the user's token
                // templates (Page Templates tab). Tokens resolve here because the
                // native mapping keeps canonical field names.
                $title_template = $this->get_option( 'post_title_format', '{model_year} {manufacturer} {model} {style}' );
                $content_template = $this->get_option( 'post_content_format', $this->default_content_format() );
                $title = $this->parse_template( $title_template, $unit );
                $content = $this->parse_template( $content_template, $unit );
            } else {
                // Third-party CPT: the vehicle tokens don't map to this post
                // type's fields, so deduce a sensible title/description from the
                // raw feed row instead of leaving unresolved {tokens} behind.
                list( $title, $content ) = $this->deduce_listing_title_content( ( $raw_unit ? $raw_unit : $unit ), $unit );
            }
            $title = sanitize_text_field( $title );
            $content = wp_kses_post( $content );
            // define post object
            $new_post = array(
                'post_title'   => $title,
                'post_content' => $content,
                'post_status'  => 'publish',
                'post_type'    => $this->get_target_post_type(),
            );
            // insert the post into the database
            $post_id = wp_insert_post( $new_post, true );
            // check if post added successfully
            if ( is_wp_error( $post_id ) ) {
                // error, cannot insert post
                $error_msg = $post_id->get_error_message();
                $this->log( "Failed to create vehicle: " . $error_msg, true );
                return false;
            }
            // Mark the post as imported by this plugin. This marker is the
            // hard-required guard for stale handling and bulk operations so we
            // never touch posts the site owner created manually — especially
            // important when targeting a third-party CPT.
            update_post_meta( $post_id, '_ais_imported', 1 );
            update_post_meta( $post_id, '_ais_imported_at', current_time( 'mysql' ) );
            // Remember the title we generated so a later re-sync can refresh it
            // while leaving dealer-edited titles untouched (see update_inventory).
            update_post_meta( $post_id, '_ais_generated_title', $title );
            $this->log( "Created new vehicle (ID: {$post_id}): " . $title );
            return $post_id;
        }

        /**
         * Add/Update data in database
         * @param Post Id to update
         * @param Unit data to be updated
         */
        public function update_inventory( $post_id, $unit ) {
            foreach ( $unit as $key => $value ) {
                // Taxonomy targets are encoded as "tax:taxonomy_slug" in the
                // mapping target column. Route those to term assignment
                // instead of post meta so users can map a feed field directly
                // to a taxonomy registered by the target plugin/theme
                // (e.g. Inventory Presser's "make" or "body-style" taxonomies).
                if ( is_string( $key ) && 0 === strpos( $key, 'tax:' ) ) {
                    $this->assign_terms_from_value( $post_id, substr( $key, 4 ), $value );
                    continue;
                }
                update_post_meta( $post_id, $key, $value );
                $this->update_theme_compatibility_field( $post_id, $key, $value );
            }
            // Shed the retired "brand" meta as each vehicle re-syncs. The make
            // now lives only in "manufacturer" (see consolidate_make_field).
            delete_post_meta( $post_id, 'brand' );
            // Ensure the import marker is set. Idempotent: writes once, then no-ops.
            // This also lazily backfills the marker on legacy posts created before
            // the marker was introduced — once a legacy post is re-synced, stale
            // handling can act on it again.
            if ( !get_post_meta( $post_id, '_ais_imported', true ) ) {
                update_post_meta( $post_id, '_ais_imported', 1 );
                update_post_meta( $post_id, '_ais_imported_at', current_time( 'mysql' ) );
            }
            // Refresh the post title from the current template so existing
            // listings pick up template/default changes (e.g. the old
            // "{manufacturer} {brand}" → "Honda Honda" titles). Only for the
            // native CPT, where the vehicle tokens apply. We overwrite the title
            // only when the dealer hasn't hand-edited it: i.e. the current title
            // still matches the title we last generated, or the marker is absent
            // (a legacy post, which was always importer-generated). When the
            // dealer has edited the title, the marker won't match and we leave it.
            if ( 'vehicles' === $this->get_target_post_type() ) {
                $title_template = $this->get_option( 'post_title_format', '{model_year} {manufacturer} {model} {style}' );
                $new_title = sanitize_text_field( $this->parse_template( $title_template, $unit ) );
                if ( '' !== $new_title ) {
                    $current_title = get_the_title( $post_id );
                    $generated_title = get_post_meta( $post_id, '_ais_generated_title', true );
                    $is_dealer_edited = '' !== $generated_title && $current_title !== $generated_title;
                    if ( !$is_dealer_edited ) {
                        if ( $current_title !== $new_title ) {
                            wp_update_post( array(
                                'ID'         => $post_id,
                                'post_title' => $new_title,
                            ) );
                        }
                        if ( $generated_title !== $new_title ) {
                            update_post_meta( $post_id, '_ais_generated_title', $new_title );
                        }
                    }
                }
            }
        }

        /**
         * Update theme compatibility fields
         * @param int $post_id Post ID
         * @param string $key Field key
         * @param mixed $value Field value
         */
        private function update_theme_compatibility_field( $post_id, $key, $value ) {
            switch ( $key ) {
                case 'manufacturer':
                    update_post_meta( $post_id, 'manufacturer_level2_value', $value );
                    break;
                case 'model_year':
                    update_post_meta( $post_id, 'year_value', $value );
                    break;
                case 'special_web_price':
                    update_post_meta( $post_id, 'price_value', $value );
                    break;
                case 'mileage':
                    update_post_meta( $post_id, 'mileage_value', $value );
                    break;
                case 'exterior_color':
                    update_post_meta( $post_id, 'color_value', $value );
                    break;
            }
        }

        /**
         * Import images from feed into Media Library and attach to vehicle
         */
        private function process_images( $post_id, $unit ) {
            $image_urls = array();
            // Combined list fields (comma / pipe / whitespace separated)
            foreach ( array('image_urls', 'images', 'photos') as $field ) {
                if ( !empty( $unit[$field] ) && is_string( $unit[$field] ) ) {
                    $parts = preg_split( '/[,\\|\\s]+/', $unit[$field] );
                    foreach ( $parts as $url ) {
                        $url = trim( $url );
                        if ( $url !== '' && preg_match( '#^https?://#i', $url ) ) {
                            $image_urls[] = esc_url_raw( $url );
                        }
                    }
                }
            }
            // Individual image/photo fields: image1, image_2, photo3, etc.
            foreach ( $unit as $key => $value ) {
                if ( !is_string( $value ) || $value === '' ) {
                    continue;
                }
                if ( preg_match( '/^(image|photo)[_\\-]?\\d*$/i', (string) $key ) && preg_match( '#^https?://#i', $value ) ) {
                    $image_urls[] = esc_url_raw( $value );
                }
            }
            $image_urls = array_values( array_unique( $image_urls ) );
            if ( empty( $image_urls ) ) {
                // No images in feed: ensure placeholder is set if needed
                if ( !has_post_thumbnail( $post_id ) ) {
                    $placeholder_id = $this->ensure_placeholder_attachment();
                    if ( $placeholder_id ) {
                        set_post_thumbnail( $post_id, $placeholder_id );
                    }
                }
                return;
            }
            // Avoid re-importing the same URLs
            $already_imported = get_post_meta( $post_id, '_afi_imported_image_urls', true );
            if ( !is_array( $already_imported ) ) {
                $already_imported = array();
            }
            require_once ABSPATH . 'wp-admin/includes/file.php';
            require_once ABSPATH . 'wp-admin/includes/media.php';
            require_once ABSPATH . 'wp-admin/includes/image.php';
            $new_imported = $already_imported;
            $index = 0;
            foreach ( $image_urls as $url ) {
                if ( in_array( $url, $already_imported, true ) ) {
                    $index++;
                    continue;
                }
                // Use download_url + media_handle_sideload instead of media_sideload_image
                // so that URLs without a file extension (e.g. placehold.co, CDN tokens) are accepted.
                $tmp = download_url( $url );
                if ( is_wp_error( $tmp ) ) {
                    $this->log( "Failed to download image {$url} for post {$post_id}: " . $tmp->get_error_message(), true );
                    $index++;
                    continue;
                }
                // Build a filename with the correct extension.
                // When the URL path has no image extension, detect the actual MIME type
                // of the downloaded file so WordPress's filetype check doesn't reject it.
                $url_path = parse_url( $url, PHP_URL_PATH );
                $filename = ( $url_path ? basename( $url_path ) : '' );
                if ( !preg_match( '/\\.(jpe?g|png|gif|webp)$/i', $filename ) ) {
                    $mime_to_ext = array(
                        'image/jpeg' => 'jpg',
                        'image/png'  => 'png',
                        'image/gif'  => 'gif',
                        'image/webp' => 'webp',
                    );
                    $mime = wp_get_image_mime( $tmp );
                    $ext = ( isset( $mime_to_ext[$mime] ) ? $mime_to_ext[$mime] : 'jpg' );
                    $filename = (( $filename !== '' ? $filename : 'image' )) . '.' . $ext;
                }
                $file_array = array(
                    'name'     => sanitize_file_name( $filename ),
                    'tmp_name' => $tmp,
                );
                $attachment_id = media_handle_sideload( $file_array, $post_id );
                if ( is_wp_error( $attachment_id ) ) {
                    @unlink( $tmp );
                    $this->log( "Failed to import image {$url} for post {$post_id}: " . $attachment_id->get_error_message(), true );
                    $index++;
                    continue;
                }
                $new_imported[] = $url;
                // First imported image becomes featured image if none set yet
                if ( $index === 0 && !has_post_thumbnail( $post_id ) ) {
                    set_post_thumbnail( $post_id, $attachment_id );
                }
                $index++;
            }
            update_post_meta( $post_id, '_afi_imported_image_urls', array_values( array_unique( $new_imported ) ) );
            // If, for some reason, no thumbnail exists after processing, set placeholder
            if ( !has_post_thumbnail( $post_id ) ) {
                $placeholder_id = $this->ensure_placeholder_attachment();
                if ( $placeholder_id ) {
                    set_post_thumbnail( $post_id, $placeholder_id );
                }
            }
        }

        /**
         * Ensure a reusable placeholder image exists in Media Library and return its attachment ID
         */
        /**
         * Title-case a display value that may arrive from a feed in ALL CAPS or all lower.
         * Skips values that look like stock numbers, VINs, or codes (short all-caps tokens
         * with digits are left alone).
         */
        public function format_display_value( $value ) {
            $trimmed = trim( $value );
            if ( $trimmed === '' ) {
                return $trimmed;
            }
            // Leave it alone if it looks like a code: all-uppercase + contains digits (e.g. "VIN123", "ABC-4D")
            if ( preg_match( '/\\d/', $trimmed ) ) {
                return $trimmed;
            }
            // mbstring is available on virtually all WP hosts; fall back to ucwords otherwise
            if ( function_exists( 'mb_convert_case' ) ) {
                return mb_convert_case( mb_strtolower( $trimmed ), MB_CASE_TITLE, 'UTF-8' );
            }
            return ucwords( strtolower( $trimmed ) );
        }

        private function ensure_placeholder_attachment() {
            // Try cached attachment ID first
            $cached_id = $this->get_option( 'placeholder_attachment_id' );
            if ( $cached_id ) {
                $cached_id = (int) $cached_id;
                if ( $cached_id > 0 && get_post( $cached_id ) ) {
                    return $cached_id;
                }
            }
            $placeholder_path = plugin_dir_path( __FILE__ ) . 'assets/car_placeholder.png';
            if ( !file_exists( $placeholder_path ) ) {
                $this->log( 'Placeholder image not found at ' . $placeholder_path, true );
                return 0;
            }
            require_once ABSPATH . 'wp-admin/includes/file.php';
            require_once ABSPATH . 'wp-admin/includes/media.php';
            require_once ABSPATH . 'wp-admin/includes/image.php';
            $upload_dir = wp_upload_dir();
            if ( !empty( $upload_dir['error'] ) ) {
                $this->log( 'Failed to access upload directory for placeholder image: ' . $upload_dir['error'], true );
                return 0;
            }
            $filename = 'car_placeholder.png';
            $dest_path = trailingslashit( $upload_dir['path'] ) . $filename;
            $dest_url = trailingslashit( $upload_dir['url'] ) . $filename;
            // Copy file into current uploads folder if it does not exist yet
            if ( !file_exists( $dest_path ) ) {
                if ( !@copy( $placeholder_path, $dest_path ) ) {
                    $this->log( 'Failed to copy placeholder image into uploads directory.', true );
                    return 0;
                }
            }
            $filetype = wp_check_filetype( $filename, null );
            $attachment = array(
                'guid'           => $dest_url,
                'post_mime_type' => $filetype['type'],
                'post_title'     => 'Vehicle Placeholder Image',
                'post_content'   => '',
                'post_status'    => 'inherit',
            );
            $attach_id = wp_insert_attachment( $attachment, $dest_path );
            if ( is_wp_error( $attach_id ) ) {
                $this->log( 'Failed to create placeholder attachment: ' . $attach_id->get_error_message(), true );
                return 0;
            }
            $attach_data = wp_generate_attachment_metadata( $attach_id, $dest_path );
            wp_update_attachment_metadata( $attach_id, $attach_data );
            $this->update_option( 'placeholder_attachment_id', $attach_id );
            return (int) $attach_id;
        }

        /**
         * Fetches the data from xml then add/update database
         */
        public function update_data() {
            global $wpdb;
            // Hard gate: if the import target is a third-party CPT and no
            // admin has explicitly accepted the data-loss disclaimer for it,
            // refuse to run. This protects users who switched the target on
            // a live site without reading the warning — the cron silently
            // skips, the manual sync surfaces a clear error, and no foreign
            // posts are ever touched without conscious acceptance.
            $target = $this->get_target_post_type();
            if ( 'vehicles' !== $target && !$this->has_accepted_foreign_cpt_disclaimer( $target ) ) {
                $this->log( "Import skipped: target post type '{$target}' requires explicit disclaimer acceptance. " . "Visit Automotive Sync → Settings to accept the third-party post type disclaimer before syncing.", true );
                return array(
                    'created' => 0,
                    'updated' => 0,
                    'success' => false,
                    'blocked' => 'disclaimer',
                );
            }
            // Gate A (defense in depth): foreign-CPT routing requires Pro. In the free
            // build this branch is unreachable because get_target_post_type() always
            // returns 'vehicles' there, but we guard here too so a tampered free build
            // cannot route imports into a foreign CPT even if it bypasses the dropdown.
            if ( 'vehicles' !== $target && !$this->is_pro_active() ) {
                $this->log( "Import skipped: importing into '{$target}' requires a Pro licence. " . "Upgrade to Pro to import into third-party post types.", true );
                return array(
                    'created' => 0,
                    'updated' => 0,
                    'success' => false,
                    'blocked' => 'pro_gate',
                );
            }
            $this->log( "Starting feed import process" );
            // get the data from the feed (CSV or XML)
            $units = ( $this->is_csv_feed() ? $this->load_csv() : $this->load_xml() );
            if ( $units === false ) {
                return;
                // Error already logged
            }
            $created = 0;
            $updated = 0;
            $skipped = 0;
            $skipped_cap = 0;
            $feed_stock_numbers = array();
            // Gate B: free plan is capped at 25 plugin-managed vehicles. Count once
            // before the loop and increment in-run so we avoid repeated DB queries.
            // Pro bypasses the cap entirely (PHP_INT_MAX acts as "no limit").
            $free_vehicle_cap = 25;
            $current_owned_count = ( $this->is_pro_active() ? PHP_INT_MAX : $this->count_imported_posts() );
            foreach ( $units as $unit ) {
                $raw_unit = $unit;
                // pre-mapping row, keyed by feed field names
                $unit = $this->apply_field_mappings( $unit );
                $unit = $this->consolidate_make_field( $unit );
                if ( !isset( $unit['stock_number'] ) ) {
                    $this->log( "Unit missing stock_number field. Check the Field Mapping tab to map your feed's unique ID field to 'stock_number'.", true );
                    $skipped++;
                    continue;
                }
                $feed_stock_numbers[] = $unit['stock_number'];
                // Check if a listing already exists for this stock number.
                // Hard safety gate: require the _ais_imported marker, so a
                // matching stock_number on a user-created post (in a foreign
                // CPT we share) never causes us to overwrite their data.
                // Legacy 'vehicles' posts get the marker via the one-time
                // backfill on plugin init and via lazy update_inventory marks.
                // Exclude trashed posts so they are not updated and don't block re-creation.
                $post_id = $wpdb->get_var( $wpdb->prepare( "SELECT pm.post_id FROM {$wpdb->postmeta} pm\n\t\t\t\t INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id\n\t\t\t\t INNER JOIN {$wpdb->postmeta} marker ON marker.post_id = p.ID\n\t\t\t\t   AND marker.meta_key = '_ais_imported' AND marker.meta_value = '1'\n\t\t\t\t WHERE pm.meta_key = 'stock_number' AND pm.meta_value = %s\n\t\t\t\t AND p.post_type = %s\n\t\t\t\t AND p.post_status != 'trash'", $unit['stock_number'], $this->get_target_post_type() ) );
                if ( $post_id == NULL ) {
                    // Gate B: new inserts are capped at 25 for free users. Existing
                    // owned vehicles always update (grandfathering — they're already
                    // counted and the cap only blocks new additions).
                    if ( !$this->is_pro_active() && $current_owned_count >= $free_vehicle_cap ) {
                        $skipped_cap++;
                        continue;
                        // skip insert and update_inventory/process_images
                    }
                    // listing does not exist, therefore, create a new listing
                    $post_id = $this->add_listing( $unit, $raw_unit );
                    if ( $post_id ) {
                        $created++;
                        $current_owned_count++;
                        // track in-run inserts toward the cap
                    }
                } else {
                    $updated++;
                }
                // now add/update the plugin data against the listing
                if ( $post_id ) {
                    $this->update_inventory( $post_id, $unit );
                    $this->process_images( $post_id, $unit );
                }
            }
            // Gate B: if we hit the free cap, log it and queue a dismissible admin notice.
            if ( $skipped_cap > 0 ) {
                $this->log( "Free-plan vehicle cap ({$free_vehicle_cap}) reached. {$skipped_cap} new vehicles were not imported. Upgrade to Pro to sync your full inventory.", true );
                // Only queue the notice if we haven't shown it in the last 7 days.
                // afi_cap_notice carries the data; afi_cap_notice_shown is the
                // rate-limit gate and persists independently so a new capped sync
                // can't immediately re-queue the notice after the user sees it.
                if ( !get_transient( 'afi_cap_notice_shown' ) ) {
                    set_transient( 'afi_cap_notice', array(
                        'synced'  => $created + $updated,
                        'total'   => count( $units ),
                        'skipped' => $skipped_cap,
                    ), 7 * DAY_IN_SECONDS );
                }
            }
            // Handle vehicles that are no longer present in the feed
            $removed = 0;
            $removed_action = $this->get_option( 'removed_vehicle_action', 'nothing' );
            if ( $removed_action !== 'nothing' && !empty( $feed_stock_numbers ) ) {
                $removed = $this->handle_removed_vehicles( $feed_stock_numbers, $removed_action );
                $this->log( "Import complete: {$created} created, {$updated} updated, {$removed} removed from feed" );
            } else {
                $this->log( "Import complete: {$created} created, {$updated} updated" );
            }
            // Remove activation banner after first successful import
            delete_transient( 'afi_activation_notice' );
            // Track successful imports for review notice
            $import_count = get_option( 'afi_total_imports', 0 );
            update_option( 'afi_total_imports', $import_count + 1 );
            // Stamp the first successful import. The Pro upsell notice waits a
            // few days from this point so it lands once the dealer has seen the
            // plugin actually working, not on day one.
            if ( !get_option( 'afi_first_import_time' ) ) {
                update_option( 'afi_first_import_time', time() );
            }
            // Record a compact summary of this run so the settings status panel
            // and dashboard widget can show "last sync" details at a glance.
            update_option( 'afi_last_sync', array(
                'time'        => time(),
                'created'     => $created,
                'updated'     => $updated,
                'removed'     => $removed,
                'skipped'     => $skipped,
                'skipped_cap' => $skipped_cap,
                'total'       => count( $feed_stock_numbers ),
                'status'      => ( $skipped > 0 || $skipped_cap > 0 ? 'warning' : 'success' ),
            ), false );
            return array(
                'created'     => $created,
                'updated'     => $updated,
                'removed'     => $removed,
                'skipped'     => $skipped,
                'skipped_cap' => $skipped_cap,
                'success'     => true,
            );
        }

        /**
         * Handle vehicles that are no longer present in the feed.
         * @param array  $feed_stock_numbers Stock numbers seen in the current feed
         * @param string $action             One of: draft, trash, sold
         * @return int Number of vehicles acted upon
         */
        public function handle_removed_vehicles( array $feed_stock_numbers, string $action ) : int {
            global $wpdb;
            // Find all active vehicle post IDs and their stock numbers.
            // Hard safety gate: only ever consider posts that carry the
            // _ais_imported marker. This guarantees stale handling cannot
            // draft/trash a post the site owner created manually — critical
            // when targeting a third-party CPT (e.g. Inventory Presser) where
            // non-feed posts live alongside ours.
            $rows = $wpdb->get_results( $wpdb->prepare( "SELECT pm.post_id, pm.meta_value AS stock_number\n\t\t\t FROM {$wpdb->postmeta} pm\n\t\t\t INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id\n\t\t\t INNER JOIN {$wpdb->postmeta} marker ON marker.post_id = p.ID\n\t\t\t   AND marker.meta_key = '_ais_imported' AND marker.meta_value = '1'\n\t\t\t WHERE pm.meta_key = 'stock_number'\n\t\t\t AND p.post_type = %s\n\t\t\t AND p.post_status NOT IN ('trash', 'auto-draft')", $this->get_target_post_type() ) );
            if ( empty( $rows ) ) {
                return 0;
            }
            $count = 0;
            foreach ( $rows as $row ) {
                if ( in_array( $row->stock_number, $feed_stock_numbers, true ) ) {
                    continue;
                    // Still in feed, skip
                }
                switch ( $action ) {
                    case 'draft':
                        wp_update_post( array(
                            'ID'          => $row->post_id,
                            'post_status' => 'draft',
                        ) );
                        $this->log( "Vehicle #{$row->post_id} (stock #{$row->stock_number}) removed from feed — marked as draft." );
                        break;
                    case 'trash':
                        wp_trash_post( $row->post_id );
                        $this->log( "Vehicle #{$row->post_id} (stock #{$row->stock_number}) removed from feed — moved to trash." );
                        break;
                    case 'sold':
                        update_post_meta( $row->post_id, 'status', 'sold' );
                        $this->log( "Vehicle #{$row->post_id} (stock #{$row->stock_number}) removed from feed — status set to sold." );
                        break;
                }
                $count++;
            }
            return $count;
        }

        /**
         * Fetch the data from database for the current listing
         * @return Associative array containing data from database 
         */
        public function get_inventory() {
            $unit = array();
            // fetch the inventory from database for current listing
            $custom_fields = get_post_custom();
            // generate array to return
            $unit['stock_number'] = array('Stock Number', $custom_fields['stock_number'][0] ?? '');
            $unit['body_type'] = array('Body Type', $custom_fields['body_type_value'][0] ?? '');
            $unit['mileage'] = array('Mileage', $custom_fields['mileage'][0] ?? '');
            $unit['designation'] = array('Designation', $custom_fields['designation'][0] ?? '');
            $unit['special_web_price'] = array('Special Web Price', $custom_fields['special_web_price'][0] ?? '');
            $unit['type'] = array('Type', $custom_fields['type'][0] ?? '');
            $unit['manufacturer'] = array('Manufacturer', $custom_fields['manufacturer'][0] ?? '');
            $unit['model'] = array('Model', $custom_fields['model'][0] ?? '');
            $unit['length'] = array('Length', $custom_fields['length'][0] ?? '');
            $unit['color'] = array('Color', $custom_fields['exterior_color'][0] ?? '');
            $unit['price'] = array('Price', $custom_fields['price_value'][0] ?? '');
            $unit['status'] = array('Status', $custom_fields['status'][0] ?? '');
            return $unit;
        }

        /**
         * Display data on listing post
         * @param Array of array containing text and value to be displayed
         */
        public function display_inventory( $unit ) {
            echo '<table class="form-table">';
            // loop through each field and display
            foreach ( $unit as $key => $val ) {
                echo '<tr><th scope="row">';
                echo '<label for="afi_field_' . esc_attr( $key ) . '">';
                echo esc_html( $val[0] );
                echo '</label>';
                echo '</th><td>';
                echo '<input type="text" class="regular-text" id="afi_field_' . esc_attr( $key ) . '" name="afi_field[' . esc_attr( $key ) . ']" value="' . esc_attr( $val[1] ) . '" />';
                echo '</td></tr>';
            }
            echo '</table>';
        }

        /**
         * Save vehicle post meta data
         */
        public function save_vehicle_meta( $post_id ) {
            // Check if this is an autosave
            if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
                return;
            }
            // Verify nonce
            if ( !isset( $_POST['afi_noncename'] ) || !wp_verify_nonce( $_POST['afi_noncename'], plugin_basename( __FILE__ ) ) ) {
                return;
            }
            // Check user permissions
            if ( !current_user_can( 'edit_post', $post_id ) ) {
                return;
            }
            // Save the fields
            if ( isset( $_POST['afi_field'] ) && is_array( $_POST['afi_field'] ) ) {
                foreach ( $_POST['afi_field'] as $key => $value ) {
                    $sanitized_value = sanitize_text_field( $value );
                    update_post_meta( $post_id, $key, $sanitized_value );
                    // Update theme compatibility fields
                    switch ( $key ) {
                        case 'manufacturer':
                            update_post_meta( $post_id, 'manufacturer_level2_value', $sanitized_value );
                            break;
                        case 'model_year':
                            update_post_meta( $post_id, 'year_value', $sanitized_value );
                            break;
                        case 'special_web_price':
                            update_post_meta( $post_id, 'price_value', $sanitized_value );
                            break;
                        case 'mileage':
                            update_post_meta( $post_id, 'mileage_value', $sanitized_value );
                            break;
                        case 'exterior_color':
                            update_post_meta( $post_id, 'color_value', $sanitized_value );
                            break;
                    }
                }
            }
        }

        /**
         * Adds a box to the main column on the Vehicle edit screen
         */
        public function add_custom_box() {
            // Surface the imported-feed meta box on whichever CPT we are
            // currently writing to, so users editing a vehicle in a third-party
            // CPT still see the field-level detail this plugin imported.
            add_meta_box(
                'afi_vehicle_info',
                __( 'Vehicle Information (Imported from Feed)', 'automotive-feed-import' ),
                array($this, 'inner_custom_box'),
                $this->get_target_post_type(),
                'normal',
                'high'
            );
        }

        /**
         * Fetches the data from database and prints on the vehicle edit screen
         */
        public function inner_custom_box() {
            // Use nonce for verification
            wp_nonce_field( plugin_basename( __FILE__ ), 'afi_noncename' );
            // Make it clear these values come from the feed and refresh on sync,
            // so manual edits aren't a surprise loss. On free, this is also the
            // natural, contextual moment to mention Pro field-lock (upsell UI,
            // safe to ship in the free build per the build-marker conventions).
            echo '<div style="background:#fff8e5;border:1px solid #f0e2b6;border-left:4px solid #dba617;border-radius:6px;padding:10px 14px;margin:0 0 14px;">';
            echo '<p style="margin:0;">' . esc_html__( 'These details come from your inventory feed and refresh automatically on every sync. Any edits you make here may be replaced the next time your feed updates.', 'automotive-feed-import' ) . '</p>';
            if ( !$this->is_pro_active() ) {
                $upgrade = ( function_exists( 'afi_fs' ) ? afi_fs()->get_upgrade_url() : '' );
                echo '<p style="margin:8px 0 0;">' . esc_html__( 'Want your manual edits to stick?', 'automotive-feed-import' );
                if ( $upgrade ) {
                    echo ' <a href="' . esc_url( $upgrade ) . '">' . esc_html__( 'Lock fields from feed updates with Pro', 'automotive-feed-import' ) . '</a>.';
                } else {
                    echo ' ' . esc_html__( 'Locking fields from feed updates is available in Pro.', 'automotive-feed-import' );
                }
                echo '</p>';
            }
            echo '</div>';
            // get the information from database
            $unit = $this->get_inventory();
            // display the unit
            $this->display_inventory( $unit );
        }

        /**
         * Register settings page
         */
        public function add_settings_page() {
            // Top-level menu. Visible to editors (edit_posts) so they keep
            // access to the nested Vehicles CPT; the Settings page itself is
            // gated to admins via the same-slug submenu below.
            add_menu_page(
                __( 'Automotive Inventory Importer Settings', 'automotive-feed-import' ),
                __( 'Automotive Sync', 'automotive-feed-import' ),
                'edit_posts',
                $this->plugin_slug,
                '',
                'dashicons-car',
                3
            );
            // Settings page — admin only. Sharing the parent slug makes this the
            // menu's landing page for users who can manage options. Capture the
            // returned hook suffix so asset enqueueing is reliable regardless of
            // whether WordPress treats this as a top-level or settings page hook.
            $this->settings_hook = add_submenu_page(
                $this->plugin_slug,
                __( 'Automotive Inventory Importer Settings', 'automotive-feed-import' ),
                __( 'Settings', 'automotive-feed-import' ),
                'manage_options',
                $this->plugin_slug,
                array($this, 'render_settings_page')
            );
        }

        /**
         * Absolute URL to the plugin's settings page (a top-level admin menu).
         *
         * @param string $suffix Extra query string, e.g. '&tab=mapping'.
         * @return string
         */
        private function settings_page_url( $suffix = '' ) {
            return admin_url( 'admin.php?page=' . $this->plugin_slug . $suffix );
        }

        /**
         * Register settings
         */
        public function register_settings() {
            // Register settings
            register_setting( $this->plugin_slug . '_settings', $this->plugin_slug . '_xml_file_path' );
            register_setting( $this->plugin_slug . '_settings', $this->plugin_slug . '_import_frequency' );
            register_setting( $this->plugin_slug . '_settings', $this->plugin_slug . '_post_title_format' );
            register_setting( $this->plugin_slug . '_settings', $this->plugin_slug . '_post_content_format' );
            register_setting( $this->plugin_slug . '_settings', $this->plugin_slug . '_currency_symbol' );
            register_setting( $this->plugin_slug . '_settings', $this->plugin_slug . '_removed_vehicle_action' );
            register_setting( $this->plugin_slug . '_settings', $this->plugin_slug . '_target_post_type', array(
                'type'              => 'string',
                'sanitize_callback' => array($this, 'sanitize_target_post_type'),
                'default'           => 'vehicles',
            ) );
            // General Settings Section
            add_settings_section(
                'afi_general_section',
                __( 'Feed & Sync Settings', 'automotive-feed-import' ),
                array($this, 'render_general_section'),
                $this->plugin_slug
            );
            // XML File Path
            add_settings_field(
                'xml_file_path',
                __( 'Your Inventory Link', 'automotive-feed-import' ),
                array($this, 'render_xml_path_field'),
                $this->plugin_slug,
                'afi_general_section'
            );
            // Import Frequency
            add_settings_field(
                'import_frequency',
                __( 'How Often to Sync', 'automotive-feed-import' ),
                array($this, 'render_frequency_field'),
                $this->plugin_slug,
                'afi_general_section'
            );
            // Removed Vehicle Action
            add_settings_field(
                'removed_vehicle_action',
                __( 'When Vehicle is Removed from Feed', 'automotive-feed-import' ),
                array($this, 'render_removed_vehicle_action_field'),
                $this->plugin_slug,
                'afi_general_section'
            );
            // Target Post Type (universal CPT targeting)
            add_settings_field(
                'target_post_type',
                __( 'Import Vehicles Into', 'automotive-feed-import' ),
                array($this, 'render_target_post_type_field'),
                $this->plugin_slug,
                'afi_general_section'
            );
            // Post Format Section
            add_settings_section(
                'afi_format_section',
                __( 'How Vehicle Pages Look', 'automotive-feed-import' ),
                array($this, 'render_format_section'),
                $this->plugin_slug
            );
            // Post Title Format
            add_settings_field(
                'post_title_format',
                __( 'Vehicle Page Title Template', 'automotive-feed-import' ),
                array($this, 'render_title_format_field'),
                $this->plugin_slug,
                'afi_format_section'
            );
            // Post Content Format
            add_settings_field(
                'post_content_format',
                __( 'Vehicle Description Template', 'automotive-feed-import' ),
                array($this, 'render_content_format_field'),
                $this->plugin_slug,
                'afi_format_section'
            );
            // Currency Symbol
            add_settings_field(
                'currency_symbol',
                __( 'Currency Symbol', 'automotive-feed-import' ),
                array($this, 'render_currency_symbol_field'),
                $this->plugin_slug,
                'afi_format_section'
            );
        }

        /**
         * Extract available field names from the feed (XML or CSV)
         * @return array|false Array of field names on success, false on failure
         */
        public function extract_xml_fields() {
            if ( empty( $this->xml_file ) ) {
                return false;
            }
            // Load feed using the appropriate parser
            $units = ( $this->is_csv_feed() ? $this->load_csv() : $this->load_xml() );
            if ( $units === false || empty( $units ) ) {
                return false;
            }
            // Union field names across records rather than trusting the first
            // one. Feeds like DealerCenter omit empty tags per record, so no
            // single vehicle is guaranteed to carry every field — sampling only
            // the first would under-report and, when the first record is sparse,
            // show "0 fields". Scan a bounded window so large feeds stay fast,
            // and preserve first-seen order for a stable, readable field list.
            $fields = array();
            foreach ( array_slice( $units, 0, 100 ) as $unit ) {
                foreach ( $unit as $key => $value ) {
                    if ( !isset( $fields[$key] ) ) {
                        $fields[$key] = true;
                    }
                }
            }
            return array_keys( $fields );
        }

        /**
         * Get available target field options for mapping
         * @return array Target fields with labels
         */
        /**
         * List taxonomies attached to a given CPT that users can map feed
         * fields into. WordPress internal taxonomies (nav menus, link
         * categories, post format) are excluded — they make no sense as
         * vehicle attributes.
         *
         * @since 2.5.0
         * @param string $post_type Post type slug.
         * @return array<string,string> Map of taxonomy slug => human label.
         */
        public function get_available_taxonomies( $post_type ) {
            $post_type = sanitize_key( $post_type );
            if ( '' === $post_type ) {
                return array();
            }
            $internal = array('nav_menu', 'link_category', 'post_format');
            $taxonomies = get_object_taxonomies( $post_type, 'objects' );
            $result = array();
            foreach ( $taxonomies as $slug => $tax ) {
                if ( in_array( $slug, $internal, true ) ) {
                    continue;
                }
                $label = ( isset( $tax->labels->singular_name ) && $tax->labels->singular_name ? $tax->labels->singular_name : $tax->label );
                $result[$slug] = $label;
            }
            return $result;
        }

        /**
         * Assign one or more terms (creating any that don't yet exist) to a
         * post for a given taxonomy. Handles common multi-value delimiters
         * (comma, pipe, semicolon) so a feed field like "SUV, Crossover"
         * becomes two terms.
         *
         * @since 2.5.0
         * @param int    $post_id  Post receiving the terms.
         * @param string $taxonomy Taxonomy slug.
         * @param mixed  $value    Feed value (string or array).
         */
        private function assign_terms_from_value( $post_id, $taxonomy, $value ) {
            $taxonomy = sanitize_key( $taxonomy );
            if ( '' === $taxonomy || !taxonomy_exists( $taxonomy ) ) {
                return;
            }
            // Normalise the input into a flat list of trimmed term names.
            if ( is_array( $value ) ) {
                $candidates = $value;
            } else {
                $candidates = preg_split( '/\\s*[,|;]\\s*/', (string) $value );
            }
            $term_ids = array();
            foreach ( $candidates as $name ) {
                $name = ( is_string( $name ) ? trim( $name ) : '' );
                if ( '' === $name ) {
                    continue;
                }
                $term = term_exists( $name, $taxonomy );
                if ( !$term ) {
                    $term = wp_insert_term( $name, $taxonomy );
                }
                if ( !is_wp_error( $term ) && !empty( $term['term_id'] ) ) {
                    $term_ids[] = (int) $term['term_id'];
                }
            }
            if ( !empty( $term_ids ) ) {
                wp_set_object_terms(
                    $post_id,
                    $term_ids,
                    $taxonomy,
                    false
                );
            }
        }

        /**
         * Scan the most recent posts of a given CPT for distinct meta keys
         * the importer could write to, ranked by how often they appear.
         *
         * Used by the field-mapping UI to suggest target meta keys when the
         * import target is a third-party CPT — so users don't have to read
         * that plugin's docs to figure out which meta keys it uses.
         * Results are cached in a 1-hour transient keyed by post type.
         * WordPress internal keys (those starting with "_") and our own
         * marker keys are excluded from suggestions.
         *
         * @since 2.5.0
         * @param string $post_type Post type slug to scan.
         * @param int    $sample    Number of recent posts to inspect.
         * @return array<string,int> Map of meta_key => occurrence count, highest first.
         */
        public function get_discovered_meta_keys( $post_type, $sample = 50 ) {
            $post_type = sanitize_key( $post_type );
            if ( '' === $post_type ) {
                return array();
            }
            $cache_key = 'afi_meta_keys_' . $post_type;
            $cached = get_transient( $cache_key );
            if ( is_array( $cached ) ) {
                return $cached;
            }
            global $wpdb;
            $rows = $wpdb->get_results( $wpdb->prepare(
                "SELECT pm.meta_key, COUNT(*) AS hits\n\t\t\t\t FROM {$wpdb->postmeta} pm\n\t\t\t\t INNER JOIN (\n\t\t\t\t   SELECT ID FROM {$wpdb->posts}\n\t\t\t\t   WHERE post_type = %s\n\t\t\t\t   AND post_status NOT IN ('auto-draft', 'trash')\n\t\t\t\t   ORDER BY post_modified DESC\n\t\t\t\t   LIMIT %d\n\t\t\t\t ) recent ON recent.ID = pm.post_id\n\t\t\t\t WHERE pm.meta_key NOT LIKE %s\n\t\t\t\t AND pm.meta_key NOT LIKE %s\n\t\t\t\t GROUP BY pm.meta_key\n\t\t\t\t ORDER BY hits DESC, pm.meta_key ASC",
                $post_type,
                $sample,
                '\\_%',
                'afi\\_%'
            ), ARRAY_A );
            $result = array();
            if ( is_array( $rows ) ) {
                foreach ( $rows as $row ) {
                    $result[$row['meta_key']] = (int) $row['hits'];
                }
            }
            set_transient( $cache_key, $result, HOUR_IN_SECONDS );
            return $result;
        }

        /**
         * Build the dropdown of available target meta fields.
         *
         * For our own 'vehicles' CPT (or when no target is supplied), returns
         * the canonical hand-curated list — that list is authoritative for
         * features built on top of this plugin's display layer.
         *
         * For a third-party CPT, returns meta keys discovered on that CPT's
         * existing posts (ranked by frequency) merged with the canonical list
         * as a fallback. The discovered keys come first so they sort to the
         * top of the dropdown — the user can pick the one their other plugin
         * already uses without guessing.
         *
         * @param string|null $post_type Optional target post type. Defaults to current setting.
         * @return array<string,string> Map of meta_key => human label.
         */
        public function get_available_target_fields( $post_type = null ) {
            if ( null === $post_type ) {
                $post_type = $this->get_target_post_type();
            }
            $canonical = $this->get_canonical_target_fields();
            $taxonomies = $this->get_available_taxonomies( $post_type );
            $merged = array();
            // Taxonomies always come first in the dropdown. They are
            // encoded with a "tax:" prefix in the stored target string so
            // update_inventory() can route them to wp_set_object_terms
            // instead of update_post_meta. Works on our own 'vehicles' CPT
            // the moment users register a taxonomy against it; works on
            // foreign CPTs (Inventory Presser's make/model/body-style,
            // Motors theme's taxonomies, etc.) out of the box.
            foreach ( $taxonomies as $tax_slug => $tax_label ) {
                /* translators: %s: taxonomy label */
                $merged['tax:' . $tax_slug] = sprintf( __( 'Taxonomy: %s', 'automotive-feed-import' ), $tax_label );
            }
            if ( 'vehicles' === $post_type ) {
                foreach ( $canonical as $key => $label ) {
                    $merged[$key] = $label;
                }
                return $merged;
            }
            // For a third-party CPT we deliberately do NOT show our own
            // built-in field names — they'd only confuse the user, who wants
            // to map into the fields their other plugin/theme actually uses.
            //
            // The one exception is stock_number: it is the internal key the
            // importer uses to recognise a vehicle on the next sync (match,
            // update, and stale-handling all key off it). Every import needs
            // exactly one field mapped to it, so we always offer it — pinned
            // to the top and labelled as the unique ID rather than as a
            // vehicle attribute, to make its purpose clear.
            $merged['stock_number'] = __( 'Unique ID — needed to keep vehicles in sync', 'automotive-feed-import' );
            // That CPT's taxonomies (added above) plus the meta keys
            // discovered on its existing posts, ranked by how common they are.
            $discovered = $this->get_discovered_meta_keys( $post_type );
            foreach ( $discovered as $key => $hits ) {
                if ( 'stock_number' === $key ) {
                    continue;
                    // already pinned above
                }
                /* translators: 1: post type label, 2: occurrence count */
                $label = sprintf( __( 'Used by %1$s (%2$d posts)', 'automotive-feed-import' ), $post_type, $hits );
                $merged[$key] = $label;
            }
            return $merged;
        }

        /**
         * The hand-curated canonical list of target meta fields this plugin
         * knows how to import into. Always available on our own 'vehicles' CPT.
         * @return array<string,string>
         */
        public function get_canonical_target_fields() {
            return array(
                'stock_number'              => __( 'Stock Number', 'automotive-feed-import' ),
                'vin'                       => __( 'VIN', 'automotive-feed-import' ),
                'manufacturer'              => __( 'Manufacturer', 'automotive-feed-import' ),
                'model'                     => __( 'Model', 'automotive-feed-import' ),
                'model_year'                => __( 'Year', 'automotive-feed-import' ),
                'designation'               => __( 'Condition/Designation', 'automotive-feed-import' ),
                'type'                      => __( 'Type', 'automotive-feed-import' ),
                'body_type'                 => __( 'Body Type', 'automotive-feed-import' ),
                'style'                     => __( 'Style', 'automotive-feed-import' ),
                'special_web_price'         => __( 'Special Web Price', 'automotive-feed-import' ),
                'base_list'                 => __( 'Base List Price', 'automotive-feed-import' ),
                'factory_list'              => __( 'Factory List Price', 'automotive-feed-import' ),
                'total_list'                => __( 'Total List Price', 'automotive-feed-import' ),
                'take_price'                => __( 'Take Price', 'automotive-feed-import' ),
                'show_web_price'            => __( 'Show Web Price', 'automotive-feed-import' ),
                'mileage'                   => __( 'Mileage', 'automotive-feed-import' ),
                'exterior_color'            => __( 'Exterior Color', 'automotive-feed-import' ),
                'interior_color'            => __( 'Interior Color', 'automotive-feed-import' ),
                'length'                    => __( 'Length', 'automotive-feed-import' ),
                'width'                     => __( 'Width', 'automotive-feed-import' ),
                'height'                    => __( 'Height', 'automotive-feed-import' ),
                'weight'                    => __( 'Weight', 'automotive-feed-import' ),
                'chassis_no'                => __( 'Chassis Number', 'automotive-feed-import' ),
                'serial_no'                 => __( 'Serial Number', 'automotive-feed-import' ),
                'status'                    => __( 'Status', 'automotive-feed-import' ),
                'status_code'               => __( 'Status Code', 'automotive-feed-import' ),
                'lot_location'              => __( 'Lot Location', 'automotive-feed-import' ),
                'lot_location_code'         => __( 'Lot Location Code', 'automotive-feed-import' ),
                'gl_location_code'          => __( 'GL Location Code', 'automotive-feed-import' ),
                'received_date'             => __( 'Received Date', 'automotive-feed-import' ),
                'sold_date'                 => __( 'Sold Date', 'automotive-feed-import' ),
                'web_dealer_id'             => __( 'Web Dealer ID', 'automotive-feed-import' ),
                'description'               => __( 'Description', 'automotive-feed-import' ),
                'features'                  => __( 'Features', 'automotive-feed-import' ),
                'options'                   => __( 'Options', 'automotive-feed-import' ),
                'notes'                     => __( 'Notes', 'automotive-feed-import' ),
                'manufacturer_level2_value' => __( 'Manufacturer (Theme Compat)', 'automotive-feed-import' ),
                'year_value'                => __( 'Year (Theme Compat)', 'automotive-feed-import' ),
                'price_value'               => __( 'Price (Theme Compat)', 'automotive-feed-import' ),
                'mileage_value'             => __( 'Mileage (Theme Compat)', 'automotive-feed-import' ),
                'color_value'               => __( 'Color (Theme Compat)', 'automotive-feed-import' ),
                'body_type_value'           => __( 'Body Type (Theme Compat)', 'automotive-feed-import' ),
            );
        }

        /**
         * Read the raw field_mappings option, upgrading the legacy flat
         * format to the per-CPT keyed format on the fly.
         *
         * Legacy format (pre-2.5.0):
         *   [ 'SourceField' => ['enabled' => true, 'target' => 'meta_key'], ... ]
         *
         * Current format:
         *   [ 'cpt_slug' => [ 'SourceField' => ['enabled' => ..., 'target' => ...], ... ], ... ]
         *
         * The legacy structure is detected by inspecting whether the first
         * value is a mapping entry (has 'enabled') instead of a CPT bucket.
         * Returns the full multi-CPT structure.
         *
         * @since 2.5.0
         * @return array<string, array<string, array{enabled:bool,target:string}>>
         */
        private function get_all_field_mappings() {
            $raw = $this->get_option( 'field_mappings', array() );
            if ( empty( $raw ) || !is_array( $raw ) ) {
                return array();
            }
            $first = reset( $raw );
            if ( is_array( $first ) && array_key_exists( 'enabled', $first ) ) {
                // Legacy flat format — wrap under our own CPT so the new code
                // path finds it. The user can still see/edit it on the mapping tab.
                return array(
                    'vehicles' => $raw,
                );
            }
            return $raw;
        }

        /**
         * Get saved field mappings for the current target post type, or
         * generate a default 1:1 mapping from the live feed if none exist.
         *
         * Returning only the current target's mappings keeps callers
         * unchanged from pre-2.5.0 — they get a flat source→config array.
         * @return array Field mappings for the current target CPT.
         */
        public function get_field_mappings() {
            $all = $this->get_all_field_mappings();
            $target = $this->get_target_post_type();
            $mappings = ( isset( $all[$target] ) ? $all[$target] : array() );
            // If no mappings exist yet for this target, generate sensible defaults.
            if ( empty( $mappings ) ) {
                $fields = $this->extract_xml_fields();
                if ( $fields !== false ) {
                    // Default to a 1:1 mapping the user can adjust by hand. For
                    // our own CPT the canonical field names already line up; for
                    // a third-party CPT, Pro users can auto-map (preset or
                    // best-guess) from the Field Mapping tab.
                    foreach ( $fields as $field ) {
                        $mappings[$field] = array(
                            'enabled' => true,
                            'target'  => $field,
                        );
                    }
                }
            }
            return $mappings;
        }

        /**
         * Save field mappings for the current target post type without
         * disturbing mappings stored for other targets.
         * @param array $mappings Field mappings to save
         */
        public function save_field_mappings( $mappings ) {
            $all = $this->get_all_field_mappings();
            $target = $this->get_target_post_type();
            $all[$target] = $mappings;
            $this->update_option( 'field_mappings', $all );
        }

        /**
         * Apply field mappings to a raw unit, renaming source keys to target keys.
         * Disabled fields are dropped. If no mappings are configured, the unit is returned as-is.
         * @param array $unit Raw unit data from feed
         * @return array Unit data with mapped field names
         */
        public function apply_field_mappings( $unit ) {
            $mappings = $this->get_field_mappings();
            if ( empty( $mappings ) ) {
                return $unit;
            }
            $mapped = array();
            foreach ( $unit as $source_field => $value ) {
                if ( !isset( $mappings[$source_field] ) || !$mappings[$source_field]['enabled'] ) {
                    continue;
                }
                $target_field = $mappings[$source_field]['target'];
                $mapped[$target_field] = $value;
            }
            return $mapped;
        }

        /**
         * Check if feed URL has changed
         * @param string $new_url New feed URL
         * @return bool True if changed, false otherwise
         */
        public function has_feed_url_changed( $new_url ) {
            $old_url = $this->get_option( 'last_feed_url', '' );
            return $old_url !== $new_url && !empty( $new_url );
        }

        /**
         * Check if field mapping is needed
         * @return bool True if mapping needed, false otherwise
         */
        public function needs_field_mapping() {
            $all = $this->get_all_field_mappings();
            $target = $this->get_target_post_type();
            $mappings = ( isset( $all[$target] ) ? $all[$target] : array() );
            $feed_url = $this->get_option( 'xml_file_path', '' );
            $last_feed_url = $this->get_option( 'last_feed_url', '' );
            // Need mapping if no mappings exist for the current target or feed URL changed
            return empty( $mappings ) || $feed_url !== $last_feed_url && !empty( $feed_url );
        }

        /**
         * Render general section description
         */
        public function render_general_section() {
            echo '<p>' . esc_html__( 'Tell us where your inventory feed lives and how often to sync it.', 'automotive-feed-import' ) . '</p>';
        }

        /**
         * Render format section description
         */
        public function render_format_section() {
            echo '<p>' . esc_html__( 'Control how vehicle titles and descriptions are built using tokens like {manufacturer}, {model}, {model_year}, etc.', 'automotive-feed-import' ) . '</p>';
        }

        /**
         * Render XML path field
         */
        public function render_xml_path_field() {
            $value = $this->get_option( 'xml_file_path' );
            echo '<span class="afi-input-row">';
            echo '<input type="text" id="afi_xml_file_path" name="' . $this->plugin_slug . '_xml_file_path" value="' . esc_attr( $value ) . '" class="regular-text" placeholder="https://example.com/feed.xml" />';
            echo '<button type="button" class="button" id="afi_test_feed">' . esc_html__( 'Check feed link', 'automotive-feed-import' ) . '</button>';
            echo '<span class="afi-input-divider" style="color:#999;font-size:12px;">' . esc_html__( '— or —', 'automotive-feed-import' ) . '</span>';
            echo '<button type="button" class="button" id="afi_browse_file" title="' . esc_attr__( 'Browse and select a feed file already uploaded to this server. Use a link instead if your feed is hosted somewhere else.', 'automotive-feed-import' ) . '">' . esc_html__( 'Browse Server…', 'automotive-feed-import' ) . '</button>';
            echo '</span>';
            echo '<p id="afi-feed-test-result" class="afi-feed-test-result" role="status" aria-live="polite"></p>';
            echo '<p class="description"><strong>' . esc_html__( 'Link:', 'automotive-feed-import' ) . '</strong> ' . esc_html__( 'Paste a feed link from your inventory provider.', 'automotive-feed-import' ) . ' &nbsp;<strong>' . esc_html__( 'Browse Server:', 'automotive-feed-import' ) . '</strong> ' . esc_html__( 'Pick a file already uploaded to this site. CSV files must include a header row.', 'automotive-feed-import' ) . '</p>';
            $validation = get_transient( 'afi_feed_validation_' . get_current_user_id() );
            if ( $validation ) {
                delete_transient( 'afi_feed_validation_' . get_current_user_id() );
                $icon = ( $validation['type'] === 'success' ? '✓' : '✗' );
                $color = ( $validation['type'] === 'success' ? '#00a32a' : '#d63638' );
                echo '<p style="color:' . esc_attr( $color ) . ';margin-top:6px;"><strong>' . esc_html( $icon . ' ' . $validation['message'] ) . '</strong></p>';
            }
        }

        /**
         * Render frequency field
         */
        public function render_frequency_field() {
            $value = $this->get_option( 'import_frequency', 'tenminute' );
            $is_pro = $this->is_pro_active();
            $upgrade_url = $this->pro_upgrade_url();
            // Frequencies available on all plans.
            $free_frequencies = array(
                'twicedaily' => __( 'Twice Daily', 'automotive-feed-import' ),
                'daily'      => __( 'Daily', 'automotive-feed-import' ),
            );
            // Sub-hourly frequencies: Pro-only. Shown disabled in free so the
            // upsell is visible; gated at the scheduler level so tampering is inert.
            $pro_frequencies = array(
                'fiveminute'    => __( 'Every 5 Minutes', 'automotive-feed-import' ),
                'tenminute'     => __( 'Every 10 Minutes', 'automotive-feed-import' ),
                'fifteenminute' => __( 'Every 15 Minutes', 'automotive-feed-import' ),
                'thirtyminute'  => __( 'Every 30 Minutes', 'automotive-feed-import' ),
                'hourly'        => __( 'Hourly', 'automotive-feed-import' ),
            );
            echo '<select name="' . esc_attr( $this->plugin_slug . '_import_frequency' ) . '">';
            foreach ( $free_frequencies as $key => $label ) {
                echo '<option value="' . esc_attr( $key ) . '" ' . selected( $value, $key, false ) . '>' . esc_html( $label ) . '</option>';
            }
            foreach ( $pro_frequencies as $key => $label ) {
                if ( $is_pro ) {
                    echo '<option value="' . esc_attr( $key ) . '" ' . selected( $value, $key, false ) . '>' . esc_html( $label ) . '</option>';
                } else {
                    /* translators: %s: frequency label */
                    $pro_label = sprintf( __( '%s (Pro)', 'automotive-feed-import' ), $label );
                    echo '<option value="' . esc_attr( $key ) . '" disabled>' . esc_html( $pro_label ) . '</option>';
                }
            }
            echo '</select>';
            if ( !$is_pro ) {
                $badge_title = esc_attr__( 'Sub-hourly sync frequencies require a Pro licence.', 'automotive-feed-import' );
                echo ' <span class="afi-pro-hint" title="' . $badge_title . '">' . esc_html__( 'Pro', 'automotive-feed-import' ) . '</span>';
                if ( '' !== $upgrade_url ) {
                    echo ' <a href="' . esc_url( $upgrade_url ) . '" target="_blank" rel="noopener noreferrer" style="margin-left:4px;">' . esc_html__( 'Upgrade to Pro →', 'automotive-feed-import' ) . '</a>';
                }
            }
            echo '<p class="description">' . esc_html__( 'Choose how often we should check your feed for new or updated vehicles.', 'automotive-feed-import' ) . '</p>';
            if ( !$is_pro ) {
                echo '<p class="description">' . esc_html__( 'Free plan: twice daily or daily. Upgrade to Pro for syncing every 5, 10, 15, or 30 minutes.', 'automotive-feed-import' ) . '</p>';
            }
        }

        /**
         * Render removed vehicle action field
         */
        public function render_removed_vehicle_action_field() {
            $value = $this->get_option( 'removed_vehicle_action', 'nothing' );
            $options = array(
                'nothing' => __( 'Do Nothing', 'automotive-feed-import' ),
                'draft'   => __( 'Mark as Draft', 'automotive-feed-import' ),
                'trash'   => __( 'Move to Trash', 'automotive-feed-import' ),
                'sold'    => __( 'Change Status to Sold', 'automotive-feed-import' ),
            );
            echo '<select name="' . $this->plugin_slug . '_removed_vehicle_action">';
            foreach ( $options as $key => $label ) {
                $selected = ( $value === $key ? 'selected' : '' );
                echo '<option value="' . esc_attr( $key ) . '" ' . $selected . '>' . esc_html( $label ) . '</option>';
            }
            echo '</select>';
            echo '<p class="description">' . esc_html__( 'What to do with a vehicle when it disappears from your feed (usually because it sold). “Change Status to Sold” keeps the page online but marks the vehicle as sold.', 'automotive-feed-import' ) . '</p>';
        }

        /**
         * Sanitize the chosen target post type.
         *
         * Falls back to 'vehicles' if the submitted value isn't a registered
         * public post type. This prevents typos or stale values from sending
         * imports into a non-existent CPT.
         *
         * @since 2.5.0
         * @param string $value Submitted post type slug.
         * @return string Sanitized slug.
         */
        public function sanitize_target_post_type( $value ) {
            $value = ( is_string( $value ) ? sanitize_key( $value ) : '' );
            if ( '' === $value || !post_type_exists( $value ) ) {
                return 'vehicles';
            }
            // Foreign-CPT routing is Pro-only: silently fall back to 'vehicles'
            // if a free user submits a non-vehicles target (e.g., via HTML tampering).
            if ( 'vehicles' !== $value && !$this->is_pro_active() ) {
                return 'vehicles';
            }
            return $value;
        }

        /**
         * Render the target post type dropdown.
         *
         * Lets users send imports into our own 'vehicles' CPT (default) or any
         * other public CPT registered by a theme/plugin. When targeting a
         * non-default CPT, the frontend rendering this plugin normally does
         * for the 'vehicles' archive/single will self-disable so the host
         * plugin/theme owns display.
         *
         * @since 2.5.0
         */
        public function render_target_post_type_field() {
            $current = $this->get_target_post_type();
            $choices = $this->get_target_cpt_choices();
            $option = $this->plugin_slug . '_target_post_type';
            $is_pro = $this->is_pro_active();
            $upgrade_url = $this->pro_upgrade_url();
            echo '<select name="' . esc_attr( $option ) . '" id="' . esc_attr( $option ) . '">';
            foreach ( $choices as $slug => $info ) {
                $label = $info['label'] . ' — ' . $info['source'];
                if ( $info['is_own'] || $is_pro ) {
                    echo '<option value="' . esc_attr( $slug ) . '" ' . selected( $current, $slug, false ) . '>' . esc_html( $label ) . '</option>';
                } else {
                    /* translators: %s: post type option label */
                    $pro_label = sprintf( __( '%s (Pro)', 'automotive-feed-import' ), $label );
                    echo '<option value="' . esc_attr( $slug ) . '" disabled>' . esc_html( $pro_label ) . '</option>';
                }
            }
            echo '</select>';
            if ( !$is_pro ) {
                $badge_title = esc_attr__( 'Import directly into your theme\'s post type (Motors, Inventory Presser, custom). Upgrade to Pro to unlock.', 'automotive-feed-import' );
                echo ' <span class="afi-pro-hint" title="' . $badge_title . '">' . esc_html__( 'Pro', 'automotive-feed-import' ) . '</span>';
                if ( '' !== $upgrade_url ) {
                    echo ' <a href="' . esc_url( $upgrade_url ) . '" target="_blank" rel="noopener noreferrer" style="margin-left:4px;">' . esc_html__( 'Upgrade to Pro →', 'automotive-feed-import' ) . '</a>';
                }
            }
            echo '<p class="description">';
            echo esc_html__( 'Choose where imported vehicles are stored. Defaults to the built-in Vehicles section. ', 'automotive-feed-import' );
            if ( $is_pro ) {
                echo esc_html__( 'Switch to another post type — for example one provided by your dealership theme or another inventory plugin — to send feed data straight into it.', 'automotive-feed-import' );
            } else {
                echo esc_html__( 'Importing into a third-party theme or plugin\'s post type requires Pro.', 'automotive-feed-import' );
            }
            echo '</p>';
            if ( 'vehicles' !== $current && $is_pro ) {
                $current_label = ( isset( $choices[$current]['label'] ) ? $choices[$current]['label'] : $current );
                $current_source = ( isset( $choices[$current]['source'] ) ? $choices[$current]['source'] : '' );
                echo '<div style="background:#eef7ff;border-left:4px solid #2271b1;padding:12px 16px;margin-top:10px;max-width:680px;">';
                echo '<p style="margin:0 0 8px 0;"><strong>' . esc_html__( 'Where your vehicles will go:', 'automotive-feed-import' ) . '</strong> ';
                printf( 
                    /* translators: 1: post type label, 2: source plugin/theme */
                    esc_html__( 'Your inventory will be sent into %1$s (%2$s), which will display your vehicles with its own design. Our built-in vehicle pages step aside so there\'s no overlap.', 'automotive-feed-import' ),
                    '<strong>' . esc_html( $current_label ) . '</strong>',
                    esc_html( $current_source )
                 );
                echo '</p>';
                echo '<ul style="margin:0 0 0 18px;padding:0;list-style:disc;color:#3c434a;">';
                echo '<li>' . esc_html__( 'You can switch back to the built-in Vehicles section anytime — your imported vehicles simply move with you, nothing is re-downloaded.', 'automotive-feed-import' ) . '</li>';
                echo '<li>' . esc_html__( 'If you later remove that theme or plugin, your vehicles may stop showing until you pick a new home for them.', 'automotive-feed-import' ) . '</li>';
                echo '<li>' . esc_html__( 'A quick backup before your first import is always a good idea.', 'automotive-feed-import' ) . '</li>';
                echo '</ul>';
                echo '</div>';
            }
        }

        /**
         * Render title format field
         */
        public function render_title_format_field() {
            $value = $this->get_option( 'post_title_format', '{model_year} {manufacturer} {model} {style}' );
            echo '<input type="text" id="afi_post_title_format" name="' . $this->plugin_slug . '_post_title_format" value="' . esc_attr( $value ) . '" class="regular-text afi-token-input" data-preview="afi-title-preview" />';
            echo '<p class="description">' . esc_html__( 'Build your vehicle page titles from feed fields. Example: {model_year} {manufacturer} {model} {style}.', 'automotive-feed-import' ) . '</p>';
            echo '<div class="afi-token-preview"><span class="afi-preview-label">' . esc_html__( 'Preview', 'automotive-feed-import' ) . '</span><span id="afi-title-preview"></span></div>';
        }

        /**
         * Render content format field
         */
        public function render_content_format_field() {
            $value = $this->get_option( 'post_content_format', $this->default_content_format() );
            echo '<textarea id="afi_post_content_format" name="' . $this->plugin_slug . '_post_content_format" class="large-text afi-token-input" rows="4" data-preview="afi-content-preview">' . esc_textarea( $value ) . '</textarea>';
            echo '<p class="description">' . esc_html__( 'The description shown on each vehicle page. Use the tokens below; any your feed doesn\'t include are simply left out.', 'automotive-feed-import' ) . '</p>';
            echo '<div class="afi-token-preview"><span class="afi-preview-label">' . esc_html__( 'Preview', 'automotive-feed-import' ) . '</span><span id="afi-content-preview"></span></div>';
        }

        /**
         * Ready-to-use default vehicle description. Built so it still reads well
         * when a feed is missing some fields: the only fixed wording sits at the
         * sentence edges, and parse_template() collapses the whitespace left by
         * any dropped tokens — so "This 2024 Toyota Camry is now available." and
         * "This is now available." are both grammatical.
         *
         * @return string
         */
        private function default_content_format() {
            return 'This {designation} {model_year} {manufacturer} {model} {style} is now available. Contact us today to schedule a test drive or to learn more about this vehicle.';
        }

        /**
         * Render the token reference shown on the Page Templates tab.
         *
         * Lists every field the user can drop into the title or description
         * template. Sourced from the canonical field list so it never drifts;
         * the internal theme-compatibility duplicates are hidden because they
         * aren't useful in a template. Tokens are click-to-copy.
         */
        public function render_token_reference() {
            $fields = $this->get_canonical_target_fields();
            echo '<div class="afi-token-ref" style="margin:18px 0;padding:16px;background:#f6f7f7;border:1px solid #dcdcde;border-radius:6px;max-width:780px;">';
            echo '<h4 style="margin:0 0 4px;">' . esc_html__( 'Tokens you can use', 'automotive-feed-import' ) . '</h4>';
            echo '<p class="description" style="margin:0 0 12px;">' . esc_html__( 'Put any of these inside the title or description boxes above, keeping the {curly braces}. Click a token to copy it. If your feed doesn\'t include a field, that token is simply left out — no blank gaps.', 'automotive-feed-import' ) . '</p>';
            echo '<div style="display:flex;flex-wrap:wrap;gap:8px;">';
            foreach ( $fields as $key => $label ) {
                // Skip the internal theme-compatibility duplicates — they mirror a
                // real field and aren't useful in a template. Detected by the
                // '_value' key suffix (not the label, which is translated).
                if ( '_value' === substr( $key, -6 ) ) {
                    continue;
                }
                $token = '{' . $key . '}';
                echo '<button type="button" class="afi-token-chip" data-token="' . esc_attr( $token ) . '" ' . 'style="cursor:pointer;text-align:left;background:#fff;border:1px solid #c3c4c7;border-radius:4px;padding:5px 9px;line-height:1.3;">' . '<code style="background:none;padding:0;font-size:12px;color:#2271b1;">' . esc_html( $token ) . '</code>' . '<span style="display:block;font-size:11px;color:#646970;">' . esc_html( $label ) . '</span>' . '</button>';
            }
            echo '</div>';
            ?>
			<script type="text/javascript">
				(function () {
					var copied = '<?php 
            echo esc_js( __( 'Copied!', 'automotive-feed-import' ) );
            ?>';
					document.querySelectorAll('.afi-token-chip').forEach(function (btn) {
						btn.addEventListener('click', function () {
							var token = btn.getAttribute('data-token');
							if (navigator.clipboard && navigator.clipboard.writeText) {
								navigator.clipboard.writeText(token);
							}
							btn.style.borderColor = '#00a32a';
							btn.setAttribute('title', copied);
							setTimeout(function () { btn.style.borderColor = '#c3c4c7'; }, 700);
						});
					});
				})();
			</script>
			<?php 
            echo '</div>';
        }

        /**
         * Render currency symbol field
         */
        public function render_currency_symbol_field() {
            $value = $this->get_option( 'currency_symbol', '$' );
            echo '<input type="text" name="' . $this->plugin_slug . '_currency_symbol" value="' . esc_attr( $value ) . '" class="small-text" maxlength="5" />';
            echo '<p class="description">' . esc_html__( 'Symbol shown before prices (e.g. $, £, €). Defaults to $.', 'automotive-feed-import' ) . '</p>';
        }

        /**
         * Count posts this plugin has imported into the current target CPT.
         * Uses the load-bearing _ais_imported marker so the number reflects
         * only feed-managed posts, never anything the user created by hand.
         *
         * @return int
         */
        private function count_imported_posts() {
            global $wpdb;
            $count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(DISTINCT p.ID)\n\t\t\t\t FROM {$wpdb->posts} p\n\t\t\t\t INNER JOIN {$wpdb->postmeta} m ON m.post_id = p.ID\n\t\t\t\t   AND m.meta_key = '_ais_imported' AND m.meta_value = '1'\n\t\t\t\t WHERE p.post_type = %s\n\t\t\t\t AND p.post_status NOT IN ('trash', 'auto-draft')", $this->get_target_post_type() ) );
            return (int) $count;
        }

        /**
         * Best-guess which feed field holds each vehicle's unique ID, by matching
         * common naming patterns. Used to pre-select the unique-ID picker so the
         * user usually just confirms rather than hunts.
         *
         * @param array $fields Feed field names.
         * @return string Best-guess field name, or '' if none obvious.
         */
        private function guess_unique_id_field( $fields ) {
            if ( empty( $fields ) || !is_array( $fields ) ) {
                return '';
            }
            // Highest-confidence names first.
            $preferred = array(
                'stock_number',
                'stocknumber',
                'stock_no',
                'stockno',
                'stock',
                'vin',
                'vehicle_id',
                'vehicleid',
                'inventory_id',
                'listing_id',
                'id',
                'ref',
                'reference',
                'sku'
            );
            $normalized = array();
            foreach ( $fields as $field ) {
                $normalized[strtolower( preg_replace( '/[^a-z0-9]/i', '', $field ) )] = $field;
            }
            foreach ( $preferred as $candidate ) {
                $key = preg_replace( '/[^a-z0-9]/', '', $candidate );
                if ( isset( $normalized[$key] ) ) {
                    return $normalized[$key];
                }
            }
            return '';
        }

        /**
         * Human label for a stored sync-frequency key.
         *
         * @param string $key Frequency option key.
         * @return string
         */
        private function frequency_label( $key ) {
            $labels = array(
                'fiveminute'    => __( 'every 5 minutes', 'automotive-feed-import' ),
                'tenminute'     => __( 'every 10 minutes', 'automotive-feed-import' ),
                'fifteenminute' => __( 'every 15 minutes', 'automotive-feed-import' ),
                'thirtyminute'  => __( 'every 30 minutes', 'automotive-feed-import' ),
                'hourly'        => __( 'hourly', 'automotive-feed-import' ),
                'twicedaily'    => __( 'twice daily', 'automotive-feed-import' ),
                'daily'         => __( 'daily', 'automotive-feed-import' ),
            );
            return ( isset( $labels[$key] ) ? $labels[$key] : $key );
        }

        /**
         * At-a-glance status panel shown at the top of every settings tab:
         * feed link, last sync, next scheduled sync, and live vehicle count.
         * This is the dealer's "is it working?" answer without digging.
         */
        private function render_status_panel() {
            $feed_url = $this->get_option( 'xml_file_path', '' );
            $last_sync = get_option( 'afi_last_sync', array() );
            $next_ts = wp_next_scheduled( 'update_xml_event' );
            $freq = $this->get_option( 'import_frequency', 'tenminute' );
            $count = $this->count_imported_posts();
            $cpt_choices = $this->get_target_cpt_choices();
            $cpt_slug = $this->get_target_post_type();
            $cpt_label = ( isset( $cpt_choices[$cpt_slug]['label'] ) ? $cpt_choices[$cpt_slug]['label'] : $cpt_slug );
            // Feed link card.
            if ( empty( $feed_url ) ) {
                $feed_class = 'is-warn';
                $feed_value = __( 'Not set up yet', 'automotive-feed-import' );
                $feed_sub = __( 'Add your feed link on Feed & Sync.', 'automotive-feed-import' );
            } else {
                $feed_class = 'is-ok';
                $feed_value = __( 'Saved', 'automotive-feed-import' );
                $feed_sub = ( strlen( $feed_url ) > 42 ? esc_html( substr( $feed_url, 0, 42 ) ) . '…' : esc_html( $feed_url ) );
            }
            // Last sync card.
            if ( empty( $last_sync ) || empty( $last_sync['time'] ) ) {
                $sync_class = ( empty( $feed_url ) ? '' : 'is-warn' );
                $sync_value = __( 'Never', 'automotive-feed-import' );
                $sync_sub = __( 'Click “Sync Inventory Now” to run your first import.', 'automotive-feed-import' );
            } else {
                $status = ( isset( $last_sync['status'] ) ? $last_sync['status'] : 'success' );
                $sync_class = ( 'warning' === $status ? 'is-warn' : 'is-ok' );
                /* translators: %s: human-readable time difference, e.g. "5 minutes" */
                $sync_value = sprintf( __( '%s ago', 'automotive-feed-import' ), human_time_diff( (int) $last_sync['time'], time() ) );
                $parts = array();
                $parts[] = sprintf( __( '%d added', 'automotive-feed-import' ), (int) ($last_sync['created'] ?? 0) );
                $parts[] = sprintf( __( '%d updated', 'automotive-feed-import' ), (int) ($last_sync['updated'] ?? 0) );
                if ( !empty( $last_sync['skipped'] ) ) {
                    $parts[] = sprintf( __( '%d skipped', 'automotive-feed-import' ), (int) $last_sync['skipped'] );
                }
                $sync_sub = implode( ', ', $parts );
            }
            // Next sync card.
            if ( $next_ts ) {
                $next_class = 'is-ok';
                /* translators: %s: human-readable time difference until next run */
                /* translators: %s: human-readable time until next run, e.g. "5 minutes". */
                $next_value = sprintf( _x( 'in %s', 'time until next sync', 'automotive-feed-import' ), human_time_diff( time(), (int) $next_ts ) );
                /* translators: %s: frequency label such as "every 10 minutes" */
                $next_sub = sprintf( __( 'Checks your feed %s.', 'automotive-feed-import' ), $this->frequency_label( $freq ) );
            } else {
                $next_class = 'is-warn';
                $next_value = __( 'Not scheduled', 'automotive-feed-import' );
                $next_sub = __( 'Save your settings to start automatic syncing.', 'automotive-feed-import' );
            }
            ?>
			<div class="afi-status-panel">
				<div class="afi-status-card <?php 
            echo esc_attr( $feed_class );
            ?>">
					<p class="afi-status-label"><?php 
            esc_html_e( 'Feed link', 'automotive-feed-import' );
            ?></p>
					<div class="afi-status-value"><span class="afi-dot"></span><?php 
            echo esc_html( $feed_value );
            ?></div>
					<p class="afi-status-sub"><?php 
            echo esc_html( $feed_sub );
            ?></p>
				</div>
				<div class="afi-status-card <?php 
            echo esc_attr( $sync_class );
            ?>">
					<p class="afi-status-label"><?php 
            esc_html_e( 'Last sync', 'automotive-feed-import' );
            ?></p>
					<div class="afi-status-value"><span class="afi-dot"></span><?php 
            echo esc_html( $sync_value );
            ?></div>
					<p class="afi-status-sub"><?php 
            echo esc_html( $sync_sub );
            ?></p>
				</div>
				<div class="afi-status-card <?php 
            echo esc_attr( $next_class );
            ?>">
					<p class="afi-status-label"><?php 
            esc_html_e( 'Next sync', 'automotive-feed-import' );
            ?></p>
					<div class="afi-status-value"><span class="afi-dot"></span><?php 
            echo esc_html( $next_value );
            ?></div>
					<p class="afi-status-sub"><?php 
            echo esc_html( $next_sub );
            ?></p>
				</div>
				<div class="afi-status-card">
					<p class="afi-status-label"><?php 
            esc_html_e( 'Vehicles live', 'automotive-feed-import' );
            ?></p>
					<div class="afi-status-value"><?php 
            echo esc_html( number_format_i18n( $count ) );
            ?></div>
					<p class="afi-status-sub"><?php 
            /* translators: %s: post type label, e.g. "Vehicles" */
            echo esc_html( sprintf( _x( 'in %s', 'stored in post type', 'automotive-feed-import' ), $cpt_label ) );
            ?></p>
				</div>
			</div>
			<?php 
        }

        /**
         * Register the dashboard "At a Glance" widget for admins, so the sync
         * status is visible without navigating into the plugin.
         *
         * @since 2.6.0
         * @return void
         */
        public function register_dashboard_widget() {
            if ( !current_user_can( 'manage_options' ) ) {
                return;
            }
            wp_add_dashboard_widget( 'afi_dashboard_status', __( 'Automotive Inventory — Sync Status', 'automotive-feed-import' ), array($this, 'render_dashboard_widget') );
        }

        /**
         * Render the dashboard widget: the same at-a-glance status panel used on
         * the settings screen, plus a link through to the settings.
         *
         * @since 2.6.0
         * @return void
         */
        public function render_dashboard_widget() {
            echo '<div class="afi-dash-widget">';
            $this->render_status_panel();
            echo '<p style="margin:8px 0 0;"><a href="' . esc_url( $this->settings_page_url() ) . '">' . esc_html__( 'Open Automotive Sync settings', 'automotive-feed-import' ) . '</a></p>';
            echo '</div>';
        }

        /**
         * First-run progress strip (Connect → Map → Sync). Reflects real state
         * and stops showing once the first successful import has run, so it
         * guides new users without nagging established ones.
         */
        private function render_setup_steps() {
            // Once a vehicle has been imported, setup is effectively done.
            if ( (int) get_option( 'afi_total_imports', 0 ) > 0 ) {
                return;
            }
            $feed_set = !empty( $this->get_option( 'xml_file_path', '' ) );
            $has_uid = false;
            $mappings = $this->get_field_mappings();
            if ( is_array( $mappings ) ) {
                foreach ( $mappings as $cfg ) {
                    if ( !empty( $cfg['enabled'] ) && isset( $cfg['target'] ) && 'stock_number' === $cfg['target'] ) {
                        $has_uid = true;
                        break;
                    }
                }
            }
            $steps = array(array(
                'title' => __( 'Connect your feed', 'automotive-feed-import' ),
                'sub'   => __( 'Add your feed link', 'automotive-feed-import' ),
                'done'  => $feed_set,
            ), array(
                'title' => __( 'Review matched fields', 'automotive-feed-import' ),
                'sub'   => __( 'Confirm your unique ID', 'automotive-feed-import' ),
                'done'  => $feed_set && $has_uid,
            ), array(
                'title' => __( 'Run your first sync', 'automotive-feed-import' ),
                'sub'   => __( 'Bring in your vehicles', 'automotive-feed-import' ),
                'done'  => false,
            ));
            // The last step is actionable as soon as the feed and unique ID are
            // set, so users import right here instead of hunting for the Sync
            // button on another tab. After the first import this whole strip is
            // gone, so a one-shot run_import link is exactly what's needed.
            $ready_to_sync = $feed_set && $has_uid;
            $sync_url = $this->settings_page_url( '&tab=log&action=run_import&_wpnonce=' . wp_create_nonce( 'run_import' ) );
            // Mark the first not-done step as current.
            $current_marked = false;
            ?>
			<ol class="afi-steps">
				<?php 
            foreach ( $steps as $index => $step ) {
                $is_sync_step = 2 === $index;
                $class = ( $step['done'] ? 'is-done' : '' );
                if ( !$step['done'] && !$current_marked ) {
                    $class .= ' is-current';
                    $current_marked = true;
                }
                ?>
					<li class="<?php 
                echo esc_attr( trim( $class ) );
                ?>">
						<span class="afi-step-title"><?php 
                echo esc_html( $step['title'] );
                ?></span>
						<?php 
                if ( $is_sync_step && $ready_to_sync ) {
                    ?>
							<button type="button" class="button button-primary button-small afi-step-action afi-sync-trigger" data-href="<?php 
                    echo esc_url( $sync_url );
                    ?>"><?php 
                    esc_html_e( 'Run first sync now', 'automotive-feed-import' );
                    ?></button>
						<?php 
                } else {
                    ?>
							<span class="afi-step-sub"><?php 
                    echo esc_html( $step['sub'] );
                    ?></span>
						<?php 
                }
                ?>
					</li>
				<?php 
            }
            ?>
			</ol>
			<?php 
        }

        /**
         * Render settings page
         */
        public function render_settings_page() {
            if ( !current_user_can( 'manage_options' ) ) {
                wp_die( esc_html__( 'You do not have permission to access this page.', 'automotive-feed-import' ) );
            }
            // Get active tab from URL or default to 'general'
            $active_tab = ( isset( $_GET['tab'] ) ? sanitize_text_field( $_GET['tab'] ) : 'general' );
            // Page Templates apply to the built-in Vehicles type only. When
            // importing into a third-party CPT, titles/descriptions are deduced
            // automatically, so the tab is hidden — and a stale &tab=format URL
            // falls back to Feed & Sync rather than rendering an empty body.
            $is_native = 'vehicles' === $this->get_target_post_type();
            if ( 'format' === $active_tab && !$is_native ) {
                $active_tab = 'general';
            }
            ?>
			<div class="wrap">
				<h1><?php 
            esc_html_e( 'Automotive Inventory Importer Settings', 'automotive-feed-import' );
            ?></h1>

				<!-- Tab Navigation -->
				<h2 class="nav-tab-wrapper">
					<a href="?page=<?php 
            echo esc_attr( $this->plugin_slug );
            ?>&tab=general" class="nav-tab <?php 
            echo ( $active_tab === 'general' ? 'nav-tab-active' : '' );
            ?>"><?php 
            esc_html_e( 'Feed & Sync', 'automotive-feed-import' );
            ?></a>
					<a href="?page=<?php 
            echo esc_attr( $this->plugin_slug );
            ?>&tab=mapping" class="nav-tab <?php 
            echo ( $active_tab === 'mapping' ? 'nav-tab-active' : '' );
            ?>"><?php 
            esc_html_e( 'Field Mapping', 'automotive-feed-import' );
            ?></a>
					<?php 
            if ( $is_native ) {
                ?>
						<a href="?page=<?php 
                echo esc_attr( $this->plugin_slug );
                ?>&tab=format" class="nav-tab <?php 
                echo ( $active_tab === 'format' ? 'nav-tab-active' : '' );
                ?>"><?php 
                esc_html_e( 'Page Templates', 'automotive-feed-import' );
                ?></a>
					<?php 
            }
            ?>
					<a href="?page=<?php 
            echo esc_attr( $this->plugin_slug );
            ?>&tab=log" class="nav-tab <?php 
            echo ( $active_tab === 'log' ? 'nav-tab-active' : '' );
            ?>"><?php 
            esc_html_e( 'Import History', 'automotive-feed-import' );
            ?></a>
				</h2>

				<div class="afi-tab-content">
					<?php 
            $this->render_status_panel();
            ?>
					<?php 
            $this->render_setup_steps();
            ?>
					<div id="afi-qsg-show-wrap" style="display:none; text-align:right; margin-bottom:10px;">
						<button type="button" id="afi-qsg-show" class="button button-secondary">🚀 <?php 
            esc_html_e( 'Show Quick Start Guide', 'automotive-feed-import' );
            ?></button>
					</div>
					<div class="afi-settings-layout">
						<div class="afi-settings-main">
							<?php 
            if ( $active_tab === 'general' ) {
                ?>
								<!-- General Settings Tab -->
								<form method="post" action="options.php">
									<?php 
                settings_fields( $this->plugin_slug . '_settings' );
                // Hidden fields to preserve format settings when saving general settings
                $title_format = $this->get_option( 'post_title_format', '{model_year} {manufacturer} {model} {style}' );
                $content_format = $this->get_option( 'post_content_format', $this->default_content_format() );
                echo '<input type="hidden" name="' . $this->plugin_slug . '_post_title_format" value="' . esc_attr( $title_format ) . '" />';
                echo '<input type="hidden" name="' . $this->plugin_slug . '_post_content_format" value="' . esc_attr( $content_format ) . '" />';
                $currency_symbol_hidden = $this->get_option( 'currency_symbol', '$' );
                echo '<input type="hidden" name="' . $this->plugin_slug . '_currency_symbol" value="' . esc_attr( $currency_symbol_hidden ) . '" />';
                ?>
									<table class="form-table">
										<?php 
                // Manually render general settings fields
                do_settings_fields( $this->plugin_slug, 'afi_general_section' );
                ?>
									</table>
									<?php 
                submit_button();
                ?>
									<p>
										<button type="button" class="button button-primary afi-sync-trigger" data-href="<?php 
                echo esc_url( $this->settings_page_url( '&tab=general&action=run_import&_wpnonce=' . wp_create_nonce( 'run_import' ) ) );
                ?>"><?php 
                esc_html_e( 'Sync Inventory Now', 'automotive-feed-import' );
                ?></button>
										<a href="<?php 
                echo esc_url( $this->settings_page_url( '&action=download_sample&_wpnonce=' . wp_create_nonce( 'download_sample' ) ) );
                ?>" class="button" style="margin-left: 8px;"><?php 
                esc_html_e( 'Download Sample XML', 'automotive-feed-import' );
                ?></a>
										<a href="<?php 
                echo esc_url( $this->settings_page_url( '&action=download_sample_csv&_wpnonce=' . wp_create_nonce( 'download_sample_csv' ) ) );
                ?>" class="button" style="margin-left: 8px;"><?php 
                esc_html_e( 'Download Sample CSV', 'automotive-feed-import' );
                ?></a>
										<a href="<?php 
                echo esc_url( $this->settings_page_url( '&tab=mapping' ) );
                ?>" class="button" style="margin-left: 8px;"><?php 
                esc_html_e( 'Configure Field Mapping', 'automotive-feed-import' );
                ?></a>
									</p>
								</form>

							<?php 
            } elseif ( $active_tab === 'format' ) {
                ?>
								<!-- Post Format Tab -->
								<form method="post" action="options.php">
									<?php 
                settings_fields( $this->plugin_slug . '_settings' );
                // Hidden fields to preserve general settings when saving format settings
                $xml_file_path = $this->get_option( 'xml_file_path' );
                $import_frequency = $this->get_option( 'import_frequency', 'tenminute' );
                echo '<input type="hidden" name="' . $this->plugin_slug . '_xml_file_path" value="' . esc_attr( $xml_file_path ) . '" />';
                echo '<input type="hidden" name="' . $this->plugin_slug . '_import_frequency" value="' . esc_attr( $import_frequency ) . '" />';
                $removed_vehicle_action = $this->get_option( 'removed_vehicle_action', 'nothing' );
                echo '<input type="hidden" name="' . $this->plugin_slug . '_removed_vehicle_action" value="' . esc_attr( $removed_vehicle_action ) . '" />';
                $target_post_type_hidden = $this->get_target_post_type();
                echo '<input type="hidden" name="' . $this->plugin_slug . '_target_post_type" value="' . esc_attr( $target_post_type_hidden ) . '" />';
                ?>
									<h3><?php 
                esc_html_e( 'How Vehicle Pages Look', 'automotive-feed-import' );
                ?></h3>
									<p><?php 
                esc_html_e( 'Decide how titles and descriptions should be built from your feed fields, using tokens like {manufacturer}, {model}, {model_year}, etc.', 'automotive-feed-import' );
                ?></p>
									<table class="form-table">
										<?php 
                // Manually render format settings fields
                do_settings_fields( $this->plugin_slug, 'afi_format_section' );
                ?>
									</table>
									<?php 
                $this->render_token_reference();
                ?>
									<?php 
                submit_button();
                ?>
								</form>

							<?php 
            } elseif ( $active_tab === 'mapping' ) {
                ?>
								<!-- Field Mapping Tab -->
								<h2><?php 
                esc_html_e( 'Match Your Feed Fields', 'automotive-feed-import' );
                ?></h2>
								<p><?php 
                esc_html_e( 'Tell us which field in your feed matches each vehicle detail. We fill in sensible matches for you — just review them, make sure your unique ID is set below, and save.', 'automotive-feed-import' );
                ?></p>

								<?php 
                // Show notification if redirected after feed URL change
                if ( isset( $_GET['feed_changed'] ) && $_GET['feed_changed'] === '1' ) {
                    echo '<div class="notice notice-info is-dismissible"><p><strong>' . esc_html__( 'Feed link updated!', 'automotive-feed-import' ) . '</strong> ' . esc_html__( 'Review your field matches below, set your unique ID, then save to start importing.', 'automotive-feed-import' ) . '</p></div>';
                }
                // Check if feed URL is set
                $feed_url = $this->get_option( 'xml_file_path', '' );
                if ( empty( $feed_url ) ) {
                    echo '<div class="notice notice-warning"><p><strong>' . esc_html__( 'No feed link yet.', 'automotive-feed-import' ) . '</strong> ' . sprintf( 
                        /* translators: %s: opening and closing link tags */
                        esc_html__( 'Add your inventory link on the %1$sFeed & Sync%2$s tab first.', 'automotive-feed-import' ),
                        '<a href="?page=' . esc_attr( $this->plugin_slug ) . '&tab=general">',
                        '</a>'
                     ) . '</p></div>';
                } else {
                    // Extract fields from XML
                    $xml_fields = $this->extract_xml_fields();
                    $current_mappings = $this->get_field_mappings();
                    // Determine which feed field (if any) currently provides the unique ID.
                    $current_uid_field = '';
                    foreach ( $current_mappings as $src => $cfg ) {
                        if ( !empty( $cfg['enabled'] ) && isset( $cfg['target'] ) && 'stock_number' === $cfg['target'] ) {
                            $current_uid_field = $src;
                            break;
                        }
                    }
                    if ( $xml_fields === false ) {
                        echo '<div class="notice notice-error"><p><strong>' . esc_html__( 'We could not read your feed.', 'automotive-feed-import' ) . '</strong> ' . esc_html__( 'Please check your feed link is correct, then try the “Check feed link” button on the Feed & Sync tab.', 'automotive-feed-import' ) . '</p></div>';
                    } else {
                        // Best-guess the unique ID field when nothing is mapped yet,
                        // so the prominent selector starts on a sensible choice.
                        $guessed_uid = ( '' !== $current_uid_field ? $current_uid_field : $this->guess_unique_id_field( $xml_fields ) );
                        ?>
										<form method="post" action="<?php 
                        echo esc_url( admin_url( 'admin-post.php' ) );
                        ?>">
											<input type="hidden" name="action" value="afi_save_field_mappings" />
											<?php 
                        wp_nonce_field( 'afi_save_field_mappings', 'afi_mapping_nonce' );
                        ?>

											<?php 
                        // Prominent, plain-language unique-ID picker. This is the
                        // single hard requirement for import to work, so it gets
                        // first-class treatment instead of a buried error.
                        $uid_is_set = '' !== $current_uid_field;
                        ?>
											<div class="afi-uid-callout <?php 
                        echo ( $uid_is_set ? 'is-set' : '' );
                        ?>">
												<h3><?php 
                        echo ( $uid_is_set ? '✓ ' : '' );
                        esc_html_e( 'Which field is each vehicle’s unique ID?', 'automotive-feed-import' );
                        ?></h3>
												<p><?php 
                        esc_html_e( 'Every vehicle needs one field that never changes and is different for each car — like a stock number or VIN. We use it to match updates to the right vehicle.', 'automotive-feed-import' );
                        ?></p>
												<label class="screen-reader-text" for="afi_unique_id_field"><?php 
                        esc_html_e( 'Unique ID field', 'automotive-feed-import' );
                        ?></label>
												<select name="afi_unique_id_field" id="afi_unique_id_field">
													<option value=""><?php 
                        esc_html_e( '— Select the field —', 'automotive-feed-import' );
                        ?></option>
													<?php 
                        foreach ( $xml_fields as $field ) {
                            ?>
														<option value="<?php 
                            echo esc_attr( $field );
                            ?>" <?php 
                            selected( $guessed_uid, $field );
                            ?>><?php 
                            echo esc_html( $field );
                            ?></option>
													<?php 
                        }
                        ?>
												</select>
												<?php 
                        if ( !$uid_is_set ) {
                            ?>
													<p style="margin:8px 0 0;color:#996800;"><strong><?php 
                            esc_html_e( 'Pick your unique ID field above, then save, to start importing.', 'automotive-feed-import' );
                            ?></strong></p>
												<?php 
                        }
                        ?>
											</div>

											<div style="background: #f0f0f1; padding: 15px; margin: 20px 0; border-left: 4px solid #0073aa;">
												<p style="margin: 0;"><strong>💡 <?php 
                        esc_html_e( 'Tip:', 'automotive-feed-import' );
                        ?></strong> <?php 
                        /* translators: %d: number of fields found in the feed */
                        echo esc_html( sprintf( __( 'We found %d fields in your feed. Turn off any you don’t need, and adjust a match if something looks off.', 'automotive-feed-import' ), count( $xml_fields ) ) );
                        ?></p>
											</div>

											<?php 
                        // Pro auto-map button (preset or best-guess) or free
                        // upsell. Uses a GET link (no nested <form>), so it's
                        // safe inside the mapping form.
                        $this->render_mapping_assist_block();
                        ?>

											<table class="wp-list-table widefat fixed striped" style="margin-top: 20px;">
												<thead>
													<tr>
														<th style="width: 60px;"><?php 
                        esc_html_e( 'Use', 'automotive-feed-import' );
                        ?></th>
														<th style="width: 40%;"><?php 
                        esc_html_e( 'Field in your feed', 'automotive-feed-import' );
                        ?></th>
														<th style="width: 40%;"><?php 
                        esc_html_e( 'Saves as', 'automotive-feed-import' );
                        ?></th>
														<th style="width: 200px;"><?php 
                        esc_html_e( 'Example from your feed', 'automotive-feed-import' );
                        ?></th>
													</tr>
												</thead>
												<tbody>
													<?php 
                        // Build example values field-by-field: the first
                        // non-empty value seen across a bounded window of
                        // records. Some feeds omit empty tags per record, so a
                        // field can be blank on the first vehicle but filled on
                        // the next — this shows a real example either way.
                        $units = ( $this->is_csv_feed() ? $this->load_csv() : $this->load_xml() );
                        $sample_values = array();
                        foreach ( array_slice( ( is_array( $units ) ? $units : array() ), 0, 100 ) as $sample_unit ) {
                            foreach ( $sample_unit as $sample_key => $sample_val ) {
                                if ( !isset( $sample_values[$sample_key] ) && '' !== trim( (string) $sample_val ) ) {
                                    $sample_values[$sample_key] = $sample_val;
                                }
                            }
                        }
                        // Get available target fields
                        $available_targets = $this->get_available_target_fields();
                        foreach ( $xml_fields as $field ) {
                            $is_enabled = ( isset( $current_mappings[$field]['enabled'] ) ? $current_mappings[$field]['enabled'] : true );
                            $target_field = ( isset( $current_mappings[$field]['target'] ) ? $current_mappings[$field]['target'] : $field );
                            $sample_value = ( isset( $sample_values[$field] ) ? $sample_values[$field] : '' );
                            // Truncate long sample values
                            if ( strlen( $sample_value ) > 50 ) {
                                $sample_value = substr( $sample_value, 0, 50 ) . '...';
                            }
                            ?>
														<tr>
															<td>
																<input type="checkbox" name="afi_mapping[<?php 
                            echo esc_attr( $field );
                            ?>][enabled]" value="1" <?php 
                            checked( $is_enabled, true );
                            ?> />
															</td>
															<td>
																<strong><?php 
                            echo esc_html( $field );
                            ?></strong>
															</td>
															<td>
																<select name="afi_mapping[<?php 
                            echo esc_attr( $field );
                            ?>][target]" class="regular-text afi-target-select" style="width: 100%;">
																	<?php 
                            foreach ( $available_targets as $target_key => $target_label ) {
                                ?>
																		<option value="<?php 
                                echo esc_attr( $target_key );
                                ?>" <?php 
                                selected( $target_field, $target_key );
                                ?>>
																			<?php 
                                echo esc_html( $target_label );
                                ?> (<?php 
                                echo esc_html( $target_key );
                                ?>)
																		</option>
																	<?php 
                            }
                            ?>
																	<!-- Add current field if not in list -->
																	<?php 
                            if ( !isset( $available_targets[$field] ) ) {
                                ?>
																		<option value="<?php 
                                echo esc_attr( $field );
                                ?>" <?php 
                                selected( $target_field, $field );
                                ?>>
																			<?php 
                                echo esc_html( $field );
                                ?> (custom)
																		</option>
																	<?php 
                            }
                            ?>
																	<!-- Add selected field if different and not in list -->
																	<?php 
                            if ( $target_field !== $field && !isset( $available_targets[$target_field] ) ) {
                                ?>
																		<option value="<?php 
                                echo esc_attr( $target_field );
                                ?>" selected>
																			<?php 
                                echo esc_html( $target_field );
                                ?> (custom)
																		</option>
																	<?php 
                            }
                            ?>
																	<option value="__afi_custom__"><?php 
                            esc_html_e( '✎ Enter a custom field name…', 'automotive-feed-import' );
                            ?></option>
																</select>
																<input type="text" name="afi_mapping[<?php 
                            echo esc_attr( $field );
                            ?>][custom_target]" class="afi-custom-target regular-text" placeholder="<?php 
                            esc_attr_e( 'Type the exact field name', 'automotive-feed-import' );
                            ?>" style="display:none; margin-top:6px; width:100%;" />
															</td>
															<td>
																<code style="font-size: 11px; color: #666;"><?php 
                            echo esc_html( $sample_value );
                            ?></code>
															</td>
														</tr>
													<?php 
                        }
                        ?>
												</tbody>
											</table>

											<?php 
                        submit_button( __( 'Save Field Mappings', 'automotive-feed-import' ) );
                        ?>

											<p style="margin-top: 20px;">
												<button type="button" class="button" onclick="if(confirm('<?php 
                        echo esc_js( __( 'Reset all mappings to default (1:1 mapping)?', 'automotive-feed-import' ) );
                        ?>')) { document.getElementById('afi-reset-mappings').value='1'; this.form.submit(); }"><?php 
                        esc_html_e( 'Reset to Defaults', 'automotive-feed-import' );
                        ?></button>
												<input type="hidden" id="afi-reset-mappings" name="afi_reset_mappings" value="0" />
											</p>
											<?php 
                        // Custom-target toggle behavior lives in assets/js/admin.js (initMappingCustomToggle).
                        ?>
										</form>
								<?php 
                    }
                }
                ?>

							<?php 
            } elseif ( $active_tab === 'log' ) {
                ?>
								<!-- Import Log Tab -->
								<h2><?php 
                esc_html_e( 'Import History', 'automotive-feed-import' );
                ?></h2>
								<?php 
                $this->display_log();
                ?>
								<p style="margin-top: 15px;">
									<button type="button" class="button" onclick="if(confirm('<?php 
                echo esc_js( __( 'Clear all saved import history?', 'automotive-feed-import' ) );
                ?>')) { window.location.href='<?php 
                echo esc_url( $this->settings_page_url( '&tab=log&action=clear_log&_wpnonce=' . wp_create_nonce( 'clear_log' ) ) );
                ?>'; }"><?php 
                esc_html_e( 'Clear History', 'automotive-feed-import' );
                ?></button>
								</p>
							<?php 
            }
            ?>
						</div>
						<div class="afi-settings-sidebar" id="afi-qsg-sidebar">
							<div class="afi-qsg-card">
								<div class="afi-qsg-header">
									<h3>🚀 <?php 
            esc_html_e( 'Quick Start Guide', 'automotive-feed-import' );
            ?></h3>
									<div class="afi-qsg-actions">
										<button type="button" class="afi-qsg-toggle" aria-expanded="true" aria-label="<?php 
            esc_attr_e( 'Collapse guide', 'automotive-feed-import' );
            ?>" title="<?php 
            esc_attr_e( 'Collapse', 'automotive-feed-import' );
            ?>">&#9662;</button>
										<button type="button" class="afi-qsg-hide" aria-label="<?php 
            esc_attr_e( 'Hide guide', 'automotive-feed-import' );
            ?>" title="<?php 
            esc_attr_e( 'Hide this guide', 'automotive-feed-import' );
            ?>">&times;</button>
									</div>
								</div>
								<div class="afi-qsg-body">
									<ol>
										<li><?php 
            echo wp_kses_post( __( '<strong>Add your feed:</strong> On the <strong>Feed &amp; Sync</strong> tab, paste your XML or CSV feed link (or click <strong>Browse Server</strong> to pick a file already uploaded to your site). Under <strong>Import Vehicles Into</strong>, keep <strong>Vehicles</strong> or choose another post type from your theme or plugin. Click <strong>Save Changes</strong>.', 'automotive-feed-import' ) );
            ?></li>
										<li><?php 
            echo wp_kses_post( __( '<strong>Map your fields:</strong> On the <strong>Field Mapping</strong> tab, match each feed field to the right field, then save.', 'automotive-feed-import' ) );
            ?> <?php 
            if ( $is_native ) {
                echo wp_kses_post( __( 'Importing into <strong>Vehicles</strong> fills in sensible defaults for you.', 'automotive-feed-import' ) );
            } else {
                esc_html_e( 'Match the fields your chosen post type uses.', 'automotive-feed-import' );
            }
            ?></li>
										<li><?php 
            echo wp_kses_post( __( '<strong>Set your sync schedule:</strong> Choose how often to check your feed so your website matches your lot, then click <strong>Save Changes</strong>.', 'automotive-feed-import' ) );
            ?></li>
										<li><?php 
            echo wp_kses_post( __( '<strong>Sync &amp; review:</strong> Click <strong>Sync Inventory Now</strong> to run your first import, then review your listings under the <strong>Vehicles</strong> menu.', 'automotive-feed-import' ) );
            if ( $is_native ) {
                ?> <?php 
                echo wp_kses_post( __( 'Fine-tune titles and descriptions on the <strong>Page Templates</strong> tab.', 'automotive-feed-import' ) );
            }
            ?></li>
									</ol>

									<?php 
            if ( $this->is_pro_active() ) {
                ?>
										<div class="afi-qsg-pro">
											<p class="afi-qsg-pro-title">&#11088; <?php 
                esc_html_e( 'You\'re on Pro — handy shortcuts', 'automotive-feed-import' );
                ?></p>
											<ul>
												<li><?php 
                echo wp_kses_post( __( 'One-click <strong>auto-map</strong> and ready-made presets when importing into another plugin\'s post type.', 'automotive-feed-import' ) );
                ?></li>
												<li><?php 
                echo wp_kses_post( __( '<strong>Lead capture</strong> forms on every vehicle page.', 'automotive-feed-import' ) );
                ?></li>
												<li><?php 
                esc_html_e( 'Shopper search & filters with a price slider, a full-screen photo viewer, and richer Google listings are added for you.', 'automotive-feed-import' );
                ?></li>
											</ul>
										</div>
									<?php 
            } else {
                ?>
										<div class="afi-qsg-pro">
											<p class="afi-qsg-pro-title">&#11088; <?php 
                esc_html_e( 'Do more with Pro', 'automotive-feed-import' );
                ?></p>
											<ul>
												<li><?php 
                esc_html_e( 'Auto-map fields & ready-made setups for popular dealer plugins.', 'automotive-feed-import' );
                ?></li>
												<li><?php 
                esc_html_e( 'Lead capture forms to turn visitors into inquiries.', 'automotive-feed-import' );
                ?></li>
												<li><?php 
                esc_html_e( 'Shopper search & filters, full-screen photo viewer, and richer Google listings.', 'automotive-feed-import' );
                ?></li>
											</ul>
											<?php 
                if ( function_exists( 'afi_fs' ) ) {
                    ?>
												<a href="<?php 
                    echo esc_url( afi_fs()->get_upgrade_url() );
                    ?>" class="button button-primary" style="margin-top:6px;"><?php 
                    esc_html_e( 'Upgrade to Pro', 'automotive-feed-import' );
                    ?></a>
											<?php 
                }
                ?>
										</div>
									<?php 
            }
            ?>

									<p class="afi-qsg-tip"><em><?php 
            esc_html_e( 'Not sure about your link? Check your inventory provider\'s portal for an "XML Feed", "CSV Export", or "Inventory Export" URL.', 'automotive-feed-import' );
            ?></em></p>
								</div>
							</div>
						</div>
					</div>
				</div>

			</div>
		<?php 
        }

        /**
         * Display import log with summary stats and color-coded errors
         */
        private function display_log() {
            if ( !file_exists( $this->log_file ) ) {
                echo '<p>' . esc_html__( 'No log file found.', 'automotive-feed-import' ) . '</p>';
                return;
            }
            $log_content = file_get_contents( $this->log_file );
            if ( empty( trim( $log_content ) ) ) {
                echo '<p>' . esc_html__( 'Log is empty.', 'automotive-feed-import' ) . '</p>';
                return;
            }
            $lines = explode( "\n", rtrim( $log_content ) );
            // --- Compute summary stats ---
            $total_runs = 0;
            $last_run_status = 'Unknown';
            $last_vehicles = null;
            $last_start_index = null;
            $last_complete_index = null;
            foreach ( $lines as $i => $line ) {
                if ( strpos( $line, 'Starting feed import process' ) !== false ) {
                    $last_start_index = $i;
                }
                if ( strpos( $line, 'Import complete:' ) !== false ) {
                    $total_runs++;
                    $last_complete_index = $i;
                    // Parse vehicle counts: "X created, Y updated" or "X created, Y updated, Z removed"
                    if ( preg_match( '/(\\d+) created,\\s*(\\d+) updated/', $line, $m ) ) {
                        $last_vehicles = (int) $m[1] + (int) $m[2];
                    }
                }
            }
            // Determine last run status
            if ( $last_start_index !== null ) {
                if ( $last_complete_index !== null && $last_complete_index >= $last_start_index ) {
                    // Check for errors between last start and last complete
                    $has_error = false;
                    for ($i = $last_start_index; $i <= $last_complete_index; $i++) {
                        if ( strpos( $lines[$i], '] [ERROR]' ) !== false ) {
                            $has_error = true;
                            break;
                        }
                    }
                    $last_run_status = ( $has_error ? 'Completed with errors' : 'Success' );
                } else {
                    $last_run_status = 'Failed';
                }
            } elseif ( $total_runs > 0 ) {
                $last_run_status = 'Success';
            }
            // Collect the error lines from the most recent run so we can turn
            // them into a plain-language issue list instead of raw log noise.
            $last_errors = array();
            if ( $last_start_index !== null ) {
                $end = ( $last_complete_index !== null && $last_complete_index >= $last_start_index ? $last_complete_index : count( $lines ) - 1 );
                for ($i = $last_start_index; $i <= $end; $i++) {
                    if ( isset( $lines[$i] ) && strpos( $lines[$i], '] [ERROR]' ) !== false ) {
                        $last_errors[] = $lines[$i];
                    }
                }
            }
            $status_class = ( 'Success' === $last_run_status ? 'is-ok' : (( 'Completed with errors' === $last_run_status || 'Failed' === $last_run_status ? 'is-bad' : '' )) );
            $last_sync = get_option( 'afi_last_sync', array() );
            // --- Plain-language summary of the last run ---
            ?>
			<div class="afi-log-summary">
				<div class="afi-status-panel">
					<div class="afi-status-card">
						<p class="afi-status-label"><?php 
            esc_html_e( 'Total syncs', 'automotive-feed-import' );
            ?></p>
						<div class="afi-status-value"><?php 
            echo esc_html( number_format_i18n( $total_runs ) );
            ?></div>
					</div>
					<div class="afi-status-card <?php 
            echo esc_attr( $status_class );
            ?>">
						<p class="afi-status-label"><?php 
            esc_html_e( 'Last sync', 'automotive-feed-import' );
            ?></p>
						<div class="afi-status-value"><span class="afi-dot"></span><?php 
            $friendly = array(
                'Success'               => __( 'All good', 'automotive-feed-import' ),
                'Completed with errors' => __( 'Finished with issues', 'automotive-feed-import' ),
                'Failed'                => __( 'Did not finish', 'automotive-feed-import' ),
                'Unknown'               => __( 'Unknown', 'automotive-feed-import' ),
            );
            echo esc_html( ( isset( $friendly[$last_run_status] ) ? $friendly[$last_run_status] : $last_run_status ) );
            ?></div>
						<?php 
            if ( !empty( $last_sync['time'] ) ) {
                ?>
							<p class="afi-status-sub"><?php 
                /* translators: %s: human-readable time difference */
                echo esc_html( sprintf( __( '%s ago', 'automotive-feed-import' ), human_time_diff( (int) $last_sync['time'], time() ) ) );
                ?></p>
						<?php 
            }
            ?>
					</div>
					<div class="afi-status-card">
						<p class="afi-status-label"><?php 
            esc_html_e( 'Vehicles (last sync)', 'automotive-feed-import' );
            ?></p>
						<div class="afi-status-value"><?php 
            echo ( $last_vehicles !== null ? esc_html( number_format_i18n( $last_vehicles ) ) : '&mdash;' );
            ?></div>
						<?php 
            if ( !empty( $last_sync ) ) {
                ?>
							<p class="afi-status-sub"><?php 
                echo esc_html( sprintf( 
                    /* translators: 1: added count, 2: updated count */
                    __( '%1$d added, %2$d updated', 'automotive-feed-import' ),
                    (int) ($last_sync['created'] ?? 0),
                    (int) ($last_sync['updated'] ?? 0)
                 ) );
                ?></p>
						<?php 
            }
            ?>
					</div>
				</div>

				<?php 
            if ( !empty( $last_errors ) ) {
                ?>
					<div class="afi-log-issues">
						<h4><?php 
                echo esc_html( sprintf( 
                    /* translators: %d: number of issues in the last run */
                    _n(
                        '%d issue needs your attention',
                        '%d issues need your attention',
                        count( $last_errors ),
                        'automotive-feed-import'
                    ),
                    count( $last_errors )
                 ) );
                ?></h4>
						<ul>
							<?php 
                // Group identical issues and translate the common ones into
                // plain language with a clear "what to do" next step.
                $grouped = array();
                foreach ( $last_errors as $err ) {
                    $msg = trim( preg_replace( '/^\\[[^\\]]*\\]\\s*\\[ERROR\\]\\s*/', '', $err ) );
                    if ( false !== stripos( $msg, 'missing stock_number' ) || false !== stripos( $msg, "missing stock number" ) ) {
                        $msg = __( 'Some vehicles had no unique ID, so they were skipped. Fix: open the Field Mapping tab and set “Which field is each vehicle’s unique ID?”', 'automotive-feed-import' );
                    } elseif ( false !== stripos( $msg, 'image' ) ) {
                        $msg = __( 'A vehicle photo could not be downloaded. This is usually temporary — it will retry on the next sync.', 'automotive-feed-import' );
                    } elseif ( false !== stripos( $msg, 'disclaimer' ) ) {
                        $msg = __( 'Importing into another plugin’s section needs your confirmation first. Open Settings and confirm to enable syncing.', 'automotive-feed-import' );
                    }
                    $grouped[$msg] = ( isset( $grouped[$msg] ) ? $grouped[$msg] + 1 : 1 );
                }
                foreach ( $grouped as $msg => $n ) {
                    ?>
								<li><?php 
                    echo esc_html( $msg );
                    echo ( $n > 1 ? ' <em>(' . esc_html( sprintf( __( '×%d', 'automotive-feed-import' ), $n ) ) . ')</em>' : '' );
                    ?></li>
							<?php 
                }
                ?>
						</ul>
					</div>
				<?php 
            } elseif ( 'Success' === $last_run_status ) {
                ?>
					<div class="afi-log-clean">
						<strong>✓ <?php 
                esc_html_e( 'Your last sync ran cleanly — no problems found.', 'automotive-feed-import' );
                ?></strong>
					</div>
				<?php 
            }
            ?>

				<details class="afi-log-details">
					<summary><?php 
            esc_html_e( 'Show full technical log', 'automotive-feed-import' );
            ?></summary>
					<pre class="afi-log-pre"><?php 
            foreach ( $lines as $line ) {
                if ( strpos( $line, '] [ERROR]' ) !== false ) {
                    echo '<span class="afi-log-error">' . esc_html( $line ) . '</span>' . "\n";
                } else {
                    echo esc_html( $line ) . "\n";
                }
            }
            ?></pre>
				</details>
			</div>
		<?php 
        }

        /**
         * Test XML feed connection and show an admin notice
         */
        private function test_connection( $path = null ) {
            if ( null === $path ) {
                $path = $this->get_option( 'xml_file_path' );
            }
            if ( empty( $path ) ) {
                return array(
                    'type'    => 'error',
                    'message' => __( 'No feed link or file is saved yet.', 'automotive-feed-import' ),
                );
            }
            $is_csv = (bool) preg_match( '/\\.csv(\\?.*)?$/i', $path );
            // Remote URL (http/https)
            if ( preg_match( '#^https?://#i', $path ) ) {
                $response = wp_remote_get( $path, array(
                    'timeout' => 15,
                ) );
                if ( is_wp_error( $response ) ) {
                    /* translators: %s: error message from the server. */
                    return array(
                        'type'    => 'error',
                        'message' => sprintf( __( 'We could not reach your feed link: %s.', 'automotive-feed-import' ), $response->get_error_message() ),
                    );
                }
                $code = wp_remote_retrieve_response_code( $response );
                if ( $code < 200 || $code >= 300 ) {
                    /* translators: %d: HTTP response code. */
                    return array(
                        'type'    => 'error',
                        'message' => sprintf( __( 'Your feed link responded with an error (code %d).', 'automotive-feed-import' ), intval( $code ) ),
                    );
                }
                $body = wp_remote_retrieve_body( $response );
                if ( $body === '' ) {
                    return array(
                        'type'    => 'error',
                        'message' => __( 'Your feed link responded, but the file was empty.', 'automotive-feed-import' ),
                    );
                }
                if ( !$is_csv ) {
                    libxml_use_internal_errors( true );
                    $xml = simplexml_load_string( $body );
                    if ( $xml === false ) {
                        $errors = libxml_get_errors();
                        libxml_clear_errors();
                        $first = reset( $errors );
                        $err_msg = ( $first ? trim( $first->message ) : __( 'the file format was not recognized', 'automotive-feed-import' ) );
                        /* translators: %s: technical reason the file could not be read. */
                        return array(
                            'type'    => 'error',
                            'message' => sprintf( __( 'We reached your feed link, but could not read the file (%s). Please check it is a valid XML or CSV feed.', 'automotive-feed-import' ), $err_msg ),
                        );
                    }
                }
            } else {
                // Local file path
                if ( !file_exists( $path ) ) {
                    /* translators: %s: file path. */
                    return array(
                        'type'    => 'error',
                        'message' => sprintf( __( 'We could not find a feed file at this path: %s.', 'automotive-feed-import' ), $path ),
                    );
                }
                if ( !is_readable( $path ) ) {
                    /* translators: %s: file path. */
                    return array(
                        'type'    => 'error',
                        'message' => sprintf( __( 'The feed file exists but cannot be read. Please check permissions on: %s.', 'automotive-feed-import' ), $path ),
                    );
                }
                if ( !$is_csv ) {
                    libxml_use_internal_errors( true );
                    $xml = simplexml_load_file( $path );
                    if ( $xml === false ) {
                        $errors = libxml_get_errors();
                        libxml_clear_errors();
                        $first = reset( $errors );
                        $err_msg = ( $first ? trim( $first->message ) : __( 'the file format was not recognized', 'automotive-feed-import' ) );
                        /* translators: %s: technical reason the file could not be read. */
                        return array(
                            'type'    => 'error',
                            'message' => sprintf( __( 'We found your feed file, but could not read it (%s). Please check it is a valid XML or CSV feed.', 'automotive-feed-import' ), $err_msg ),
                        );
                    }
                }
            }
            if ( $is_csv ) {
                $message = __( 'Success! We connected and read your CSV feed.', 'automotive-feed-import' );
            } else {
                $message = __( 'Success! We connected and read your XML feed.', 'automotive-feed-import' );
            }
            return array(
                'type'    => 'success',
                'message' => $message,
            );
        }

        /**
         * AJAX: live-test a feed link/path before the user saves it.
         *
         * Admin-only. Mirrors the on-save validation so users get instant
         * "did it work?" feedback from the Feed & Sync tab.
         *
         * @since 2.6.0
         * @return void
         */
        public function ajax_test_feed() {
            check_ajax_referer( 'afi_test_feed', 'nonce' );
            if ( !current_user_can( 'manage_options' ) ) {
                wp_send_json_error( array(
                    'message' => __( 'You do not have permission to do that.', 'automotive-feed-import' ),
                ) );
            }
            $url = ( isset( $_POST['url'] ) ? trim( wp_unslash( $_POST['url'] ) ) : '' );
            if ( preg_match( '#^https?://#i', $url ) ) {
                $url = esc_url_raw( $url );
            } else {
                $url = sanitize_text_field( $url );
            }
            $result = $this->test_connection( $url );
            if ( isset( $result['type'] ) && 'success' === $result['type'] ) {
                wp_send_json_success( array(
                    'message' => $result['message'],
                ) );
            }
            wp_send_json_error( array(
                'message' => ( isset( $result['message'] ) ? $result['message'] : __( 'We could not check that link.', 'automotive-feed-import' ) ),
            ) );
        }

        /**
         * Handle admin actions
         */
        public function handle_admin_actions() {
            if ( !isset( $_GET['page'] ) || $_GET['page'] !== $this->plugin_slug ) {
                return;
            }
            if ( !isset( $_GET['action'] ) ) {
                // Check if we should redirect to field mapping after settings save
                if ( isset( $_GET['settings-updated'] ) && $_GET['settings-updated'] === 'true' ) {
                    // Auto-validate the feed URL on every save
                    $validation = $this->test_connection();
                    if ( $validation !== null ) {
                        set_transient( 'afi_feed_validation_' . get_current_user_id(), $validation, 60 );
                    }
                    $current_feed_url = $this->get_option( 'xml_file_path', '' );
                    $last_feed_url = $this->get_option( 'last_feed_url', '' );
                    // If feed URL changed, not empty, and validation succeeded, redirect to mapping tab
                    if ( !empty( $current_feed_url ) && $current_feed_url !== $last_feed_url && isset( $validation['type'] ) && $validation['type'] === 'success' ) {
                        $this->update_option( 'last_feed_url', $current_feed_url );
                        wp_redirect( $this->settings_page_url( '&tab=mapping&feed_changed=1' ) );
                        exit;
                    }
                    // Otherwise confirm the save right here. WordPress doesn't show a
                    // "saved" message on our custom top-level page automatically, so
                    // surface one so users always know their change took effect.
                    set_transient( 'afi_admin_notice', array(
                        'type'    => 'success',
                        'message' => '<strong>' . esc_html__( 'Your changes have been saved.', 'automotive-feed-import' ) . '</strong>',
                    ), 30 );
                }
                return;
            }
            $action = sanitize_text_field( $_GET['action'] );
            $tab = ( isset( $_GET['tab'] ) ? '&tab=' . sanitize_text_field( $_GET['tab'] ) : '' );
            // Clear log
            if ( $action === 'clear_log' && check_admin_referer( 'clear_log' ) ) {
                if ( file_exists( $this->log_file ) ) {
                    file_put_contents( $this->log_file, '' );
                    set_transient( 'afi_admin_notice', array(
                        'type'    => 'success',
                        'message' => __( 'Log cleared successfully.', 'automotive-feed-import' ),
                    ), 30 );
                }
                wp_redirect( $this->settings_page_url( $tab ) );
                exit;
            }
            // Run import
            if ( $action === 'run_import' && check_admin_referer( 'run_import' ) ) {
                $result = $this->update_data();
                if ( $result && $result['success'] ) {
                    set_transient( 'afi_admin_notice', array(
                        'type'    => 'success',
                        'message' => sprintf( 
                            /* translators: 1: number of vehicles created, 2: number of vehicles updated. */
                            __( '<strong>Import Successful!</strong> %1$d vehicles created, %2$d updated.', 'automotive-feed-import' ),
                            (int) $result['created'],
                            (int) $result['updated']
                         ),
                    ), 30 );
                } else {
                    set_transient( 'afi_admin_notice', array(
                        'type'    => 'error',
                        'message' => __( '<strong>Import Failed!</strong> Check the log below for details.', 'automotive-feed-import' ),
                    ), 30 );
                }
                wp_redirect( $this->settings_page_url( $tab ) );
                exit;
            }
            // Download sample XML
            if ( $action === 'download_sample' && check_admin_referer( 'download_sample' ) ) {
                $sample_file = plugin_dir_path( __FILE__ ) . 'sample-inventory.xml';
                if ( file_exists( $sample_file ) ) {
                    header( 'Content-Type: application/xml' );
                    header( 'Content-Disposition: attachment; filename="sample-vehicle-feed.xml"' );
                    header( 'Content-Length: ' . filesize( $sample_file ) );
                    readfile( $sample_file );
                    exit;
                }
            }
            // Download sample CSV
            if ( $action === 'download_sample_csv' && check_admin_referer( 'download_sample_csv' ) ) {
                $sample_file = plugin_dir_path( __FILE__ ) . 'sample-inventory-demo.csv';
                if ( file_exists( $sample_file ) ) {
                    header( 'Content-Type: text/csv' );
                    header( 'Content-Disposition: attachment; filename="sample-vehicle-feed.csv"' );
                    header( 'Content-Length: ' . filesize( $sample_file ) );
                    readfile( $sample_file );
                    exit;
                }
            }
        }

        /**
         * Handle field mapping save
         */
        public function handle_field_mapping_save() {
            // Verify nonce
            if ( !isset( $_POST['afi_mapping_nonce'] ) || !wp_verify_nonce( $_POST['afi_mapping_nonce'], 'afi_save_field_mappings' ) ) {
                wp_die( esc_html__( 'Security check failed', 'automotive-feed-import' ) );
            }
            // Check user permissions
            if ( !current_user_can( 'manage_options' ) ) {
                wp_die( esc_html__( 'Unauthorized access', 'automotive-feed-import' ) );
            }
            // Check if reset was requested
            if ( isset( $_POST['afi_reset_mappings'] ) && $_POST['afi_reset_mappings'] === '1' ) {
                // Reset only the current target CPT's mappings; mappings stored
                // for other targets stay intact so switching CPTs doesn't blow
                // away unrelated configurations.
                $all = $this->get_all_field_mappings();
                $target = $this->get_target_post_type();
                unset($all[$target]);
                $this->update_option( 'field_mappings', $all );
                set_transient( 'afi_admin_notice', array(
                    'type'    => 'success',
                    'message' => __( 'Field mappings reset to defaults successfully.', 'automotive-feed-import' ),
                ), 30 );
                wp_redirect( $this->settings_page_url( '&tab=mapping' ) );
                exit;
            }
            // Process mappings
            $mappings = array();
            if ( isset( $_POST['afi_mapping'] ) && is_array( $_POST['afi_mapping'] ) ) {
                foreach ( $_POST['afi_mapping'] as $source_field => $config ) {
                    $source_field = sanitize_text_field( $source_field );
                    $target_field = ( isset( $config['target'] ) ? sanitize_text_field( $config['target'] ) : $source_field );
                    // If the user chose "Enter a custom field name…", use the
                    // text they typed instead of the placeholder option value.
                    if ( $target_field === '__afi_custom__' ) {
                        $custom = ( isset( $config['custom_target'] ) ? sanitize_text_field( $config['custom_target'] ) : '' );
                        $target_field = ( '' !== $custom ? $custom : $source_field );
                    }
                    $enabled = isset( $config['enabled'] ) && $config['enabled'] === '1';
                    $mappings[$source_field] = array(
                        'enabled' => $enabled,
                        'target'  => $target_field,
                    );
                }
            }
            // Apply the prominent unique-ID picker: whichever feed field the user
            // chose there becomes the stock_number source (enabled), and any other
            // field that was previously the unique ID is released. This keeps the
            // single hard requirement correct no matter what the table rows say.
            if ( isset( $_POST['afi_unique_id_field'] ) ) {
                $uid_field = sanitize_text_field( wp_unslash( $_POST['afi_unique_id_field'] ) );
                if ( '' !== $uid_field && isset( $mappings[$uid_field] ) ) {
                    foreach ( $mappings as $src => $cfg ) {
                        if ( 'stock_number' === $cfg['target'] && $src !== $uid_field ) {
                            // Release a stale unique-ID mapping back to its own name.
                            $mappings[$src]['target'] = $src;
                        }
                    }
                    $mappings[$uid_field]['target'] = 'stock_number';
                    $mappings[$uid_field]['enabled'] = true;
                }
            }
            // Save mappings
            $this->save_field_mappings( $mappings );
            set_transient( 'afi_admin_notice', array(
                'type'    => 'success',
                'message' => __( 'Field mappings saved successfully. Import will now use these mappings.', 'automotive-feed-import' ),
            ), 30 );
            wp_redirect( $this->settings_page_url( '&tab=mapping' ) );
            exit;
        }

        /**
         * Render feedback banner (dashboard only, after first import)
         */
        public function render_feedback_banner() {
            // Only show if an import has actually been completed successfully
            $import_count = get_option( 'afi_total_imports', 0 );
            if ( $import_count < 1 ) {
                return;
            }
            // Only show on dashboard
            $screen = get_current_screen();
            if ( !$screen || $screen->id !== 'dashboard' ) {
                return;
            }
            // Check if already dismissed
            $dismissed = get_user_meta( get_current_user_id(), 'afi_feedback_dismissed', true );
            if ( $dismissed ) {
                return;
            }
            // Show at most once per 7 days until dismissed (no constant nagging).
            if ( get_transient( 'afi_feedback_gate' ) ) {
                return;
            }
            set_transient( 'afi_feedback_gate', 1, 7 * DAY_IN_SECONDS );
            ?>
			<div class="notice notice-success is-dismissible" data-dismiss-type="feedback" style="border-left-color: #ffb900;">
				<p style="font-size: 15px;">🚗 <strong><?php 
            esc_html_e( 'Is your inventory looking good?', 'automotive-feed-import' );
            ?></strong>
					<?php 
            esc_html_e( 'If Automotive Inventory Importer saved you time today, would you mind leaving a quick review? It helps us keep the plugin free!', 'automotive-feed-import' );
            ?>
					<a href="https://wordpress.org/support/plugin/automotive-feed-import/reviews/#new-post" target="_blank" class="button button-secondary" style="margin-left: 10px;"><?php 
            esc_html_e( 'Leave a Review ★★★★★', 'automotive-feed-import' );
            ?></a>
				</p>
			</div>
		<?php 
        }

        /**
         * Render survey banner (dashboard only, after first import)
         */
        public function render_survey_banner() {
            // Only show if an import has actually been completed successfully
            $import_count = get_option( 'afi_total_imports', 0 );
            if ( $import_count < 1 ) {
                return;
            }
            // Only show on dashboard
            $screen = get_current_screen();
            if ( !$screen || $screen->id !== 'dashboard' ) {
                return;
            }
            // Check if already dismissed
            $dismissed = get_user_meta( get_current_user_id(), 'afi_survey_dismissed', true );
            if ( $dismissed ) {
                return;
            }
            // One ask at a time: hold the survey until the review prompt has been
            // dealt with, so the dealer never sees two upsell banners at once.
            $feedback_dismissed = get_user_meta( get_current_user_id(), 'afi_feedback_dismissed', true );
            if ( !$feedback_dismissed ) {
                return;
            }
            // Show at most once per 7 days until dismissed.
            if ( get_transient( 'afi_survey_gate' ) ) {
                return;
            }
            set_transient( 'afi_survey_gate', 1, 7 * DAY_IN_SECONDS );
            ?>
			<div class="notice notice-info is-dismissible" data-dismiss-type="survey" style="border-left-color: #00a0d2;">
				<p style="font-size: 15px;">📋 <strong><?php 
            esc_html_e( 'Making your vehicle imports easier?', 'automotive-feed-import' );
            ?></strong>
					<?php 
            esc_html_e( 'Help shape the future of Automotive Inventory Importer! Share your experience in our quick 2-minute survey. Your insights help us build features you actually need.', 'automotive-feed-import' );
            ?>
					<a href="<?php 
            echo esc_url( $this->survey_url );
            ?>" target="_blank" class="button button-primary" style="margin-left: 10px;"><?php 
            esc_html_e( 'Take Survey', 'automotive-feed-import' );
            ?></a>
				</p>
			</div>
		<?php 
        }

        /**
         * Canonical Pro feature blurbs for the in-dashboard upsell.
         *
         * Single source of truth so the wording stays consistent across the
         * upsell notice and any point-of-use feature gates. Each entry has a
         * short 'label' (for the notice's feature list), a fuller 'gate'
         * sentence (for a feature gate at the point a user tries to use it),
         * and an 'available' flag — false for features that are on the Pro
         * roadmap but not shipped yet, so the copy never promises something
         * Pro can't deliver today.
         *
         * @since 2.6.0
         * @return array<string, array{label:string, gate:string, available:bool}>
         */
        private function pro_feature_blurbs() {
            return array(
                'schema'         => array(
                    'label'     => __( 'vehicle schema markup for Google SEO', 'automotive-feed-import' ),
                    'gate'      => __( 'Vehicle schema markup helps your listings appear as rich results in Google. Available in Pro.', 'automotive-feed-import' ),
                    'available' => true,
                ),
                'filters'        => array(
                    'label'     => __( 'advanced search filters with price sliders', 'automotive-feed-import' ),
                    'gate'      => __( 'Price sliders and multi-select filters are available in Pro. Help your buyers find the right car faster.', 'automotive-feed-import' ),
                    'available' => true,
                ),
                'leads'          => array(
                    'label'     => __( 'lead capture forms', 'automotive-feed-import' ),
                    'gate'      => __( 'Capture inquiries directly from each vehicle page. Lead capture forms are available in Pro.', 'automotive-feed-import' ),
                    'available' => true,
                ),
                'bulk_edit'      => array(
                    'label'     => __( 'bulk edit (coming soon)', 'automotive-feed-import' ),
                    'gate'      => __( 'Bulk edit lets you update prices, status, or location across many vehicles at once. Coming soon to Pro.', 'automotive-feed-import' ),
                    'available' => false,
                ),
                'multi_location' => array(
                    'label'     => __( 'multi-location support (coming soon)', 'automotive-feed-import' ),
                    'gate'      => __( 'Multi-location support gives you separate archive pages, per-location contact info, and location-based filtering. Coming soon to Pro.', 'automotive-feed-import' ),
                    'available' => false,
                ),
            );
        }

        /**
         * In-dashboard Pro upsell notice.
         *
         * Shown to free-plan administrators after a few days of real use — at
         * least one successful import, and three days since that first import —
         * so it lands once the dealer has seen the plugin work, not on day one.
         * Dismissible, and capped to once a week until dismissed.
         *
         * Waits behind the review and survey banners so the dealer never sees
         * two banners at once (the same one-ask-at-a-time rule used above).
         *
         * @since 2.6.0
         * @return void
         */
        public function render_pro_upsell_notice() {
            // Nothing to upsell to Pro users; only ever shown to admins.
            if ( !function_exists( 'afi_fs' ) || afi_fs()->can_use_premium_code() ) {
                return;
            }
            if ( !current_user_can( 'manage_options' ) ) {
                return;
            }
            // Dashboard only, matching the review/survey banners.
            $screen = get_current_screen();
            if ( !$screen || 'dashboard' !== $screen->id ) {
                return;
            }
            // Need a real track record: at least one import, and three days
            // since the first one.
            $first_import = (int) get_option( 'afi_first_import_time', 0 );
            if ( $first_import < 1 || time() - $first_import < 3 * DAY_IN_SECONDS ) {
                return;
            }
            // One ask at a time: hold this until the review and survey banners
            // have both been dealt with.
            $user_id = get_current_user_id();
            if ( !get_user_meta( $user_id, 'afi_feedback_dismissed', true ) ) {
                return;
            }
            if ( !get_user_meta( $user_id, 'afi_survey_dismissed', true ) ) {
                return;
            }
            // Permanently dismissed by this user?
            if ( get_user_meta( $user_id, 'afi_pro_upsell_dismissed', true ) ) {
                return;
            }
            // At most once per 7 days until dismissed.
            if ( get_transient( 'afi_pro_upsell_gate' ) ) {
                return;
            }
            $url = $this->pro_upgrade_url();
            if ( '' === $url ) {
                return;
            }
            set_transient( 'afi_pro_upsell_gate', 1, 7 * DAY_IN_SECONDS );
            // Build the feature list from the single source of truth, then add
            // priority support (always part of Pro).
            $features = array();
            foreach ( $this->pro_feature_blurbs() as $blurb ) {
                $features[] = $blurb['label'];
            }
            $features[] = __( 'priority support', 'automotive-feed-import' );
            $feature_list = implode( ', ', $features );
            // Lead with the dealer's own usage when we have it.
            $count = $this->count_imported_posts();
            $intro = '';
            if ( $count > 0 ) {
                $intro = sprintf( 
                    /* translators: %s: number of vehicles imported so far. */
                    _n(
                        'You\'ve imported %s vehicle so far!',
                        'You\'ve imported %s vehicles so far!',
                        $count,
                        'automotive-feed-import'
                    ),
                    number_format_i18n( $count )
                 ) . ' ';
            }
            ?>
			<div class="notice notice-info is-dismissible" data-dismiss-type="pro_upsell" style="border-left-color: #f0a500;">
				<p style="font-size: 15px; margin-bottom: 4px;">⭐ <strong><?php 
            esc_html_e( 'Enjoying Automotive Inventory Importer?', 'automotive-feed-import' );
            ?></strong></p>
				<p style="font-size: 14px;">
					<?php 
            echo esc_html( $intro );
            printf( 
                /* translators: %s: comma-separated list of Pro features. */
                esc_html__( 'The Pro version adds %s.', 'automotive-feed-import' ),
                esc_html( $feature_list )
             );
            ?>
					<a href="<?php 
            echo esc_url( $url );
            ?>" target="_blank" class="button button-primary" style="margin-left: 10px;"><?php 
            esc_html_e( 'Learn About Pro →', 'automotive-feed-import' );
            ?></a>
				</p>
			</div>
			<?php 
        }

        /**
         * Show a dismissible admin notice when the free-plan 25-vehicle cap was hit
         * during the most recent sync. Surfaces once per capped sync run, then
         * clears until the next import that hits the cap again.
         *
         * @since 2.6.0
         * @return void
         */
        public function display_cap_notice() {
            if ( $this->is_pro_active() ) {
                return;
            }
            if ( !current_user_can( 'manage_options' ) ) {
                return;
            }
            $cap_data = get_transient( 'afi_cap_notice' );
            if ( !$cap_data ) {
                return;
            }
            // Consume the data transient and arm the 7-day rate-limit gate so
            // subsequent capped syncs cannot re-queue the notice for 7 days.
            delete_transient( 'afi_cap_notice' );
            set_transient( 'afi_cap_notice_shown', 1, 7 * DAY_IN_SECONDS );
            $upgrade_url = $this->pro_upgrade_url();
            $synced = ( isset( $cap_data['synced'] ) ? (int) $cap_data['synced'] : 0 );
            $total = ( isset( $cap_data['total'] ) ? (int) $cap_data['total'] : 0 );
            $skipped = ( isset( $cap_data['skipped'] ) ? (int) $cap_data['skipped'] : 0 );
            echo '<div class="notice notice-warning is-dismissible"><p>';
            printf(
                /* translators: 1: number of vehicles synced, 2: total vehicles in feed, 3: number of vehicles not imported */
                esc_html__( 'Synced %1$s of %2$s vehicles from your feed. %3$s more aren\'t being imported — the free version is limited to 25 vehicles.', 'automotive-feed-import' ),
                '<strong>' . esc_html( number_format_i18n( $synced ) ) . '</strong>',
                '<strong>' . esc_html( number_format_i18n( $total ) ) . '</strong>',
                '<strong>' . esc_html( number_format_i18n( $skipped ) ) . '</strong>'
            );
            if ( '' !== $upgrade_url ) {
                echo ' <a href="' . esc_url( $upgrade_url ) . '" target="_blank" rel="noopener noreferrer"><strong>' . esc_html__( 'Upgrade to Pro to sync your full inventory →', 'automotive-feed-import' ) . '</strong></a>';
            }
            echo '</p></div>';
        }

        /**
         * Handle AJAX banner dismissal
         */
        public function dismiss_banner() {
            check_ajax_referer( 'afi_dismiss_banner', 'nonce' );
            $type = sanitize_text_field( $_POST['type'] );
            $user_id = get_current_user_id();
            if ( $type === 'feedback' ) {
                update_user_meta( $user_id, 'afi_feedback_dismissed', true );
            } elseif ( $type === 'survey' ) {
                update_user_meta( $user_id, 'afi_survey_dismissed', true );
            } elseif ( $type === 'activation' ) {
                update_user_meta( $user_id, 'afi_activation_notice_dismissed', true );
                delete_transient( 'afi_activation_notice' );
            } elseif ( $type === 'pro_upsell' ) {
                update_user_meta( $user_id, 'afi_pro_upsell_dismissed', true );
            }
            wp_send_json_success();
        }

        /**
         * Handle AJAX file browser
         */
        public function ajax_browse_files() {
            check_ajax_referer( 'afi_browse_files', 'nonce' );
            if ( !current_user_can( 'manage_options' ) ) {
                wp_send_json_error( __( 'Insufficient permissions', 'automotive-feed-import' ) );
            }
            $path = ( isset( $_POST['path'] ) ? sanitize_text_field( $_POST['path'] ) : ABSPATH );
            // Security: ensure path is within ABSPATH
            $real_path = realpath( $path );
            $real_abspath = realpath( ABSPATH );
            if ( $real_path === false || strpos( $real_path, $real_abspath ) !== 0 ) {
                $path = ABSPATH;
                $real_path = $real_abspath;
            }
            $html = '<div class="afi-current-path">' . esc_html__( 'Current:', 'automotive-feed-import' ) . ' ' . esc_html( $real_path ) . '</div>';
            if ( !is_readable( $real_path ) ) {
                wp_send_json_error( __( 'Directory not readable', 'automotive-feed-import' ) );
            }
            $items = scandir( $real_path );
            $html .= '<ul class="afi-file-list">';
            // Add parent directory link if not at root
            if ( $real_path !== $real_abspath ) {
                $parent = dirname( $real_path );
                $html .= '<li><a href="#" class="afi-folder-item" data-path="' . esc_attr( $parent ) . '">📁 ..</a></li>';
            }
            // List directories first, then files
            $dirs = [];
            $files = [];
            foreach ( $items as $item ) {
                if ( $item === '.' || $item === '..' ) {
                    continue;
                }
                $item_path = $real_path . DIRECTORY_SEPARATOR . $item;
                if ( is_dir( $item_path ) ) {
                    $dirs[] = array(
                        'name' => $item,
                        'path' => $item_path,
                    );
                } else {
                    $ext = strtolower( pathinfo( $item, PATHINFO_EXTENSION ) );
                    if ( 'xml' === $ext || 'csv' === $ext ) {
                        $files[] = array(
                            'name' => $item,
                            'path' => $item_path,
                        );
                    }
                }
            }
            // Sort and display
            sort( $dirs );
            sort( $files );
            foreach ( $dirs as $dir ) {
                $html .= '<li><a href="#" class="afi-folder-item" data-path="' . esc_attr( $dir['path'] ) . '">📁 ' . esc_html( $dir['name'] ) . '</a></li>';
            }
            foreach ( $files as $file ) {
                $html .= '<li><a href="#" class="afi-file-item" data-path="' . esc_attr( $file['path'] ) . '">📄 ' . esc_html( $file['name'] ) . '</a></li>';
            }
            if ( empty( $dirs ) && empty( $files ) ) {
                $html .= '<li style="color: #666; font-style: italic;">' . esc_html__( 'No XML or CSV files or folders found', 'automotive-feed-import' ) . '</li>';
            }
            $html .= '</ul>';
            wp_send_json_success( array(
                'html' => $html,
            ) );
        }

        /**
         * Build the full single-vehicle detail HTML: icon sprite + scoped .aii
         * page wrapper + the (theme-overridable) detail partial + the Pro lightbox.
         * Called by single-vehicle-page.php.
         *
         * @since 2.5.0
         * @param int $post_id Vehicle post ID.
         * @return string
         */
        public function get_single_vehicle_html( $post_id ) {
            $detail = $this->get_vehicle_detail_data( $post_id );
            ob_start();
            include $this->afi_locate_template( 'single-vehicle.php' );
            $single_html = ob_get_clean();
            // The Pro lightbox (if active) renders its own overlay on wp_footer
            // and binds to the .aii-gallery markup — nothing to append here.
            return $this->get_aii_icon_sprite() . '<div class="aii" data-theme="' . esc_attr( $this->get_aii_color_scheme() ) . '"><div class="aii-archive-page">' . $single_html . '</div></div>';
        }

        /**
         * Gallery image set for a vehicle (full/large/thumb URLs + alt), with the
         * configured placeholder excluded and a featured-image fallback. Public so
         * the Pro lightbox can build its full image list from the same source.
         *
         * @since 2.6.0
         * @param int $post_id Vehicle post ID.
         * @return array<int,array{full:string,large:string,thumb:string,alt:string}>
         */
        public function get_vehicle_gallery( $post_id ) {
            $placeholder_id = (int) $this->get_option( 'placeholder_attachment_id' );
            $att_ids = get_children( array(
                'post_parent'    => $post_id,
                'post_type'      => 'attachment',
                'post_mime_type' => 'image',
                'orderby'        => 'menu_order',
                'order'          => 'ASC',
                'numberposts'    => -1,
                'fields'         => 'ids',
            ) );
            if ( $placeholder_id ) {
                $k = array_search( $placeholder_id, $att_ids, true );
                if ( false !== $k ) {
                    unset($att_ids[$k]);
                }
            }
            $att_ids = array_values( $att_ids );
            if ( empty( $att_ids ) ) {
                $thumb_id = get_post_thumbnail_id( $post_id );
                if ( $thumb_id && (!$placeholder_id || (int) $thumb_id !== $placeholder_id) ) {
                    $att_ids = array((int) $thumb_id);
                }
            }
            $title = $this->normalize_title_display( get_the_title( $post_id ) );
            $gallery = array();
            foreach ( $att_ids as $att_id ) {
                $alt = get_post_meta( $att_id, '_wp_attachment_image_alt', true );
                $gallery[] = array(
                    'full'  => wp_get_attachment_image_url( $att_id, 'full' ),
                    'large' => wp_get_attachment_image_url( $att_id, 'large' ),
                    'thumb' => wp_get_attachment_image_url( $att_id, 'medium' ),
                    'alt'   => ( $alt ? $alt : $title ),
                );
            }
            return $gallery;
        }

        /**
         * Colour scheme for the .aii root.
         *
         * Defaults to 'light' so the plugin stays readable on the typical (light)
         * dealer site even when the visitor's OS prefers dark — the shipped CSS
         * otherwise auto-switches to dark via prefers-color-scheme, which would
         * put light text on a light host page. Dealers can opt into 'dark' (force)
         * or 'auto' (follow the visitor's OS) via the color_scheme option.
         *
         * @since 2.5.0
         * @return string One of 'light', 'dark', 'auto'.
         */
        public function get_aii_color_scheme() {
            $scheme = $this->get_option( 'color_scheme', 'light' );
            $scheme = ( is_string( $scheme ) ? strtolower( trim( $scheme ) ) : 'light' );
            return ( in_array( $scheme, array('light', 'dark', 'auto'), true ) ? $scheme : 'light' );
        }

        /**
         * Read the active GET filter values for the archive search bar.
         *
         * @since 2.5.0
         * @return array{search:string,make:string,year:string,condition:string,price_min:int,price_max:int}
         */
        private function get_active_filters() {
            // phpcs:disable WordPress.Security.NonceVerification.Recommended -- public read-only GET search form.
            return array(
                'search'    => ( isset( $_GET['afi_search'] ) ? sanitize_text_field( wp_unslash( $_GET['afi_search'] ) ) : '' ),
                'make'      => ( isset( $_GET['afi_make'] ) ? sanitize_text_field( wp_unslash( $_GET['afi_make'] ) ) : '' ),
                'year'      => ( isset( $_GET['afi_year'] ) ? sanitize_text_field( wp_unslash( $_GET['afi_year'] ) ) : '' ),
                'condition' => ( isset( $_GET['afi_condition'] ) ? sanitize_text_field( wp_unslash( $_GET['afi_condition'] ) ) : '' ),
                'price_min' => ( isset( $_GET['afi_price_min'] ) ? absint( $_GET['afi_price_min'] ) : 0 ),
                'price_max' => ( isset( $_GET['afi_price_max'] ) ? absint( $_GET['afi_price_max'] ) : 0 ),
            );
            // phpcs:enable WordPress.Security.NonceVerification.Recommended
        }

        /**
         * Render the free archive search/filter bar (.aii-searchbar) plus the
         * active-filter chip row. Standard GET form — reuses the existing
         * afi_* query params handled by filter_vehicle_query()/_search().
         *
         * Returns '' when there is no inventory to filter and no active filters.
         *
         * @since 2.5.0
         * @return string
         */
        public function render_vehicle_searchbar() {
            $opts = $this->get_filter_options();
            $f = $this->get_active_filters();
            $has_filters = '' !== $f['search'] || '' !== $f['make'] || '' !== $f['year'] || '' !== $f['condition'] || $f['price_min'] || $f['price_max'];
            $has_options = !empty( $opts['makes'] ) || !empty( $opts['years'] ) || !empty( $opts['conditions'] );
            if ( !$has_options && !$has_filters ) {
                return '';
            }
            $archive = get_post_type_archive_link( 'vehicles' );
            $currency = $this->get_option( 'currency_symbol', '$' );
            ob_start();
            ?>
			<div class="aii-archive__filters">
			<form class="aii-searchbar aii-searchbar--free" method="get" action="<?php 
            echo esc_url( $archive );
            ?>" role="search">
				<div class="aii-field">
					<label class="aii-label" for="afi-q"><?php 
            esc_html_e( 'Keyword', 'automotive-feed-import' );
            ?></label>
					<div class="aii-input-icon">
						<svg class="aii-input-icon__ic" aria-hidden="true" focusable="false"><use href="#i-search"></use></svg>
						<input class="aii-input" type="search" id="afi-q" name="afi_search" value="<?php 
            echo esc_attr( $f['search'] );
            ?>" placeholder="<?php 
            esc_attr_e( 'Make, model, or stock #', 'automotive-feed-import' );
            ?>">
					</div>
				</div>
				<div class="aii-field">
					<label class="aii-label" for="afi-make"><?php 
            esc_html_e( 'Make', 'automotive-feed-import' );
            ?></label>
					<select class="aii-select" id="afi-make" name="afi_make">
						<option value=""><?php 
            esc_html_e( 'Any make', 'automotive-feed-import' );
            ?></option>
						<?php 
            foreach ( $opts['makes'] as $opt ) {
                ?>
							<option value="<?php 
                echo esc_attr( $opt );
                ?>" <?php 
                selected( 0 === strcasecmp( $f['make'], $opt ) );
                ?>><?php 
                echo esc_html( $this->normalize_title_display( $opt ) );
                ?></option>
						<?php 
            }
            ?>
					</select>
				</div>
				<div class="aii-field">
					<label class="aii-label" for="afi-year"><?php 
            esc_html_e( 'Year', 'automotive-feed-import' );
            ?></label>
					<select class="aii-select" id="afi-year" name="afi_year">
						<option value=""><?php 
            esc_html_e( 'Any year', 'automotive-feed-import' );
            ?></option>
						<?php 
            foreach ( $opts['years'] as $opt ) {
                ?>
							<option value="<?php 
                echo esc_attr( $opt );
                ?>" <?php 
                selected( $f['year'], $opt );
                ?>><?php 
                echo esc_html( $opt );
                ?></option>
						<?php 
            }
            ?>
					</select>
				</div>
				<div class="aii-field">
					<label class="aii-label" for="afi-cond"><?php 
            esc_html_e( 'Condition', 'automotive-feed-import' );
            ?></label>
					<select class="aii-select" id="afi-cond" name="afi_condition">
						<option value=""><?php 
            esc_html_e( 'Any', 'automotive-feed-import' );
            ?></option>
						<?php 
            foreach ( $opts['conditions'] as $opt ) {
                ?>
							<option value="<?php 
                echo esc_attr( $opt );
                ?>" <?php 
                selected( 0 === strcasecmp( $f['condition'], $opt ) );
                ?>><?php 
                echo esc_html( $this->normalize_title_display( $opt ) );
                ?></option>
						<?php 
            }
            ?>
					</select>
				</div>
				<div class="aii-field">
					<label class="aii-label"><?php 
            esc_html_e( 'Price', 'automotive-feed-import' );
            ?></label>
					<div class="aii-searchbar__price">
						<input class="aii-input" type="number" name="afi_price_min" min="0" value="<?php 
            echo ( $f['price_min'] ? esc_attr( $f['price_min'] ) : '' );
            ?>" placeholder="<?php 
            esc_attr_e( 'Min', 'automotive-feed-import' );
            ?>" aria-label="<?php 
            esc_attr_e( 'Minimum price', 'automotive-feed-import' );
            ?>">
						<span class="aii-searchbar__price-sep" aria-hidden="true">&ndash;</span>
						<input class="aii-input" type="number" name="afi_price_max" min="0" value="<?php 
            echo ( $f['price_max'] ? esc_attr( $f['price_max'] ) : '' );
            ?>" placeholder="<?php 
            esc_attr_e( 'Max', 'automotive-feed-import' );
            ?>" aria-label="<?php 
            esc_attr_e( 'Maximum price', 'automotive-feed-import' );
            ?>">
					</div>
				</div>
				<button type="submit" class="aii-btn aii-btn--primary aii-searchbar__submit"><?php 
            esc_html_e( 'Search', 'automotive-feed-import' );
            ?></button>
			</form>
			<?php 
            if ( $has_filters ) {
                // Build the current URL from the active params so each chip's ×
                // links to the same view minus that one filter.
                $current = array_filter( array(
                    'afi_search'    => $f['search'],
                    'afi_make'      => $f['make'],
                    'afi_year'      => $f['year'],
                    'afi_condition' => $f['condition'],
                    'afi_price_min' => ( $f['price_min'] ? $f['price_min'] : '' ),
                    'afi_price_max' => ( $f['price_max'] ? $f['price_max'] : '' ),
                ), static function ( $v ) {
                    return '' !== $v && 0 !== $v;
                } );
                $current_url = add_query_arg( $current, $archive );
                $chips = array();
                if ( '' !== $f['search'] ) {
                    $chips[] = array(
                        'label'  => '“' . $f['search'] . '”',
                        'remove' => array('afi_search'),
                    );
                }
                if ( '' !== $f['make'] ) {
                    $chips[] = array(
                        'label'  => $this->normalize_title_display( $f['make'] ),
                        'remove' => array('afi_make'),
                    );
                }
                if ( '' !== $f['year'] ) {
                    $chips[] = array(
                        'label'  => $f['year'],
                        'remove' => array('afi_year'),
                    );
                }
                if ( '' !== $f['condition'] ) {
                    $chips[] = array(
                        'label'  => $this->normalize_title_display( $f['condition'] ),
                        'remove' => array('afi_condition'),
                    );
                }
                if ( $f['price_min'] || $f['price_max'] ) {
                    if ( $f['price_min'] && $f['price_max'] ) {
                        $plabel = $currency . number_format( $f['price_min'] ) . '–' . $currency . number_format( $f['price_max'] );
                    } elseif ( $f['price_min'] ) {
                        /* translators: %s: minimum price. */
                        $plabel = sprintf( __( 'From %s', 'automotive-feed-import' ), $currency . number_format( $f['price_min'] ) );
                    } else {
                        /* translators: %s: maximum price. */
                        $plabel = sprintf( __( 'Up to %s', 'automotive-feed-import' ), $currency . number_format( $f['price_max'] ) );
                    }
                    $chips[] = array(
                        'label'  => $plabel,
                        'remove' => array('afi_price_min', 'afi_price_max'),
                    );
                }
                ?>
				<div class="aii-applied">
					<span class="aii-applied__label"><?php 
                esc_html_e( 'Active:', 'automotive-feed-import' );
                ?></span>
					<?php 
                foreach ( $chips as $chip ) {
                    ?>
						<span class="aii-chip"><?php 
                    echo esc_html( $chip['label'] );
                    ?> <a class="aii-chip__x" href="<?php 
                    echo esc_url( remove_query_arg( $chip['remove'], $current_url ) );
                    ?>" aria-label="<?php 
                    /* translators: %s: filter label. */
                    echo esc_attr( sprintf( __( 'Remove filter %s', 'automotive-feed-import' ), $chip['label'] ) );
                    ?>"><svg aria-hidden="true" focusable="false"><use href="#i-x"></use></svg></a></span>
					<?php 
                }
                ?>
					<a class="aii-chip aii-chip--clear" href="<?php 
                echo esc_url( $archive );
                ?>"><?php 
                esc_html_e( 'Clear all', 'automotive-feed-import' );
                ?></a>
				</div>
				<?php 
            }
            ?>
			</div>
			<?php 
            return ob_get_clean();
        }

        /**
         * Render the archive empty state (.aii-empty): a "no matches" variant when
         * filters are active, or an "inventory on its way" variant otherwise.
         *
         * @since 2.5.0
         * @return string
         */
        public function render_vehicle_empty_state() {
            $f = $this->get_active_filters();
            $has_filters = '' !== $f['search'] || '' !== $f['make'] || '' !== $f['year'] || '' !== $f['condition'] || $f['price_min'] || $f['price_max'];
            $archive = get_post_type_archive_link( 'vehicles' );
            ob_start();
            if ( $has_filters ) {
                ?>
				<div class="aii-empty">
					<div class="aii-empty__ic"><svg aria-hidden="true" focusable="false"><use href="#i-search"></use></svg></div>
					<div class="aii-empty__title"><?php 
                esc_html_e( 'No vehicles match your filters', 'automotive-feed-import' );
                ?></div>
					<p class="aii-empty__text"><?php 
                esc_html_e( 'Try widening your price range or removing a filter to see more vehicles.', 'automotive-feed-import' );
                ?></p>
					<div class="aii-empty__actions">
						<a class="aii-btn aii-btn--primary" href="<?php 
                echo esc_url( $archive );
                ?>"><?php 
                esc_html_e( 'Clear all filters', 'automotive-feed-import' );
                ?></a>
					</div>
				</div>
			<?php 
            } else {
                ?>
				<div class="aii-empty">
					<div class="aii-empty__ic"><svg aria-hidden="true" focusable="false"><use href="#i-car"></use></svg></div>
					<div class="aii-empty__title"><?php 
                esc_html_e( 'Inventory is on its way', 'automotive-feed-import' );
                ?></div>
					<p class="aii-empty__text"><?php 
                esc_html_e( 'No vehicles have been added yet. New inventory syncs automatically from your feed — check back shortly.', 'automotive-feed-import' );
                ?></p>
				</div>
				<?php 
            }
            return ob_get_clean();
        }

        /**
         * Freemius upgrade URL (best-effort across SDK versions).
         *
         * @return string
         */
        private function pro_upgrade_url() {
            if ( !function_exists( 'afi_fs' ) ) {
                return '';
            }
            $fs = afi_fs();
            if ( method_exists( $fs, 'get_upgrade_url' ) ) {
                return $fs->get_upgrade_url();
            }
            if ( method_exists( $fs, 'pricing_url' ) ) {
                return $fs->pricing_url();
            }
            return '';
        }

        /**
         * Pro upsell card for the archive.
         *
         * Ships in the free build (this is the upsell), but is shown ONLY to
         * logged-in admins on the free plan — never to public car shoppers, and
         * never when Pro is active. Uses the premium-amber Pro tokens so it reads
         * as an upsell, not a primary action.
         *
         * @since 2.6.0
         * @return string
         */
        public function render_pro_upsell() {
            if ( !function_exists( 'afi_fs' ) || afi_fs()->can_use_premium_code() ) {
                return '';
            }
            if ( !current_user_can( 'manage_options' ) ) {
                return '';
            }
            $url = $this->pro_upgrade_url();
            if ( '' === $url ) {
                return '';
            }
            ob_start();
            ?>
			<div class="aii-upsell">
				<div class="aii-upsell__main">
					<div class="aii-upsell__title"><span class="aii-badge aii-badge--pro"><?php 
            esc_html_e( 'Pro', 'automotive-feed-import' );
            ?></span> <?php 
            esc_html_e( 'Power up your vehicle pages', 'automotive-feed-import' );
            ?></div>
					<div class="aii-upsell__text"><?php 
            esc_html_e( 'Advanced filters with a price slider and instant results, a full-screen photo viewer, lead-capture forms, and rich search-engine markup. Only you (an administrator) can see this notice.', 'automotive-feed-import' );
            ?></div>
				</div>
				<a class="aii-btn aii-btn--pro aii-btn--lg" href="<?php 
            echo esc_url( $url );
            ?>"><?php 
            esc_html_e( 'Upgrade to Pro', 'automotive-feed-import' );
            ?></a>
			</div>
			<?php 
            return ob_get_clean();
        }

        /**
         * Render the archive body (filter UI + results).
         *
         * Free default = the GET search bar + card grid + pagination/empty state.
         * The 'afi_archive_body' filter lets the Pro advanced AJAX filter replace
         * the whole region with the sidebar layout — keeping Pro code out of the
         * free build entirely (no mixed-strip blocks here).
         *
         * @since 2.5.0
         * @return string
         */
        public function render_archive_body() {
            /**
             * Filter the archive body HTML. Return a non-null string to replace
             * the free default (used by the Pro advanced AJAX filter).
             *
             * @param string|null          $html Replacement HTML, or null for the free default.
             * @param AutomotiveFeedImport $afi  Plugin instance.
             */
            $pro = apply_filters( 'afi_archive_body', null, $this );
            if ( null !== $pro ) {
                return (string) $pro;
            }
            ob_start();
            echo $this->render_vehicle_searchbar();
            // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
            echo $this->render_pro_upsell();
            // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
            if ( have_posts() ) {
                echo '<div class="aii-grid">';
                while ( have_posts() ) {
                    the_post();
                    echo $this->get_vehicle_card_html( get_the_ID() );
                    // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
                }
                echo '</div>';
                the_posts_pagination( array(
                    'mid_size'  => 1,
                    'prev_text' => esc_html__( 'Previous', 'automotive-feed-import' ),
                    'next_text' => esc_html__( 'Next', 'automotive-feed-import' ),
                ) );
            } else {
                echo $this->render_vehicle_empty_state();
                // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
            }
            return ob_get_clean();
        }

        /**
         * Enqueue the portable frontend design-system stylesheet on vehicle views.
         *
         * Ships assets/css/aii-frontend.css (the .aii-scoped, token-driven design
         * system) as-is. Loaded only on the vehicle archive, taxonomy, and single
         * views so it never weighs down unrelated pages.
         *
         * @since 2.5.0
         * @return void
         */
        public function enqueue_aii_frontend_assets() {
            if ( !(is_post_type_archive( 'vehicles' ) || is_singular( 'vehicles' ) || is_tax()) ) {
                return;
            }
            wp_enqueue_style(
                'aii-frontend',
                plugins_url( 'assets/css/aii-frontend.css', __FILE__ ),
                array(),
                AFI_VERSION
            );
        }

        /**
         * Locate a plugin template, letting the active theme override it.
         *
         * Looks first in {theme}/automotive-feed-import/{name}, then falls back to
         * the plugin's own templates/ directory. Lets site owners customise the
         * markup without editing the plugin.
         *
         * @since 2.5.0
         * @param string $name Template file name, e.g. 'archive-vehicles.php'.
         * @return string Absolute path to the template to load.
         */
        public function afi_locate_template( $name ) {
            $theme = locate_template( 'automotive-feed-import/' . $name );
            if ( $theme ) {
                return $theme;
            }
            return plugin_dir_path( __FILE__ ) . 'templates/' . $name;
        }

        /**
         * Take over the vehicle archive and single views with the plugin's own
         * design-system templates.
         *
         * @since 2.5.0
         * @param string $template The template WordPress resolved.
         * @return string Template path to load.
         */
        public function load_vehicle_template( $template ) {
            if ( is_post_type_archive( 'vehicles' ) ) {
                return $this->afi_locate_template( 'archive-vehicles.php' );
            }
            if ( is_singular( 'vehicles' ) && is_main_query() ) {
                return $this->afi_locate_template( 'single-vehicle-page.php' );
            }
            return $template;
        }

        /**
         * Render a full design-system page around a body callback, preserving the
         * host theme's chrome on both classic and block / FSE themes.
         *
         * On block themes the header/footer template parts are rendered into
         * buffers BEFORE wp_head() so their block-support layout styles register
         * in time to be printed, then wrapped in semantic <header>/<footer> tags
         * inside .wp-site-blocks. On classic themes get_header()/get_footer() are
         * used. Either way the plugin owns the document body, so it can emit a
         * single <h1> (no duplicate theme post-title).
         *
         * @since 2.5.0
         * @param callable $body Callback that echoes the page body (inside <main>).
         * @return void
         */
        public function render_aii_page_shell( $body ) {
            $is_block = function_exists( 'wp_is_block_theme' ) && wp_is_block_theme();
            if ( !$is_block ) {
                get_header();
                call_user_func( $body );
                get_footer();
                return;
            }
            ob_start();
            block_template_part( 'header' );
            $header_html = ob_get_clean();
            ob_start();
            block_template_part( 'footer' );
            $footer_html = ob_get_clean();
            ?>
<!DOCTYPE html>
<html <?php 
            language_attributes();
            ?>>
<head>
	<meta charset="<?php 
            bloginfo( 'charset' );
            ?>">
	<meta name="viewport" content="width=device-width, initial-scale=1">
	<?php 
            wp_head();
            ?>
</head>
<body <?php 
            body_class();
            ?>>
<?php 
            wp_body_open();
            ?>
<div class="wp-site-blocks">
	<?php 
            if ( '' !== trim( $header_html ) ) {
                ?>
		<header class="wp-block-template-part"><?php 
                echo $header_html;
                // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- core block output.
                ?></header>
	<?php 
            }
            ?>

	<main class="wp-block-group">
		<?php 
            call_user_func( $body );
            ?>
	</main>

	<?php 
            if ( '' !== trim( $footer_html ) ) {
                ?>
		<footer class="wp-block-template-part"><?php 
                echo $footer_html;
                // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- core block output.
                ?></footer>
	<?php 
            }
            ?>
</div>
<?php 
            wp_footer();
            ?>
</body>
</html>
			<?php 
        }

        /**
         * Map a raw feed "condition / designation" value to a card badge.
         *
         * @since 2.5.0
         * @param string $designation Raw designation value.
         * @return array|null array{class:string,label:string}, or null when empty.
         */
        private function map_condition( $designation ) {
            $designation = trim( (string) $designation );
            if ( '' === $designation ) {
                return null;
            }
            $lower = strtolower( $designation );
            if ( false !== strpos( $lower, 'cert' ) || false !== strpos( $lower, 'cpo' ) ) {
                return array(
                    'class' => 'certified',
                    'label' => __( 'Certified', 'automotive-feed-import' ),
                );
            }
            if ( false !== strpos( $lower, 'new' ) ) {
                return array(
                    'class' => 'new',
                    'label' => __( 'New', 'automotive-feed-import' ),
                );
            }
            return array(
                'class' => 'used',
                'label' => $this->format_display_value( $designation ),
            );
        }

        /**
         * Title-case a multi-word display title while preserving known acronyms
         * and alphanumeric model codes.
         *
         * Unlike format_display_value() (which bails on any digit), this walks the
         * title word by word: known acronyms (BMW, GMC, XLT, AWD, ...) stay
         * uppercase, words containing a digit are left untouched (model codes /
         * years such as "330i", "F-150", "RAV4", "2017"), and everything else is
         * title-cased (hyphenated parts handled individually).
         *
         * @since 2.5.0
         * @param string $text Raw title (often ALL CAPS from a feed).
         * @return string Normalized title.
         */
        public function normalize_title_display( $text ) {
            $text = trim( (string) $text );
            if ( '' === $text ) {
                return '';
            }
            static $acronyms = array(
                'BMW',
                'GMC',
                'RAM',
                'GT',
                'GTI',
                'GTS',
                'SUV',
                'AWD',
                'FWD',
                'RWD',
                '2WD',
                '4X4',
                'XLT',
                'XLE',
                'SXT',
                'SLT',
                'SLE',
                'LTZ',
                'LX',
                'EX',
                'SE',
                'SEL',
                'LE',
                'LS',
                'LT',
                'RS',
                'ST',
                'SR',
                'SV',
                'SL',
                'TDI',
                'TRD',
                'EV',
                'GLS',
                'GLE',
                'GLA',
                'GLC',
                'ABS'
            );
            $word_caser = static function ( $part ) {
                if ( '' === $part ) {
                    return $part;
                }
                if ( function_exists( 'mb_convert_case' ) ) {
                    return mb_convert_case( mb_strtolower( $part, 'UTF-8' ), MB_CASE_TITLE, 'UTF-8' );
                }
                return ucfirst( strtolower( $part ) );
            };
            $words = preg_split( '/\\s+/', $text );
            foreach ( $words as &$word ) {
                if ( '' === $word ) {
                    continue;
                }
                // Leave alphanumeric codes (years, "330i", "F-150", "RAV4") alone.
                if ( preg_match( '/\\d/', $word ) ) {
                    continue;
                }
                $upper = strtoupper( $word );
                if ( in_array( $upper, $acronyms, true ) ) {
                    $word = $upper;
                    continue;
                }
                // Title-case, preserving hyphenated parts (e.g. "Grand-Touring").
                $word = implode( '-', array_map( $word_caser, explode( '-', $word ) ) );
            }
            unset($word);
            return implode( ' ', $words );
        }

        /**
         * Build the display-ready data array for one vehicle card.
         *
         * Every slot is omitted when its underlying field is empty, so a card
         * never renders blank rows or dead UI. Sources map to the importer's
         * canonical meta keys (see get_canonical_target_fields()).
         *
         * @since 2.5.0
         * @param int $post_id Vehicle post ID.
         * @return array
         */
        private function get_vehicle_card_data( $post_id ) {
            $m = get_post_custom( $post_id );
            $get = static function ( $key ) use($m) {
                return ( isset( $m[$key][0] ) ? trim( (string) $m[$key][0] ) : '' );
            };
            $currency = $this->get_option( 'currency_symbol', '$' );
            // Title: the post title — composed by the importer per the dealer's
            // configured title format and shown as the single-page heading — so
            // the card always matches the page it links to. Normalized to Title
            // Case (feeds often arrive ALL CAPS) with acronyms preserved.
            $title = $this->normalize_title_display( get_the_title( $post_id ) );
            if ( '' === $title ) {
                // Fallback for an untitled post: compose from core meta.
                $parts = array_filter( array($get( 'model_year' ), ( '' !== $get( 'manufacturer' ) ? $this->format_display_value( $get( 'manufacturer' ) ) : $this->format_display_value( $get( 'brand' ) ) ), $this->format_display_value( $get( 'model' ) )), static function ( $v ) {
                    return '' !== $v;
                } );
                $title = implode( ' ', $parts );
            }
            // Price + optional struck MSRP (only when MSRP exceeds the price).
            $price_raw = $get( 'special_web_price' );
            $price = ( '' !== $price_raw ? $currency . number_format( (float) $price_raw ) : '' );
            $msrp_raw = $get( 'factory_list' );
            $msrp = '';
            if ( '' !== $msrp_raw && '' !== $price_raw && (float) $msrp_raw > (float) $price_raw ) {
                $msrp = $currency . number_format( (float) $msrp_raw );
            }
            // Spec pills — only those we actually have a source for.
            $specs = array();
            $mileage = $get( 'mileage' );
            if ( '' !== $mileage ) {
                $specs[] = array(
                    'icon' => 'i-gauge',
                    'text' => number_format( (float) $mileage ) . ' ' . __( 'mi', 'automotive-feed-import' ),
                );
            }
            $body = ( '' !== $get( 'body_type' ) ? $get( 'body_type' ) : $get( 'type' ) );
            if ( '' !== $body ) {
                $specs[] = array(
                    'icon' => 'i-car',
                    'text' => $this->format_display_value( $body ),
                );
            }
            // Sold status: explicit status text or a recorded sold date.
            $is_sold = false !== stripos( $get( 'status' ), 'sold' ) || '' !== $get( 'sold_date' );
            // Media: real featured image, else fall back to the placeholder block.
            $thumbnail_id = get_post_thumbnail_id( $post_id );
            $placeholder_id = (int) $this->get_option( 'placeholder_attachment_id' );
            $has_real_image = $thumbnail_id && (!$placeholder_id || (int) $thumbnail_id !== $placeholder_id);
            $photo_img = '';
            if ( $has_real_image ) {
                // Bypass hide_vehicle_thumbnail() while we fetch our own card image.
                $this->rendering_vehicle_card = true;
                $photo_img = get_the_post_thumbnail( $post_id, 'large', array(
                    'loading' => 'lazy',
                    'alt'     => $title,
                ) );
                $this->rendering_vehicle_card = false;
            }
            // Photo-count chip — gallery images, excluding the placeholder.
            $photo_ids = get_children( array(
                'post_parent'    => $post_id,
                'post_type'      => 'attachment',
                'post_mime_type' => 'image',
                'fields'         => 'ids',
                'numberposts'    => -1,
            ) );
            if ( $placeholder_id ) {
                $k = array_search( $placeholder_id, $photo_ids, true );
                if ( false !== $k ) {
                    unset($photo_ids[$k]);
                }
            }
            return array(
                'permalink'   => get_permalink( $post_id ),
                'title'       => $title,
                'price'       => $price,
                'msrp'        => $msrp,
                'trim'        => ( '' !== $get( 'style' ) ? $this->format_display_value( $get( 'style' ) ) : '' ),
                'condition'   => $this->map_condition( $get( 'designation' ) ),
                'is_sold'     => $is_sold,
                'specs'       => $specs,
                'photo_img'   => $photo_img,
                'photo_count' => count( $photo_ids ),
                'location'    => ( '' !== $get( 'lot_location' ) ? $this->format_display_value( $get( 'lot_location' ) ) : '' ),
            );
        }

        /**
         * Render one vehicle card via the (theme-overridable) card partial.
         *
         * @since 2.5.0
         * @param int $post_id Vehicle post ID.
         * @return string Card HTML.
         */
        public function get_vehicle_card_html( $post_id ) {
            $card = $this->get_vehicle_card_data( $post_id );
            ob_start();
            include $this->afi_locate_template( 'card-vehicle.php' );
            return ob_get_clean();
        }

        /**
         * Build the display-ready data array for the single vehicle detail page.
         *
         * Mirrors get_vehicle_card_data(): every slot is pre-resolved and empties
         * are omitted. Markup is kept semantic (single <h1>, <dl> spec pairs) so
         * a Pro build can emit JSON-LD Vehicle schema from the same fields.
         *
         * @since 2.5.0
         * @param int $post_id Vehicle post ID.
         * @return array
         */
        private function get_vehicle_detail_data( $post_id ) {
            $m = get_post_custom( $post_id );
            $get = static function ( $key ) use($m) {
                return ( isset( $m[$key][0] ) ? trim( (string) $m[$key][0] ) : '' );
            };
            $currency = $this->get_option( 'currency_symbol', '$' );
            $title = $this->normalize_title_display( get_the_title( $post_id ) );
            if ( '' === $title ) {
                $title = get_the_title( $post_id );
            }
            // --- Gallery images (full/large/thumb/alt; placeholder excluded) ---
            $gallery = $this->get_vehicle_gallery( $post_id );
            // --- Badges ---
            $condition = $this->map_condition( $get( 'designation' ) );
            $location = ( '' !== $get( 'lot_location' ) ? $this->format_display_value( $get( 'lot_location' ) ) : '' );
            // --- Trim subtitle (style + colours) ---
            $trim_parts = array();
            if ( '' !== $get( 'style' ) ) {
                $trim_parts[] = $this->format_display_value( $get( 'style' ) );
            }
            if ( '' !== $get( 'exterior_color' ) ) {
                /* translators: %s: exterior colour. */
                $trim_parts[] = sprintf( __( '%s exterior', 'automotive-feed-import' ), $this->format_display_value( $get( 'exterior_color' ) ) );
            }
            if ( '' !== $get( 'interior_color' ) ) {
                /* translators: %s: interior colour. */
                $trim_parts[] = sprintf( __( '%s interior', 'automotive-feed-import' ), $this->format_display_value( $get( 'interior_color' ) ) );
            }
            $trim = implode( ' · ', $trim_parts );
            // --- Price block ---
            $price_raw = $get( 'special_web_price' );
            $price = ( '' !== $price_raw ? $currency . number_format( (float) $price_raw ) : '' );
            $msrp_raw = $get( 'factory_list' );
            $msrp = '';
            if ( '' !== $msrp_raw && '' !== $price_raw && (float) $msrp_raw > (float) $price_raw ) {
                $msrp = $currency . number_format( (float) $msrp_raw );
            }
            $note_parts = array();
            if ( '' !== $get( 'stock_number' ) ) {
                /* translators: %s: stock number. */
                $note_parts[] = sprintf( __( 'Stock #%s', 'automotive-feed-import' ), $get( 'stock_number' ) );
            }
            $note_parts[] = __( 'Plus applicable tax, title & fees', 'automotive-feed-import' );
            $note = implode( ' · ', $note_parts );
            // Free CTA: a mailto to the site contact, vehicle pre-filled in the subject.
            $dealer_email = sanitize_email( (string) get_option( 'admin_email' ) );
            $mailto = '';
            if ( '' !== $dealer_email ) {
                /* translators: %s: vehicle title. */
                $subject = sprintf( __( 'Inquiry: %s', 'automotive-feed-import' ), $title );
                if ( '' !== $get( 'stock_number' ) ) {
                    $subject .= ' (' . sprintf( __( 'Stock #%s', 'automotive-feed-import' ), $get( 'stock_number' ) ) . ')';
                }
                $mailto = 'mailto:' . $dealer_email . '?subject=' . rawurlencode( $subject );
            }
            // --- Key facts (up to 4 available, priority order) ---
            $keyfacts = array();
            if ( '' !== $get( 'mileage' ) ) {
                $keyfacts[] = array(
                    'icon' => 'i-gauge',
                    'v'    => number_format( (float) $get( 'mileage' ) ),
                    'k'    => __( 'Miles', 'automotive-feed-import' ),
                );
            }
            if ( '' !== $get( 'model_year' ) ) {
                $keyfacts[] = array(
                    'icon' => 'i-cal',
                    'v'    => $get( 'model_year' ),
                    'k'    => __( 'Year', 'automotive-feed-import' ),
                );
            }
            if ( null !== $condition ) {
                $keyfacts[] = array(
                    'icon' => 'i-shield',
                    'v'    => $condition['label'],
                    'k'    => __( 'Condition', 'automotive-feed-import' ),
                );
            }
            $body = ( '' !== $get( 'body_type' ) ? $get( 'body_type' ) : $get( 'type' ) );
            if ( '' !== $body ) {
                $keyfacts[] = array(
                    'icon' => 'i-car',
                    'v'    => $this->format_display_value( $body ),
                    'k'    => __( 'Body', 'automotive-feed-import' ),
                );
            }
            if ( count( $keyfacts ) < 4 && '' !== $get( 'exterior_color' ) ) {
                $keyfacts[] = array(
                    'icon' => 'i-palette',
                    'v'    => $this->format_display_value( $get( 'exterior_color' ) ),
                    'k'    => __( 'Color', 'automotive-feed-import' ),
                );
            }
            $keyfacts = array_slice( $keyfacts, 0, 4 );
            // --- Specs table (semantic <dl>; omit empty) ---
            $spec_map = array(
                'manufacturer'   => array(__( 'Make', 'automotive-feed-import' ), 'i-car', false),
                'model'          => array(__( 'Model', 'automotive-feed-import' ), 'i-car', false),
                'style'          => array(__( 'Trim', 'automotive-feed-import' ), 'i-info', false),
                'model_year'     => array(__( 'Year', 'automotive-feed-import' ), 'i-cal', false),
                'body_type'      => array(__( 'Body Style', 'automotive-feed-import' ), 'i-car', false),
                'type'           => array(__( 'Type', 'automotive-feed-import' ), 'i-car', false),
                'mileage'        => array(__( 'Mileage', 'automotive-feed-import' ), 'i-gauge', false),
                'designation'    => array(__( 'Condition', 'automotive-feed-import' ), 'i-shield', false),
                'exterior_color' => array(__( 'Exterior Color', 'automotive-feed-import' ), 'i-palette', false),
                'interior_color' => array(__( 'Interior Color', 'automotive-feed-import' ), 'i-palette', false),
                'length'         => array(__( 'Length', 'automotive-feed-import' ), 'i-info', false),
                'status'         => array(__( 'Status', 'automotive-feed-import' ), 'i-info', false),
                'vin'            => array(__( 'VIN', 'automotive-feed-import' ), 'i-hash', true),
                'stock_number'   => array(__( 'Stock #', 'automotive-feed-import' ), 'i-hash', true),
            );
            $specs = array();
            foreach ( $spec_map as $key => $def ) {
                $value = $get( $key );
                if ( '' === $value ) {
                    continue;
                }
                if ( 'mileage' === $key ) {
                    $display = number_format( (float) $value ) . ' ' . __( 'mi', 'automotive-feed-import' );
                } elseif ( $def[2] ) {
                    $display = $value;
                    // VIN / stock # — codes, leave as-is.
                } else {
                    $display = $this->format_display_value( $value );
                }
                $specs[] = array(
                    'label' => $def[0],
                    'icon'  => $def[1],
                    'value' => $display,
                    'mono'  => $def[2],
                );
            }
            // --- Description (real feed description only; omit if absent) ---
            $description = ( '' !== $get( 'description' ) ? wpautop( wp_kses_post( $get( 'description' ) ) ) : '' );
            return array(
                'permalink'   => get_permalink( $post_id ),
                'title'       => $title,
                'condition'   => $condition,
                'location'    => $location,
                'trim'        => $trim,
                'price'       => $price,
                'msrp'        => $msrp,
                'note'        => $note,
                'mailto'      => $mailto,
                'keyfacts'    => $keyfacts,
                'specs'       => $specs,
                'description' => $description,
                'gallery'     => $gallery,
            );
        }

        /**
         * Return the inline SVG symbol sprite used by the design-system markup.
         *
         * Guarded so the sprite is emitted only once per request, whether the
         * caller echoes it (archive template) or concatenates it into returned
         * content (single-page the_content filter). Icons are referenced via
         * <use href="#i-...">.
         *
         * @since 2.5.0
         * @return string Sprite markup, or '' if already emitted this request.
         */
        public function get_aii_icon_sprite() {
            static $printed = false;
            if ( $printed ) {
                return '';
            }
            $printed = true;
            return '<svg width="0" height="0" style="position:absolute;overflow:hidden" aria-hidden="true" focusable="false">' . '<symbol id="i-nophoto" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M2 2l20 20M21 15V7a2 2 0 0 0-2-2H9M3 7v10a2 2 0 0 0 2 2h12"/><path d="m6 17 4-3"/><circle cx="9" cy="10" r="1.4"/></symbol>' . '<symbol id="i-camera" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 4h-5L8 6H4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-4l-1.5-2Z"/><circle cx="12" cy="13" r="3.2"/></symbol>' . '<symbol id="i-gauge" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M12 14l3-3"/><path d="M3.5 18a9 9 0 1 1 17 0"/><circle cx="12" cy="14" r="1"/></symbol>' . '<symbol id="i-car" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 13l1.8-5A2 2 0 0 1 6.7 6.6h10.6a2 2 0 0 1 1.9 1.4L21 13M5 13h14v4a1 1 0 0 1-1 1h-1a1 1 0 0 1-1-1H8a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1v-4Z"/><circle cx="7.5" cy="15.5" r="1"/><circle cx="16.5" cy="15.5" r="1"/></symbol>' . '<symbol id="i-pin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M12 21s7-5.7 7-11a7 7 0 1 0-14 0c0 5.3 7 11 7 11Z"/><circle cx="12" cy="10" r="2.6"/></symbol>' . '<symbol id="i-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M13 6l6 6-6 6"/></symbol>' . '<symbol id="i-cal" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4.5" width="18" height="16" rx="2"/><path d="M3 9h18M8 2.5v4M16 2.5v4"/></symbol>' . '<symbol id="i-shield" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l7 3v5c0 4.5-3 8-7 10-4-2-7-5.5-7-10V6l7-3Z"/><path d="m9 12 2 2 4-4"/></symbol>' . '<symbol id="i-palette" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><circle cx="8.5" cy="10" r="1"/><circle cx="12" cy="8" r="1"/><circle cx="15.5" cy="10" r="1"/></symbol>' . '<symbol id="i-hash" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M9 4 7 20M17 4l-2 16M5 9h15M4 15h15"/></symbol>' . '<symbol id="i-gear" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M12 4v4M12 16v4M4 12h4M16 12h4"/><circle cx="12" cy="12" r="3.4"/></symbol>' . '<symbol id="i-fuel" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M5 21V5a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v16M3 21h14"/><path d="M15 9h2.5a2 2 0 0 1 2 2v5a1.5 1.5 0 0 0 3 0V8l-2.5-2.5"/><path d="M7 8h6"/></symbol>' . '<symbol id="i-drive" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><circle cx="12" cy="12" r="9"/><path d="M12 3v6M12 15v6M3 12h6M15 12h6"/></symbol>' . '<symbol id="i-engine" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M7 9V7h4v2h3l2-2h2v3h2v5h-2v3H9v-3l-2-2H4V9h3Z"/></symbol>' . '<symbol id="i-door" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M5 21V5a2 2 0 0 1 2-2h7l5 4v14M3 21h18"/><circle cx="15" cy="12" r="1"/></symbol>' . '<symbol id="i-info" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 11v5M12 7.5h.01"/></symbol>' . '<symbol id="i-mail" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="m3 7 9 6 9-6"/></symbol>' . '<symbol id="i-phone" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M5 4h3l1.5 4-2 1.5a11 11 0 0 0 5 5L19 12l3 1.5V17a2 2 0 0 1-2 2A16 16 0 0 1 4 6a2 2 0 0 1 1-2Z"/></symbol>' . '<symbol id="i-chevL" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 6l-6 6 6 6"/></symbol>' . '<symbol id="i-chevR" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 6l6 6-6 6"/></symbol>' . '<symbol id="i-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6l12 12M18 6 6 18"/></symbol>' . '<symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.2-3.2"/></symbol>' . '<symbol id="i-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.5l4.5 4.5L19 7"/></symbol>' . '<symbol id="i-alert" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 7.5v5M12 16h.01"/></symbol>' . '</svg>';
        }

        /**
         * Echo the design-system icon sprite (archive template helper).
         *
         * @since 2.5.0
         * @return void
         */
        public function render_aii_icon_sprite() {
            echo $this->get_aii_icon_sprite();
            // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static inline SVG.
        }

        /**
         * Get distinct makes, years, and conditions from published vehicles
         */
        public function get_filter_options() {
            global $wpdb;
            $base = "FROM {$wpdb->postmeta} pm\n\t\t         INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id\n\t\t         WHERE p.post_type = 'vehicles' AND p.post_status = 'publish'";
            $makes = $wpdb->get_col( "SELECT DISTINCT pm.meta_value {$base} AND pm.meta_key = 'manufacturer' AND pm.meta_value != '' ORDER BY pm.meta_value" );
            $years = $wpdb->get_col( "SELECT DISTINCT pm.meta_value {$base} AND pm.meta_key = 'model_year' AND pm.meta_value != '' ORDER BY pm.meta_value DESC" );
            $conditions = $wpdb->get_col( "SELECT DISTINCT pm.meta_value {$base} AND pm.meta_key = 'designation' AND pm.meta_value != '' ORDER BY pm.meta_value" );
            return compact( 'makes', 'years', 'conditions' );
        }

        /**
         * Modify the main vehicle archive query based on filter GET params
         */
        public function filter_vehicle_query( $query ) {
            if ( is_admin() || !$query->is_main_query() ) {
                return;
            }
            if ( !$query->is_post_type_archive( 'vehicles' ) && !$query->is_tax() ) {
                return;
            }
            $meta_query = [];
            if ( !empty( $_GET['afi_make'] ) ) {
                // phpcs:ignore WordPress.Security.NonceVerification.Recommended
                $meta_query[] = [
                    'key'     => 'manufacturer',
                    'value'   => sanitize_text_field( $_GET['afi_make'] ),
                    'compare' => '=',
                ];
                // phpcs:ignore WordPress.Security.NonceVerification.Recommended
            }
            if ( !empty( $_GET['afi_year'] ) ) {
                // phpcs:ignore WordPress.Security.NonceVerification.Recommended
                $meta_query[] = [
                    'key'     => 'model_year',
                    'value'   => sanitize_text_field( $_GET['afi_year'] ),
                    'compare' => '=',
                ];
                // phpcs:ignore WordPress.Security.NonceVerification.Recommended
            }
            if ( !empty( $_GET['afi_condition'] ) ) {
                // phpcs:ignore WordPress.Security.NonceVerification.Recommended
                $meta_query[] = [
                    'key'     => 'designation',
                    'value'   => sanitize_text_field( $_GET['afi_condition'] ),
                    'compare' => '=',
                ];
                // phpcs:ignore WordPress.Security.NonceVerification.Recommended
            }
            if ( !empty( $_GET['afi_price_min'] ) ) {
                // phpcs:ignore WordPress.Security.NonceVerification.Recommended
                $meta_query[] = [
                    'key'     => 'special_web_price',
                    'value'   => (float) $_GET['afi_price_min'],
                    'compare' => '>=',
                    'type'    => 'DECIMAL(10,2)',
                ];
                // phpcs:ignore WordPress.Security.NonceVerification.Recommended
            }
            if ( !empty( $_GET['afi_price_max'] ) ) {
                // phpcs:ignore WordPress.Security.NonceVerification.Recommended
                $meta_query[] = [
                    'key'     => 'special_web_price',
                    'value'   => (float) $_GET['afi_price_max'],
                    'compare' => '<=',
                    'type'    => 'DECIMAL(10,2)',
                ];
                // phpcs:ignore WordPress.Security.NonceVerification.Recommended
            }
            if ( !empty( $meta_query ) ) {
                $meta_query['relation'] = 'AND';
                $query->set( 'meta_query', $meta_query );
            }
        }

        /**
         * Add text search against post title and content for the vehicle archive
         */
        public function filter_vehicle_search( $where, $query ) {
            if ( is_admin() || !$query->is_main_query() ) {
                return $where;
            }
            if ( !$query->is_post_type_archive( 'vehicles' ) && !$query->is_tax() ) {
                return $where;
            }
            if ( !empty( $_GET['afi_search'] ) ) {
                // phpcs:ignore WordPress.Security.NonceVerification.Recommended
                global $wpdb;
                $term = '%' . $wpdb->esc_like( sanitize_text_field( $_GET['afi_search'] ) ) . '%';
                // phpcs:ignore WordPress.Security.NonceVerification.Recommended
                $where .= $wpdb->prepare( " AND ({$wpdb->posts}.post_title LIKE %s OR {$wpdb->posts}.post_content LIKE %s)", $term, $term );
                // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
            }
            return $where;
        }

        /**
         * Add Settings link to plugin action links
         */
        public function add_plugin_action_links( $links ) {
            $settings_link = '<a href="' . esc_url( $this->settings_page_url() ) . '">' . esc_html__( 'Settings', 'automotive-feed-import' ) . '</a>';
            array_unshift( $links, $settings_link );
            return $links;
        }

        /**
         * Enqueue admin scripts
         */
        /**
         * Append a subtle developer/brand credit to the admin footer, but only
         * on this plugin's own settings screen. Every other admin screen keeps
         * WordPress's default footer text.
         *
         * @since 2.5.x
         * @param string $text Existing admin footer text.
         * @return string
         */
        public function admin_footer_credit( $text ) {
            $screen = get_current_screen();
            if ( !$screen ) {
                return $text;
            }
            // Reuse the same page-detection logic as enqueue_admin_scripts():
            // the captured hook suffix, with the known fallback patterns.
            $is_plugin_page = $this->settings_hook && $screen->id === $this->settings_hook || $screen->id === 'toplevel_page_' . $this->plugin_slug || $screen->id === 'settings_page_' . $this->plugin_slug;
            if ( !$is_plugin_page ) {
                return $text;
            }
            $jawaid = sprintf( '<a href="%1$s" target="_blank" rel="noopener noreferrer">%2$s</a>', esc_url( 'https://jawaid.dev' ), esc_html__( 'Jawaid', 'automotive-feed-import' ) );
            $ibexoft = sprintf( '<a href="%1$s" target="_blank" rel="noopener noreferrer">%2$s</a>', esc_url( 'https://ibexoft.com' ), esc_html__( 'Ibexoft', 'automotive-feed-import' ) );
            // translators: 1: Jawaid link (developer), 2: Ibexoft link (brand).
            $credit = sprintf( esc_html__( 'Developed with ❤️ by %1$s · Powered by 🚀 %2$s', 'automotive-feed-import' ), $jawaid, $ibexoft );
            // Preserve WordPress's (and any other plugin's) footer text; append ours.
            if ( '' === trim( (string) $text ) ) {
                return $credit;
            }
            return $text . ' · ' . $credit;
        }

        public function enqueue_admin_scripts( $hook ) {
            // Our settings screen is a top-level menu page, whose hook suffix
            // WordPress derives at registration. Compare against the captured
            // value (falling back to known patterns) so assets load reliably.
            $is_settings = $this->settings_hook && $hook === $this->settings_hook || $hook === 'toplevel_page_' . $this->plugin_slug || $hook === 'settings_page_' . $this->plugin_slug;
            // The dashboard "At a Glance" widget reuses the status-panel styles,
            // so load just the stylesheet there too (no scripts needed).
            if ( 'index.php' === $hook ) {
                wp_enqueue_style(
                    'afi-admin',
                    plugins_url( 'assets/css/admin.css', __FILE__ ),
                    array(),
                    AFI_VERSION
                );
                return;
            }
            if ( !$is_settings ) {
                return;
            }
            wp_enqueue_style(
                'afi-admin',
                plugins_url( 'assets/css/admin.css', __FILE__ ),
                array(),
                AFI_VERSION
            );
            add_thickbox();
            wp_enqueue_script(
                'afi-admin',
                plugins_url( 'assets/js/admin.js', __FILE__ ),
                array('jquery'),
                AFI_VERSION,
                true
            );
            $active_tab = ( isset( $_GET['tab'] ) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : 'general' );
            wp_localize_script( 'afi-admin', 'afiAdmin', array(
                'abspath'      => ABSPATH,
                'browseNonce'  => wp_create_nonce( 'afi_browse_files' ),
                'testNonce'    => wp_create_nonce( 'afi_test_feed' ),
                'dismissNonce' => wp_create_nonce( 'afi_dismiss_banner' ),
                'sample'       => ( 'format' === $active_tab ? $this->get_sample_unit_for_preview() : new stdClass() ),
                'i18n'         => array(
                    'browseTitle'      => __( 'Browse Server Files', 'automotive-feed-import' ),
                    'feedTestBusy'     => __( 'Checking your feed link…', 'automotive-feed-import' ),
                    'feedTestEmpty'    => __( 'Enter a feed link or pick a file first.', 'automotive-feed-import' ),
                    'feedTestFail'     => __( 'We could not check that link. Please try again.', 'automotive-feed-import' ),
                    'syncConfirm'      => __( 'Run a fresh import now?', 'automotive-feed-import' ),
                    'syncOverlayTitle' => __( 'Syncing your inventory…', 'automotive-feed-import' ),
                    'syncOverlayBody'  => __( 'Please keep this window open and don’t navigate away. This can take a minute for large feeds.', 'automotive-feed-import' ),
                    'previewEmpty'     => __( 'Nothing yet — type a template above.', 'automotive-feed-import' ),
                    'expand'           => __( 'Expand', 'automotive-feed-import' ),
                    'collapse'         => __( 'Collapse', 'automotive-feed-import' ),
                ),
            ) );
        }

        /**
         * Build a lightweight sample row (field => value) from the first unit in
         * the configured feed, used to power the live token preview on the Page
         * Templates tab. Returns an empty object when no feed/sample is available.
         *
         * Parsing the whole feed just to read the first row is expensive for
         * large or remote feeds, and this runs on every Page Templates page load.
         * The lightweight sample is cached in a transient keyed by the feed path
         * so the feed is parsed at most once per hour rather than on every view.
         *
         * @since 2.6.0
         * @return array|stdClass
         */
        private function get_sample_unit_for_preview() {
            $feed_path = (string) $this->get_option( 'xml_file_path', '' );
            if ( '' === $feed_path ) {
                return new stdClass();
            }
            $cache_key = 'afi_preview_sample_' . md5( $feed_path );
            $cached = get_transient( $cache_key );
            if ( false !== $cached ) {
                return ( is_array( $cached ) && !empty( $cached ) ? $cached : new stdClass() );
            }
            $units = ( $this->is_csv_feed() ? $this->load_csv() : $this->load_xml() );
            if ( false === $units ) {
                // The feed failed to load (timeout, bad response, unreadable or
                // invalid file). Don't cache the failure — let the next page load
                // retry rather than blanking the preview for an hour.
                return new stdClass();
            }
            $clean = array();
            if ( is_array( $units ) && !empty( $units ) ) {
                $sample = reset( $units );
                if ( is_array( $sample ) ) {
                    foreach ( $sample as $key => $value ) {
                        if ( is_scalar( $value ) ) {
                            $clean[(string) $key] = (string) $value;
                        }
                    }
                }
            }
            // The feed loaded successfully; cache the result even when empty (a
            // valid feed whose first row has no usable fields) so we don't
            // re-parse it on every page load.
            set_transient( $cache_key, $clean, HOUR_IN_SECONDS );
            return ( empty( $clean ) ? new stdClass() : $clean );
        }

        /**
         * Suppress post date/time output on vehicle pages.
         * Hooked to get_the_date and get_the_time so themes that call
         * these functions directly return an empty string.
         */
        public function hide_vehicle_date( $date ) {
            if ( is_singular( 'vehicles' ) || is_post_type_archive( 'vehicles' ) || is_tax() ) {
                return '';
            }
            return $date;
        }

        /**
         * Remove the "Archives: Vehicles" heading on the vehicle listing page.
         */
        public function hide_vehicle_archive_title( $title ) {
            if ( is_post_type_archive( 'vehicles' ) || is_tax() ) {
                return '';
            }
            return $title;
        }

        /**
         * Suppress the "written by" author on vehicle pages.
         */
        public function hide_vehicle_author( $author ) {
            if ( is_singular( 'vehicles' ) || is_post_type_archive( 'vehicles' ) || is_tax() ) {
                return '';
            }
            return $author;
        }

        /**
         * Output a <style> block in <head> to hide the theme's byline on vehicle pages.
         * Using wp_head guarantees output regardless of which stylesheet handles are enqueued.
         */
        public function hide_vehicle_byline_style() {
            if ( !(is_singular( 'vehicles' ) || is_post_type_archive( 'vehicles' ) || is_tax()) ) {
                return;
            }
            echo '<style>
			/* AFI: hide theme author/category byline on vehicle pages */
			.entry-meta,
			.entry-footer,
			.byline,
			.posted-by,
			.author-byline,
			.post-author,
			.post-meta,
			.author-info,
			.cat-links,
			.posted-on,
			.entry-date,
			/* Block theme (Twenty Twenty-Two+) post meta blocks */
			.wp-block-post-author,
			.wp-block-post-author__name,
			.wp-block-post-author__bio,
			.wp-block-post-terms,
			.wp-block-post-date {
				display: none !important;
			}
		</style>' . "\n";
        }

        /**
         * Suppress the featured image that themes output above the_content on archive pages.
         * The plugin already outputs the image inside the card via the_content filter.
         */
        public function hide_vehicle_thumbnail( $html ) {
            // Allow our own card renderer to fetch the thumbnail unfiltered.
            if ( $this->rendering_vehicle_card ) {
                return $html;
            }
            if ( is_post_type_archive( 'vehicles' ) || is_tax() ) {
                return '';
            }
            return $html;
        }

    }

    // end of class
    /**
     * Inline-SVG icon reference for the design-system markup.
     *
     * Returns a <use> reference into the sprite printed by
     * AutomotiveFeedImport::render_aii_icon_sprite().
     *
     * @param string $id    Symbol id, e.g. 'i-arrow'.
     * @param string $class Optional CSS class for the <svg>.
     * @return string <svg><use> markup.
     */
    if ( !function_exists( 'afi_aii_icon' ) ) {
        function afi_aii_icon(  $id, $class = ''  ) {
            $class_attr = ( '' !== $class ? ' class="' . esc_attr( $class ) . '"' : '' );
            return '<svg' . $class_attr . ' aria-hidden="true" focusable="false"><use href="#' . esc_attr( $id ) . '"></use></svg>';
        }

    }
    /////////////////////////////////////////////////////////////////////
    // Load Pro presets if the /pro/ folder is present. Freemius strips
    // this folder from the free WordPress.org build via @fs_premium_only
    // annotations, so the file_exists guard simply no-ops in that case
    // and the preset registry stays empty (the UI then shows the upsell
    // prompt instead of "Apply preset" buttons).
    if ( file_exists( dirname( __FILE__ ) . '/pro/class-preset-base.php' ) ) {
        require_once dirname( __FILE__ ) . '/pro/class-preset-base.php';
        require_once dirname( __FILE__ ) . '/pro/class-preset-registry.php';
        foreach ( glob( dirname( __FILE__ ) . '/pro/presets/class-preset-*.php' ) as $preset_file ) {
            require_once $preset_file;
        }
    }
    $afi = new AutomotiveFeedImport();
    // Pro frontend features. The /pro/ folder is stripped from the free
    // WordPress.org build (so file_exists() is false there); the license gate
    // ensures each feature only activates for paying users.
    if ( function_exists( 'afi_fs' ) && afi_fs()->can_use_premium_code() ) {
        // Advanced AJAX search & filter.
        if ( file_exists( dirname( __FILE__ ) . '/pro/class-advanced-filter.php' ) ) {
            require_once dirname( __FILE__ ) . '/pro/class-advanced-filter.php';
            if ( class_exists( 'AFI_Advanced_Filter' ) ) {
                new AFI_Advanced_Filter($afi);
            }
        }
        // JSON-LD Vehicle schema on single pages.
        if ( file_exists( dirname( __FILE__ ) . '/pro/class-vehicle-schema.php' ) ) {
            require_once dirname( __FILE__ ) . '/pro/class-vehicle-schema.php';
            if ( class_exists( 'AFI_Vehicle_Schema' ) ) {
                new AFI_Vehicle_Schema($afi);
            }
        }
        // Lead-capture form on single pages.
        if ( file_exists( dirname( __FILE__ ) . '/pro/class-lead-form.php' ) ) {
            require_once dirname( __FILE__ ) . '/pro/class-lead-form.php';
            if ( class_exists( 'AFI_Lead_Form' ) ) {
                new AFI_Lead_Form($afi);
            }
        }
        // Full-screen gallery lightbox on single pages.
        if ( file_exists( dirname( __FILE__ ) . '/pro/class-lightbox.php' ) ) {
            require_once dirname( __FILE__ ) . '/pro/class-lightbox.php';
            if ( class_exists( 'AFI_Lightbox' ) ) {
                new AFI_Lightbox($afi);
            }
        }
    }
    /* The activation hook is executed when the plugin is activated. */
    register_activation_hook( __FILE__, array($afi, 'init') );
    /* The deactivation hook is executed when the plugin is deactivated */
    register_deactivation_hook( __FILE__, array($afi, 'uninit') );
    // Actions
    // Load translations on init. WordPress 6.7+ emits a _doing_it_wrong
    // notice if a text domain is loaded before the init hook, so this must
    // not move earlier (e.g. to plugins_loaded).
    add_action( 'init', array($afi, 'load_textdomain') );
    add_action( 'init', array($afi, 'register_vehicles_post_type') );
    add_action( 'admin_init', array($afi, 'add_custom_box'), 1 );
    add_action( 'admin_init', array($afi, 'register_settings') );
    add_action( 'admin_init', array($afi, 'handle_admin_actions') );
    add_action( 'admin_menu', array($afi, 'add_settings_page') );
    add_action( 'admin_notices', array($afi, 'render_foreign_cpt_disclaimer_notice') );
    add_action( 'admin_notices', array($afi, 'display_activation_notice') );
    add_action( 'admin_notices', array($afi, 'display_transient_notices') );
    add_action( 'admin_notices', array($afi, 'display_cap_notice') );
    add_action( 'admin_post_afi_accept_foreign_cpt_disclaimer', array($afi, 'handle_foreign_cpt_disclaimer_accept') );
    // Auto-mapping is Pro-only; its handler is stripped from the free build,
    // so only register the action when premium code is active.
    if ( function_exists( 'afi_fs' ) && afi_fs()->can_use_premium_code() ) {
        add_action( 'admin_post_afi_auto_map', array($afi, 'handle_auto_map__premium_only') );
    }
    add_action( 'admin_notices', array($afi, 'render_feedback_banner') );
    add_action( 'admin_notices', array($afi, 'render_survey_banner') );
    add_action( 'admin_notices', array($afi, 'render_pro_upsell_notice') );
    add_action( 'update_xml_event', array($afi, 'update_data') );
    // Bind to the generic save_post so the meta box save handler works
    // regardless of which CPT the user has configured as the import target.
    // The handler itself is guarded by $_POST['afi_field'] + nonce, so it
    // no-ops on any post type that did not render our meta box.
    add_action( 'save_post', array($afi, 'save_vehicle_meta') );
    add_action( 'wp_ajax_afi_dismiss_banner', array($afi, 'dismiss_banner') );
    add_action( 'wp_ajax_afi_browse_files', array($afi, 'ajax_browse_files') );
    add_action( 'wp_ajax_afi_test_feed', array($afi, 'ajax_test_feed') );
    add_action( 'admin_enqueue_scripts', array($afi, 'enqueue_admin_scripts') );
    add_filter( 'admin_footer_text', array($afi, 'admin_footer_credit') );
    add_action( 'wp_dashboard_setup', array($afi, 'register_dashboard_widget') );
    add_action( 'admin_post_afi_save_field_mappings', array($afi, 'handle_field_mapping_save') );
    add_action( 'wp_enqueue_scripts', array($afi, 'enqueue_aii_frontend_assets') );
    add_filter( 'template_include', array($afi, 'load_vehicle_template') );
    add_action( 'pre_get_posts', array($afi, 'filter_vehicle_query') );
    // Filters
    add_filter( 'cron_schedules', array($afi, 'define_interval') );
    add_action( 'update_option_automotive-feed-import_import_frequency', array($afi, 'reschedule') );
    add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), array($afi, 'add_plugin_action_links') );
    add_filter(
        'posts_where',
        array($afi, 'filter_vehicle_search'),
        10,
        2
    );
    add_filter( 'get_the_date', array($afi, 'hide_vehicle_date') );
    add_filter( 'get_the_time', array($afi, 'hide_vehicle_date') );
    add_filter( 'the_author', array($afi, 'hide_vehicle_author') );
    add_filter( 'get_the_archive_title', array($afi, 'hide_vehicle_archive_title') );
    add_filter( 'post_thumbnail_html', array($afi, 'hide_vehicle_thumbnail') );
    add_action( 'wp_head', array($afi, 'hide_vehicle_byline_style') );
}