<?php
// phpcs:disable PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound, WordPress.WP.AlternativeFunctions.file_system_operations_fopen, WordPress.WP.AlternativeFunctions.file_system_operations_fclose, WordPress.WP.AlternativeFunctions.file_system_operations_fread, WordPress.WP.AlternativeFunctions.file_system_operations_fwrite, WordPress.WP.AlternativeFunctions.file_system_operations_readfile, WordPress.WP.AlternativeFunctions.unlink_unlink, Squiz.PHP.DiscouragedFunctions.Discouraged
// Reason: All $wpdb interpolations use $wpdb->prefix-derived table names or keys from hardcoded internal arrays (never request input). Direct queries hit plugin-owned tables that have no WP REST/cache layer. Variables flagged as non-prefixed are local helpers / loop indices, not globals. CSV import/export streams use fopen()/fread()/fwrite()/fclose() because WP_Filesystem_API has no equivalent streaming interface and the operations are admin-only. set_time_limit()/ini_set() are needed inside long-running CSV exports + activation cleanup; they're capability-gated and short-circuit when running under PHP-FPM.
/*
Plugin Name: AI eShop Optimizer
Plugin URI: https://rooster.systems/get-aieo/
Description: Adaptive productivity, conversion & retention for eshops and content sites. Anthropic/Voyage AI: messaging, recommendations, attribution, loyalty, merchandising.
Version: 8.5
Requires at least: 7.0
Tested up to: 7.0
Requires PHP: 7.4
Author: Oxford Metadata Ltd
Author URI: https://oxfordmetadata.co.uk
License: GPL3
Tags: ai-chat, content-recommendations, semantic-search, conversational-ai, claude

*/

// Ensure the plugin is only loaded within WordPress
if (!defined('ABSPATH')) {
    exit;
}

/**
 * Centralised debug logger. Calls error_log() only when WP_DEBUG and
 * WP_DEBUG_LOG are both enabled, so production sites stay quiet by default.
 * Used in place of bare error_log() throughout the plugin.
 *
 * @param string|mixed $message Message or value to log.
 */
if (!function_exists('aieo_debug_log')) {
    function aieo_debug_log($message) {
        if (defined('WP_DEBUG') && WP_DEBUG && defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) {
            // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Single gated entry point for plugin diagnostics.
            error_log(is_scalar($message) ? (string) $message : wp_json_encode($message));
        }
    }
}

/**
 * Prefer the plugin's BUNDLED translations over an incomplete wp.org language
 * pack. WordPress loads .mo/.l10n.php from wp-content/languages/plugins/ (the
 * translate.wordpress.org GlotPress pack) BEFORE the plugin's own languages/
 * dir and returns early — so a stub pack (e.g. the 10-string Greek pack) would
 * shadow our full bundled file and leave most of the admin UI in English.
 * Redirect any global-dir load for our domain to the bundled file whenever we
 * ship one for that locale; other locales keep using their wp.org packs.
 */
if (!function_exists('aieo_prefer_bundled_textdomain')) {
    function aieo_prefer_bundled_textdomain($mofile, $domain) {
        if ('ai-eshop-optimizer' !== $domain) {
            return $mofile;
        }
        // Only override when WP is about to load from the global languages dir.
        if (0 === strpos($mofile, WP_LANG_DIR)) {
            $bundled = plugin_dir_path(__FILE__) . 'languages/' . basename($mofile);
            if (is_readable($bundled)) {
                return $bundled;
            }
        }
        return $mofile;
    }
}
add_filter('load_textdomain_mofile', 'aieo_prefer_bundled_textdomain', 10, 2);

/**
 * Stop WordPress from (re)installing the incomplete translate.wordpress.org
 * language pack for this plugin. We are listed on wp.org, so WP's translations
 * updater keeps fetching the community GlotPress pack — which currently has
 * only ~6 Greek strings — into wp-content/languages/plugins/ on every
 * translations cron and plugin update. That stub then shows up as a second,
 * "stale" Greek file in Loco; although aieo_prefer_bundled_textdomain() above
 * already redirects loading back to our COMPLETE bundled catalogue, the stub
 * keeps reappearing, confuses translators and clutters the languages dir.
 * Dropping our slug from the update transient means the stub is never fetched.
 */
if (!function_exists('aieo_block_wporg_translation_pack')) {
    function aieo_block_wporg_translation_pack($transient) {
        if (isset($transient->translations) && is_array($transient->translations)) {
            foreach ($transient->translations as $i => $t) {
                if (isset($t['slug']) && 'ai-eshop-optimizer' === $t['slug']) {
                    unset($transient->translations[$i]);
                }
            }
            $transient->translations = array_values($transient->translations);
        }
        return $transient;
    }
}
add_filter('pre_set_site_transient_update_plugins', 'aieo_block_wporg_translation_pack');
add_filter('site_transient_update_plugins',        'aieo_block_wporg_translation_pack');

// Load text domain IMMEDIATELY for AJAX requests (before any classes load)
// This ensures translations are available during whitelisted AJAX requests
if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
    // phpcs:ignore PluginCheck.CodeAnalysis.DiscouragedFunctions.load_plugin_textdomainFound -- Fallback for non-WordPress.org hosted translations
    load_plugin_textdomain(
        'ai-eshop-optimizer',
        false,
        dirname( plugin_basename( __FILE__ ) ) . '/languages'
    );
}

if (!defined('AIEO_VERSION')) { define('AIEO_VERSION', '8.5'); }
// Schema/data-migration counter, independent of the plugin version. Bumped when
// aieo_run_db_migrations() gains a new step. v1=#33 sessions index trim,
// v2=#34 reco reverse-ref table + backfill, v3=#35 analytics atomic-swap procs.
if (!defined('AIEO_DB_VERSION')) { define('AIEO_DB_VERSION', 3); }
// Guarded: the runtime plugin loads FIRST (WP sorts active_plugins and
// "-runtime" sorts before "/"), and it pre-seeds AIEO_FILE/AIEO_DIR/AIEO_URI
// (pointing at THIS plugin's path, same values) so its bundled classes work
// even when the core fatals. Unguarded re-defines here warned on every
// request under PHP 8.5 and become fatal on PHP 9.
if (!defined('AIEO_FILE')) { define('AIEO_FILE', __FILE__); }
if (!defined('AIEO_BASE')) { define('AIEO_BASE', trailingslashit(plugin_basename(AIEO_FILE))); }
if (!defined('AIEO_DIR'))  { define('AIEO_DIR', plugin_dir_path(__FILE__)); }
if (!defined('AIEO_URI'))  { define('AIEO_URI', plugin_dir_url(__FILE__)); }
define('AIEO_orders_upload_endpoint', 'https://uploads.eshop-optimizer.com/wp-json/aieo/v1/order_uploads');

// Capability required to access the plugin admin pages
// Using 'edit_pages' allows editor, shop_manager, and admin roles to access
// (edit_pages is shared by Editor, Shop Manager, and Administrator)
define('AIEO_CAPABILITY', 'edit_pages');

// Declare WooCommerce feature compatibility if WooCommerce is active.
// `variation_gallery` + `wc-visual-attribute` are WC 10.9+ experimental features;
// AIEO writes its data into WC's canonical keys so they Just Work when toggled on.
add_action('before_woocommerce_init', function () {
    if (class_exists(\Automattic\WooCommerce\Utilities\FeaturesUtil::class)) {
        \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility('custom_order_tables', __FILE__, true);
        \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility('variation_gallery', __FILE__, true);
        \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility('wc-visual-attribute', __FILE__, true);
    }
});


// Load the plugin class
require_once plugin_dir_path(__FILE__) . '/includes/class-eshop-optimizer.php';

// SQL layer split (Phase-1 perf, 2026-06-13). sprocs.php (~6.3k lines of
// CREATE PROCEDURE installers, which itself pulls in dmm-schema.php ~3.5k
// lines + brand-vitals-schema.php) used to load HERE on every request,
// including anonymous storefront page views that never touch it. Now:
//   • sql-runtime-helpers.php — the handful of helpers runtime code calls
//     (charset/collate, aieo_dbdelta, table probes, role columns) — always;
//   • the full DDL layer — only where installers/exports actually run:
//     admin, AJAX, cron, CLI, plus activation/deactivation/upgrade via
//     aieo_load_sql_layer() (called inside those entry points).
require_once plugin_dir_path(__FILE__) . '/includes/sql/sql-runtime-helpers.php';

/**
 * Load the full SQL/DDL layer (sprocs + dmm-schema + brand-vitals schema)
 * on demand. Idempotent. MUST run before any aieo_create_* / aieo_drop_sp_*
 * call — the activation / deactivation / upgrade entry points all invoke it
 * as their first statement, so every call path is covered regardless of
 * request context.
 */
function aieo_load_sql_layer() {
    require_once plugin_dir_path(__FILE__) . '/includes/sql/sprocs.php';
}

if (is_admin() || wp_doing_ajax() || wp_doing_cron() || (defined('WP_CLI') && WP_CLI)) {
    aieo_load_sql_layer();
}

// Load Session Tracker (Core analytics functionality).
// AIEO Session Tracker has a canonical copy at
// ai-eshop-optimizer-runtime/includes/class-aieo-session-tracker.php
// (moved 2026-05-16 — see runtime plugin's STATUS.md). When the
// autoloader mu-plugin AND the runtime plugin are present, this
// require_once is a no-op (the autoloader resolved the class first).
// When the admin plugin is installed STANDALONE — typical for a fresh
// wp.org download where neither the autoloader nor the runtime ship —
// the require_once loads our own fallback copy. This guard is the
// stability boundary that lets the admin plugin work on any host
// without external prerequisites.
if ( ! class_exists( 'AIEO_Session_Tracker' ) ) {
    require_once plugin_dir_path( __FILE__ ) . '/includes/class-aieo-session-tracker.php';
}

// Load Session Schema Migrator (Database migration tools)
require_once plugin_dir_path(__FILE__) . '/includes/class-session-schema-migrator.php';

// Load Recommendations Cache (Multi-tier cache for product recommendations)
require_once plugin_dir_path(__FILE__) . '/includes/class-recommendations-cache.php';

// Serve built *.min.css / *.min.js via a global URL-rewrite filter (no
// per-enqueue edits). No-ops when SCRIPT_DEBUG is on or a .min file is absent.
require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-asset-min.php';
AIEO_Asset_Min::register();

// Central wp_set_script_translations() wiring for all JS handles that use wp.i18n.
require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-script-translations.php';
require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-admin-color.php';

// Per-device (desktop/mobile) value controls for EVERY AIEO block — adds the
// editor "Responsive (per device)" panel + render-time scoped CSS via global
// filters keyed on aieo/* (covers all 19 blocks + any future one automatically).
require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-responsive-controls.php';
AIEO_Responsive_Controls::register();

// wpDataTables report-page enhancements (glass tooltips + origin tagging),
// ported from the standalone om-wpdt-enhancements plugin. register() only
// adds hooks — the front-end enqueue is page-scoped (aieo_wpdt_enhance_pages
// option, managed on Tools → Product/Orders analytics) and the origin-tag
// fires solely on the wdt_save_table_frontend AJAX action.
require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-wpdt-enhancements.php';
AIEO_WPDT_Enhancements::register();

// Load Wishlist (favorites storage + AJAX endpoints; phase-1 of the
// migration from om-product-page-essentials).
// AIEO_Wishlist has a canonical copy in the runtime plugin
// (ai-eshop-optimizer-runtime/includes/class-aieo-wishlist.php — moved
// 2026-05-06). With autoloader+runtime present, the require_once below
// is a no-op. Without them, the admin plugin loads its own fallback
// copy so wishlist still works on a standalone install.
if ( ! class_exists( 'AIEO_Wishlist' ) ) {
    require_once plugin_dir_path( __FILE__ ) . '/includes/class-aieo-wishlist.php';
}
// Phase-2 PPE compatibility shim — proxies om_ppe_wishlist_toggle →
// aieo_wishlist_toggle so themes / widgets / external callers that
// reference the legacy endpoint keep working after PPE's wishlist
// module is disabled.
require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-wishlist-compat.php';

// Star Ratings (AIEO_Ratings) — canonical copy lives in the runtime
// plugin (ai-eshop-optimizer-runtime/includes/class-aieo-ratings.php —
// moved 2026-05-06). With autoloader+runtime present, the require_once
// below is a no-op. Without them, the admin plugin loads its own
// fallback copy so ratings still work on a standalone install. The
// REST sibling class is admin-only and always loads from here.
if ( ! class_exists( 'AIEO_Ratings' ) ) {
    require_once plugin_dir_path( __FILE__ ) . '/includes/class-aieo-ratings.php';
}
require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-ratings-rest.php';

// Per-brand flat projection (sibling of core_product_vitals). Read+refresh
// model for wp_aieo_core_brand_vitals — sole front-end source for every
// brand-shopping surface (A-Z catalogue, sliders, archive, navigator).
require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-core-brand-vitals.php';

// Classic-PHP shortcode wrappers for the three "post-related-*" blocks
// (brands / categories / needs). Re-registers the legacy Pods-era
// `[related_brand_shortcode]` and `[secondary_brand_shortcode]` aliases
// so themes that hard-code them in single.php keep working.
require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-post-relations-shortcodes.php';

// FAQ content type (`faq`) + grouping taxonomy (`faq_group`) + the [faq] /
// [aieo_faq] accordion shortcode (self-contained, own scoped CSS). The entry
// list is surfaced from the DMM "Custom Post Types" nav group, not a stray
// top-level menu. Self-instantiates at file end.
require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-faq.php';

// The store CPTs — theme-owned `store_location` + plugin-owned `aieo_store` —
// are surfaced from the DMM "Custom Post Types" nav group, so drop their stray
// top-level sidebar menus. (The `faq` CPT already registers show_in_menu=false.)
add_filter('register_post_type_args', function ($args, $post_type) {
    if (in_array($post_type, array('store_location', 'aieo_store'), true)) {
        $args['show_in_menu'] = false;
    }
    return $args;
}, 20, 2);

// `aieo/menu` block — renders a WP menu as an inline strip (header) or
// accordion (footer); replaces the roosterx-ff [ff_simple_menu] shortcode +
// the hand-coded footer markup with a proper menu-picker block.
require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-menu-block.php';

// `aieo/trust-badges` block — configurable trust strip (icon + title +
// subtitle columns); replaces the hand-coded ff-trust-strip / roosterx trust
// tiles. Bundled in the runtime too (class_exists-guarded).
require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-trust-badges-block.php';

// `aieo/social-icons` block — social links (FB/IG/Pinterest/YouTube/TikTok) with
// colour, shape (round/square) + responsive gap. Bundled in the runtime too.
require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-social-icons-block.php';

// Per-customer engagement-signal query helpers (favourite brands,
// wishlisted brands, rated brands) — bridges brand_affinity_json +
// wp_aieo_wishlist + wp_aieo_ratings to blocks/navigator/push triggers.
require_once plugin_dir_path(__FILE__) . '/includes/dmm/class-aieo-customer-intelligence-query.php';

// Attribution — source tracking for every add-to-cart event.
// See ATTRIBUTION-SCHEMA-BRIEF.md for the schema + GA4 contract.
require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-attribution.php';

// Canonical read-through for per-order facts (revenue + channel). D1 single-
// source-of-truth accessor — see documentation/ORDER_DATA_MAP.md.
require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-order-facts.php';

// ROI Wizard cost ledger (operator/connector ad-spend per dimension per period).
// See documentation/ROI_WIZARD_PLAN.md.
require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-attribution-cost.php';
if ( class_exists( 'AIEO_Attribution_Cost' ) ) { AIEO_Attribution_Cost::init(); }

// Shared attribution-block renderer (styled table + channel vocabulary) used
// by BOTH the order-email origin block and the Gravity notification block.
// Bundled identically in the runtime plugin (checkout-whitelist delegate) —
// class_exists-guarded so whichever copy loads first wins.
if ( ! class_exists( 'AIEO_Attribution_Block' ) ) {
	require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-attribution-block.php';
}

// Order origin + attribution block in the staff (admin) order emails — replaces
// the unreliable PixelYourSite origin line (marketplace vs Site + channel/campaign).
// Also bundled in the runtime plugin: optimised sites trim THIS plugin off
// checkout requests, where synchronous gateways (cheque/COD/bacs) send the
// staff new-order email — the runtime delegate renders the block there.
if ( ! class_exists( 'AIEO_Order_Origin_Email' ) ) {
	require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-order-origin-email.php';
}
if ( class_exists( 'AIEO_Order_Origin_Email' ) ) { AIEO_Order_Origin_Email::register(); }

// ROI Wizard subtab — self-registers onto the DMM module bus when it loads
// (same pattern as the marketplace-order-importer). See documentation/ROI_WIZARD_PLAN.md.
add_action( 'aieo_dmm_register_dynamic_modules', function ( $dmm ) {
	if ( ! class_exists( 'AIEO_DMM_Module_Base' ) ) { return; }
	require_once plugin_dir_path( __FILE__ ) . '/includes/dmm/modules/class-aieo-dmm-roi-wizard.php';
	require_once plugin_dir_path( __FILE__ ) . '/includes/dmm/modules/class-aieo-dmm-ad-connectors.php';
	require_once plugin_dir_path( __FILE__ ) . '/includes/dmm/modules/class-aieo-dmm-utm-explorer.php';
	if ( is_object( $dmm ) && method_exists( $dmm, 'register_module' ) ) {
		if ( class_exists( 'AIEO_DMM_ROI_Wizard' ) ) { $dmm->register_module( new AIEO_DMM_ROI_Wizard() ); }
		if ( class_exists( 'AIEO_DMM_Ad_Connectors' ) ) { $dmm->register_module( new AIEO_DMM_Ad_Connectors() ); }
		// UTM Explorer is consolidated onto the ROI Wizard page (the ROI Wizard
		// composes + renders it), so it is required above but NOT a separate tab.
	}
}, 10, 1 );

// Tier-2 session × attribution bridge — query helpers JOINing the
// Session Tracker tables with the Attribution Engine table for joint
// engagement-attribution analytics. Backs 11 Claude abilities.
require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-session-attribution-bridge.php';

// Heuristic conversion-probability model — buckets historical sessions
// on engagement (scroll-depth × time-on-page) and reports empirical
// P(cart-add) per bucket. Backs the buy-probability-* abilities.
require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-conversion-probability-model.php';

// Perf — source-level PageSpeed mitigations: non-critical CSS deferral
// + LCP image preload on PDP. Hookless until front-end render.
require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-perf.php';

// AIEO Star Ratings → SEO-plugin schema bridge. Feeds the materialized
// `wp_aieo_rating_stats` aggregate into Product JSON-LD emitted by
// AIOSEO / Yoast / Rank Math so Google's rich snippet uses the AIEO
// star data (WC reviews are off on this site). Falls back to its own
// JSON-LD block if no SEO plugin is detected.
require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-ratings-schema.php';

// SEO engine (Stage A). The offline head BUILDER (AIEO_SEO_Head_Builder)
// materialises wp_aieo_seo_head_cache on save/cron; the thin, bundled
// AIEO_SEO_Head emitter serves that cache with one read. All AIEO_SEO_*
// classes autoload from includes/seo/ via the mu-plugin autoloader. The
// builder + store (AIEO_SEO_Store) are core-only; the emitter is bundled in
// both plugins. The builder self-gates on AIEO_SEO_Store::is_active_provider()
// so registering its hooks is harmless while AIOSEO is still active.
if ( class_exists( 'AIEO_SEO_Head_Builder' ) ) {
    AIEO_SEO_Head_Builder::register_build_hooks();
}
// SEO editor metabox (admin only) — the canonical-category picker + SEO title /
// meta / canonical-override / noindex, all written to wp_aieo_seo_posts
// (nothing in postmeta). Allowed regardless of provider state so operators can
// pin canonical categories before cut-over.
if ( is_admin() && class_exists( 'AIEO_SEO_Post_Metabox' ) ) {
    AIEO_SEO_Post_Metabox::register();
}
// Slug monitor (admin only, A2) — auto-create a 301 when a published object's
// slug/URL changes so existing links don't 404. Writes via AIEO_SEO_Redirects.
if ( is_admin() && class_exists( 'AIEO_SEO_Slug_Monitor' ) ) {
    AIEO_SEO_Slug_Monitor::register();
}
// SEO schema self-heal — the same maybe_install() delta pattern every other
// feature uses (Carts/Ratings/Attribution/Consent). Creates/upgrades the SEO
// tables when the bundled schema version is newer than what's installed, so an
// existing site picks up new tables (e.g. the A2 redirects + 404 log) on the
// next admin load after a file upload — no reactivation / version bump needed.
if ( is_admin() ) {
    add_action( 'admin_init', static function () {
        $seo_schema = AIEO_DIR . 'includes/dmm/sql/seo-schema.php';
        if ( file_exists( $seo_schema ) ) {
            require_once $seo_schema;
            if ( function_exists( 'aieo_seo_maybe_install' ) ) {
                aieo_seo_maybe_install();
            }
        }
    }, 5 );
}
// SEO score column on the product/post/page list tables (toggle via Screen
// Options). Reads stored scores in one query per page; gated on seo.enabled.
if ( is_admin() && class_exists( 'AIEO_SEO_Admin_Columns' ) ) {
    AIEO_SEO_Admin_Columns::register();
}
// Goal tracker (site-profiles Phase 3): server-side conversion events (form
// submits, newsletter signups, LearnDash enrol/complete, registrations) into
// the session event stream — the content analog of add-to-cart. Consent-safe
// by construction (records only alongside the consented session cookie).
// Capability shim (products are content, content is products): 60+ AIEO
// handlers historically gate on manage_woocommerce, which NO user holds on
// sites without WooCommerce (quhoma, alfavitari, portals) - every rebuild/
// export button dies with Permission denied there. Grant it to
// administrators when WooCommerce is absent; zero change on shops.
add_filter('user_has_cap', function ($allcaps) {
    if (!empty($allcaps['manage_options']) && empty($allcaps['manage_woocommerce']) && !class_exists('WooCommerce')) {
        $allcaps['manage_woocommerce'] = true;
    }
    return $allcaps;
});

// Canonical landing/campaign stats — parses landing_path + utm_* out of the
// session statics and keeps the durable daily rollup. Registers only the
// aieo_archive_run hook here; the heavy lifting runs inside the crons.
if ( file_exists( AIEO_DIR . 'includes/class-aieo-landing-stats.php' ) ) {
    require_once AIEO_DIR . 'includes/class-aieo-landing-stats.php';
    if ( class_exists( 'AIEO_Landing_Stats' ) ) { AIEO_Landing_Stats::register(); }
}

if ( file_exists( AIEO_DIR . 'includes/class-aieo-goal-tracker.php' ) ) {
    require_once AIEO_DIR . 'includes/class-aieo-goal-tracker.php';
    add_action( 'init', array( 'AIEO_Goal_Tracker', 'register' ), 20 );
}
// Front-end emit: when the runtime plugin is present it OWNS the SEO head
// (defines AIEO_RUNTIME_OWNS_SEO_HEAD + registers AIEO_SEO_Head itself);
// otherwise the core registers the emitter standalone. DEFERRED to
// plugins_loaded@11: the runtime defines its OWNS constant at its own file
// load, which runs AFTER this core file is included (plugins load
// alphabetically: ai-eshop-optimizer before ai-eshop-optimizer-runtime), so an
// include-time !defined() check would always be true and the core would
// register too. Checking on plugins_loaded — after every plugin's main file
// has run — makes the skip reliable. Mirrors the AIEO_RUNTIME_OWNS_SSO_DISPLAY
// gate precedent; register() is idempotent as a second safety net.
add_action( 'plugins_loaded', static function () {
    // Canonical-category permalink filters (A1.3) — registered UNCONDITIONALLY
    // (NOT under the OWNS_SEO_HEAD gate): register() is idempotent (static
    // $done), so whichever plugin calls first wins and the other no-ops. This
    // decouples the permalink filter from the head-emitter OWNS constant, so it
    // can never end up registered in NEITHER plugin. Self-gated on
    // active-provider; harmless no-op without %product_cat%. NOT admin-gated:
    // must be active during the offline build so get_permalink() materialises
    // the canonical-category URL.
    if ( class_exists( 'AIEO_SEO_Primary_Term' ) ) {
        AIEO_SEO_Primary_Term::register();
    }
    // Visible breadcrumb shortcode [aieo_breadcrumb] (the aieo/breadcrumb block
    // auto-registers via block.json). Idempotent; shares AIEO_SEO_Breadcrumb's
    // resolver with the JSON-LD BreadcrumbList so the trail uses the pinned
    // canonical category.
    if ( class_exists( 'AIEO_SEO_Breadcrumb' ) ) {
        AIEO_SEO_Breadcrumb::register();
    }
    // Static sitemap (A1.4): the /sitemap.xml PHP fallback + the robots.txt
    // Sitemap line — self-gated on active-provider (core-free). In production
    // nginx serves the static file at the edge; the build runs offline via the
    // seo_sitemap data-prep stage.
    if ( class_exists( 'AIEO_SEO_Sitemap' ) ) {
        AIEO_SEO_Sitemap::register();
    }
    // Redirect engine (A2): serves 301/302/307/410 + logs 404s. The runtime OWNS
    // it when present — its bundled copy + the aieo-seo-redirects mu-plugin serve
    // front-end redirects, so the core must NOT touch the class here. Referencing
    // it while the runtime copy is already declared triggered a "Cannot redeclare
    // class AIEO_SEO_Redirects" fatal on admin requests (e.g. product save). The
    // core registers it ONLY standalone (runtime absent).
    if ( ! defined( 'AIEO_RUNTIME_OWNS_SEO_HEAD' ) && class_exists( 'AIEO_SEO_Redirects' ) ) {
        AIEO_SEO_Redirects::register();
    }
    // Head emitter: the runtime OWNS it when present (defines
    // AIEO_RUNTIME_OWNS_SEO_HEAD + registers its bundled copy); the core
    // registers it only standalone.
    if ( defined( 'AIEO_RUNTIME_OWNS_SEO_HEAD' ) ) {
        return;
    }
    if ( class_exists( 'AIEO_SEO_Head' ) ) {
        AIEO_SEO_Head::register();
    }
}, 11 );

// Product Swatches runtime + AJAX — canonical copies live in the
// runtime plugin (ai-eshop-optimizer-runtime/includes/dmm/modules/).
// With the runtime present, these require_once calls are no-ops
// (class_exists short-circuits). Without it — i.e. on sites that
// run the main plugin standalone — the admin plugin loads its own
// bundled copy so the frontend swatch render still works.
if ( ! class_exists( 'AIEO_DMM_Swatches_Runtime' ) ) {
    require_once plugin_dir_path( __FILE__ ) . '/includes/dmm/modules/class-aieo-dmm-swatches-runtime.php';
}
if ( ! class_exists( 'AIEO_DMM_Swatches_Ajax' ) ) {
    require_once plugin_dir_path( __FILE__ ) . '/includes/dmm/modules/class-aieo-dmm-swatches-ajax.php';
}

// Load AI Chat addon (Claude-powered conversational search)
require_once plugin_dir_path(__FILE__) . '/ai-eshop-chat-addon.php';

// Load Discount & Marketplace Management (DMM) bootstrap.
require_once plugin_dir_path(__FILE__) . '/includes/dmm/class-aieo-dmm.php';

// Saved & Abandoned Carts subsystem — storefront save/share/resume on a
// dedicated table pair (wp_aieo_carts + wp_aieo_cart_items) PLUS a permanent
// abandoned-cart log captured from wc-checkout-draft orders (with outcome
// tracking) before WooCommerce purges them. Bootstrap loads the data layer,
// the storefront feature, and the hourly capture job.
require_once plugin_dir_path(__FILE__) . '/includes/carts/class-aieo-carts.php';

// Server Events (Track B) — server-side conversion forwarding (Facebook CAPI
// today; GA4 Measurement Protocol later). Loaded + booted UNCONDITIONALLY: the
// server side is pure backend with no front-end surface and must run with or
// without the runtime. Hook callbacks lazy-reference their deps, so load order
// is irrelevant. The browser pixel surface (B2b) is wired separately behind a
// dedicated AIEO_RUNTIME_OWNS_PIXEL constant.
require_once plugin_dir_path(__FILE__) . '/includes/server-events/class-aieo-server-events.php';
AIEO_Server_Events::instance();

// First-party GDPR consent (Track C) — one source of truth (cookie + allows()
// API), Google Consent Mode v2 signals, and a thin first-party banner. The
// session-tracker consent gates consult this first; external CMPs remain the
// fallback when the banner is off. Core-loaded (front-end surface).
require_once plugin_dir_path(__FILE__) . '/includes/consent/class-aieo-consent.php';
AIEO_Consent::instance()->register();

// Google One Tap (recognise-only) — thin front-end loader. Renders the GSI
// loader for logged-out visitors when the merchant enables One Tap (Consent/GDPR
// page) + a Google client_id is set (Social Login). The JS gates on the
// `personalize` consent flag + skips logged-in browsers. Default-OFF.
require_once plugin_dir_path(__FILE__) . '/includes/social-login/class-aieo-one-tap.php';
AIEO_One_Tap::register();

// Panel manifest — the single descriptor the Rooster panel polls
// (uploads/aieo-panel/manifest.json). Self-contained; rewrites on push/SEO engine
// + VAPID changes (debounced). Registers the `wp aieo push prepare` /
// `verify-readiness` CLI commands. Activation also defaults a FRESH site's push
// engine to paused (add_option no-op → existing live sites untouched).
require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-panel-manifest.php';
AIEO_Panel_Manifest::register();
register_activation_hook(__FILE__, 'aieo_panel_manifest_activate');

// WC 10.9 forward-compat migration — relocates botiga_variation_gallery
// + product_attribute_* term metas into WooCommerce's canonical keys
// (`_product_image_gallery`, `color`) so that when a merchant enables
// the `variation_gallery` / `wc-visual-attribute` feature toggles the
// data is already where WC expects it. Idempotent, batched via Action
// Scheduler; also exposed as `wp aieo migrate wc-compat`.
require_once plugin_dir_path(__FILE__) . '/includes/migrations/class-aieo-wc-compat-migration.php';
AIEO_WC_Compat_Migration::bootstrap();

// Load Claude Abilities API bootstrap — registers 21 read-only abilities
// (audience priorities, product needs, recos, attribution, ratings,
// brand vitals, customer lookalikes) under the `aieo` category for the
// woocommerce-claude MCP server / Claude Code skill harness to call.
// Exposed at /wp-json/wp-abilities/v1/abilities/ when WP 6.9+.
require_once plugin_dir_path(__FILE__) . '/includes/claude/class-aieo-claude-bootstrap.php';
add_action('init', array('AIEO\\Claude\\Bootstrap', 'init'), 5);

// Merchant Insights Chat - wp-admin chat page.
if ( is_admin() || ( defined('DOING_AJAX') && DOING_AJAX ) ) {
    require_once plugin_dir_path(__FILE__) . '/includes/chat/class-aieo-merchant-chat.php';
    add_action('init', array('AIEO_Merchant_Chat', 'init'), 20);
}


// External-database connection registry (ERP / sister-store DBs).
// Always loaded — its hooks are admin-conditional internally, but the
// connection helper (AIEO_ERP::for_connection_id) needs to be reachable
// from cron and wp-cli too.
require_once plugin_dir_path(__FILE__) . '/includes/erp/class-aieo-erp.php';

// Retention + Remote-Export bootstrap (2026-05).
// Schedules the nightly aggregate-rollup cron, the configurable remote
// export cron, and binds AE/Reco archive handlers to the shared
// `aieo_archive_run` hook fired by AIEO_Session_Tracker::archive_sessions().
// Installs the 5 rollup tables + remote-export state bookkeeping table.
// MUST load AFTER class-aieo-erp.php — the remote-export manager calls
// AIEO_ERP::for_connection_id() to talk to the destination DB.
require_once plugin_dir_path(__FILE__) . '/includes/class-aieo-retention-bootstrap.php';
AIEO_Retention_Bootstrap::init();

// IAPI control panel (Phase C-α+ admin UI).
if ( is_admin() ) {
    require_once plugin_dir_path(__FILE__) . '/includes/admin/class-aieo-iapi-admin.php';
}

// Ctrl/Cmd+K command palette — overlay restricted to AIEO admin pages only.
if ( is_admin() ) {
    require_once plugin_dir_path(__FILE__) . '/includes/admin/class-aieo-command-palette.php';
}

// Initialize the plugin
function AIEO_init()
{
    // Phase-1 perf gate: class AIEO is the admin monolith (menus, CSV-export
    // pipeline, Rooster API client, DB optimizer — ~6.4k lines). Its hooks
    // are admin_menu / admin_bar_menu / admin_post_* / wp_ajax_* plus ONE
    // frontend-relevant hook: the wp_head admin-bar CSS fix. So:
    //   • admin + AJAX requests construct it eagerly (status quo);
    //   • all other requests defer to init and construct it only for
    //     logged-in users — the only visitors who can see the admin bar.
    //     Deciding at init (not here at plugins_loaded) avoids forcing
    //     early user determination, which breaks late-registered auth
    //     filters (session tokens, 2FA).
    // Anonymous storefront / REST / cron traffic skips the class entirely:
    // none of its hooks can fire there.
    if (is_admin() || wp_doing_ajax()) {
        new AIEO();
    } else {
        add_action('init', function () {
            if (is_user_logged_in()) {
                new AIEO();
            }
        }, 0);
    }

    // ALWAYS initialize Session Tracker to register REST endpoints
    // The tracker will handle whether to enqueue scripts based on settings
    AIEO_Session_Tracker::get_instance();

    // Initialize Recommendations Cache hooks (invalidation on product save/delete)
    // Only when WooCommerce is active (recommendations are WooCommerce-specific)
    if ( class_exists( 'WooCommerce' ) && class_exists( 'AIEO_Recommendations_Cache' ) ) {
        AIEO_Recommendations_Cache::init_hooks();
    }

    // Discount & Marketplace Management bootstrap (menu, modules, hooks).
    if ( class_exists( 'AIEO_DMM' ) ) {
        AIEO_DMM::instance();
    }

    // Saved & Abandoned Carts — boots the storefront save/share/resume
    // hooks + the hourly abandoned-cart capture job. Tables install lazily.
    // Skipped when the runtime plugin is present: it owns the frontend
    // surface (AIEO_RUNTIME_OWNS_AJAX) and instantiates AIEO_Carts itself,
    // so the feature keeps working on trimmed-whitelist requests where this
    // admin plugin isn't loaded. (AIEO_Carts::instance() is a singleton, so
    // even un-gated double calls are harmless — this just honours the contract.)
    if ( class_exists( 'AIEO_Carts' ) && ! defined( 'AIEO_RUNTIME_OWNS_AJAX' ) ) {
        AIEO_Carts::instance();
    }

    // IAPI control panel — admin-only.
    if ( is_admin() && class_exists( 'AIEO_IAPI_Admin' ) ) {
        AIEO_IAPI_Admin::instance();
    }

    // Wishlist — table install runs lazily on plugins_loaded via the
    // class's own maybe_install(); this just kicks the singleton so
    // the AJAX hooks + asset localisation register on every load.
    if ( class_exists( 'AIEO_Wishlist' ) ) {
        AIEO_Wishlist::instance();
    }
    // PPE compatibility shim — routes legacy `om_ppe_wishlist_toggle`
    // AJAX calls to the AIEO endpoint + emits a shim `omPPEWishlist`
    // global. Safe to run alongside PPE (our handler wins on priority).
    if ( class_exists( 'AIEO_Wishlist_PPE_Compat' ) ) {
        AIEO_Wishlist_PPE_Compat::instance();
    }

    // Star Ratings — table install runs lazily; this kicks the singleton
    // so the AJAX hooks + asset enqueue + admin menu register on every load.
    if ( class_exists( 'AIEO_Ratings' ) ) {
        AIEO_Ratings::instance();
    }
    if ( class_exists( 'AIEO_Ratings_REST' ) ) {
        AIEO_Ratings_REST::instance();
    }

    // Product Swatches runtime — frontend renderer that emits per-card
    // swatch markup from `wp_aieo_dmm_swatches_cache` without
    // instantiating WC_Product_Variation objects. Gated internally by
    // `aieo_dmm_swatches_runtime_enabled` so the kill-switch flip
    // happens via the Swatches admin tab. AJAX class always registers
    // its endpoints so the cart/quick-view round-trips work regardless
    // of the render flag.
    if ( class_exists( 'AIEO_DMM_Swatches_Runtime' ) ) {
        AIEO_DMM_Swatches_Runtime::instance();
    }
    if ( class_exists( 'AIEO_DMM_Swatches_Ajax' ) ) {
        AIEO_DMM_Swatches_Ajax::instance();
    }

    // Core Brand Vitals — registers auto-mark-dirty hooks on product/term
    // saves and the AJAX bundle endpoint for the brand-slider block.
    if ( class_exists( 'AIEO_Core_Brand_Vitals' ) ) {
        AIEO_Core_Brand_Vitals::instance();
    }

    // Attribution — boots the singleton so AJAX endpoints + schema
    // install + order backfill hook all register. Cheap no-op when
    // the table is already in place.
    if ( class_exists( 'AIEO_Attribution' ) ) {
        AIEO_Attribution::instance();
    }

    // Let the pricing engine preload per-request on archive/single views.
    if ( class_exists( 'AIEO_DMM_Pricing_Engine' ) ) {
        AIEO_DMM_Pricing_Engine::instance()->register_preload_hooks();
    }
}
add_action('plugins_loaded', 'AIEO_init');




function aieo_is_index_wp_mysql_for_speed_active()
{
    if (!function_exists('is_plugin_active')) {
        include_once ABSPATH . 'wp-admin/includes/plugin.php';
    }
    return is_plugin_active('index-wp-mysql-for-speed/index-wp-mysql-for-speed.php');
}


function aieo_set_default_options()
{
    global $wpdb;

    if (get_option('aieo_version') === false) {
        add_option('aieo_version', AIEO_VERSION, '', 'no');
    }

    // Check if the option already exists. If not, add it.
    if (get_option('aieo_site_prefix') === false) {
        // Set the value for 'aieo_site_prefix' option
        $prefix_name = $wpdb->prefix;
        add_option('aieo_site_prefix', $prefix_name, '', 'no');
    }

    // Always ensure correct table names (fix old installations with wrong suffix)
    $correct_orders_table = $wpdb->prefix . 'aieo_temp_orders';
    // Use update_option instead of add_option to ensure immediate availability
    update_option('aieo_site_orders_table', $correct_orders_table, 'no');

    $correct_store_orders_table = $wpdb->prefix . 'aieo_temp_store_orders';
    update_option('aieo_store_orders_table', $correct_store_orders_table, 'no');

    update_option('aieo_offline_orders', $correct_store_orders_table, 'no');

    $correct_products_table = $wpdb->prefix . 'aieo_temp_product_catalogue';
    update_option('aieo_site_products_table', $correct_products_table, 'no');

    if (get_option('aieo_core_attribute_brands') === false) {
        // Set the value for 'aieo_core_attribute_brands' option
        $default_attribute_for_brand = 'product_brand';
        add_option('aieo_core_attribute_brands', $default_attribute_for_brand, '', 'no');
    }

    if (get_option('aieo_core_attribute_size') === false) {
        // Set the value for 'aieo_core_attribute_size' option
        $default_attribute_for_size = 'pa_size,pa_noumero,pa_megethos';
        add_option('aieo_core_attribute_size', $default_attribute_for_size, '', 'no');
    }

    if (get_option('aieo_core_attribute_color') === false) {
        // Set the value for 'aieo_core_attribute_color' option
        $default_attribute_for_color = 'pa_color,pa_colour';
        add_option('aieo_core_attribute_color', $default_attribute_for_color, '', 'no');
    }

    if (get_option('aieo_core_attribute_need') === false) {
        // Set the value for 'aieo_core_attribute_need' option
        $default_attribute_for_need = 'pa_need';
        add_option('aieo_core_attribute_need', $default_attribute_for_need, '', 'no');
    }

    if (get_option('aieo_season_days_past') === false) {
        // Set the value for 'aieo_season_days_past' option
        $default_season_days_past = '15';
        add_option('aieo_season_days_past', $default_season_days_past, '', 'no');
    }

    if (get_option('aieo_analytics_eponymous_id_field') === false) {
        // Set the value for 'aieo_analytics_eponymous_id_field' option
        $default_analytics_eponymous_id_field = 'EponymousId';
        add_option('aieo_analytics_eponymous_id_field', $default_analytics_eponymous_id_field, '', 'no');
    }

    if (get_option('aieo_season_days_after') === false) {
        // Set the value for 'aieo_season_days_after' option
        $default_season_days_after = '60';
        add_option('aieo_season_days_after', $default_season_days_after, '', 'no');
    }

    // Use update_option instead of add_option to ensure immediate availability
    $table_name = $wpdb->prefix . 'aieo_core_product_attributes';
    update_option('aieo_core_product_attributes_table', $table_name, 'no');

    $table_name = $wpdb->prefix . 'aieo_core_product_category_hierarchy';
    update_option('aieo_core_product_category_hierarchy_table', $table_name, 'no');

    $table_name = $wpdb->prefix . 'aieo_core_category_brand_popularity';
    update_option('aieo_core_category_brand_popularity_table', $table_name, 'no');

    $table_name = $wpdb->prefix . 'aieo_core_product_vitals';
    update_option('aieo_core_product_vitals_table', $table_name, 'no');

    $table_name = $wpdb->prefix . 'aieo_core_variation_vitals';
    update_option('aieo_core_variation_vitals_table', $table_name, 'no');

    $table_name = $wpdb->prefix . 'aieo_core_reciprocal_products';
    update_option('aieo_core_reciprocal_products_table', $table_name, 'no');

    $table_name = $wpdb->prefix . 'aieo_core_brand_vitals';
    update_option('aieo_core_brand_vitals_table', $table_name, 'no');

    if (get_option('aieo_erp_store_products_table') === false) {
        // Set the value for 'aieo_erp_store_products_table' option
        $table_name = $wpdb->prefix . 'aieo_erp_store_products';
        add_option('aieo_erp_store_products_table', $table_name, '', 'no');
    }

    if (get_option('aieo_erp_store_orders_table') === false) {
        // Set the value for 'aieo_erp_store_orders_table' option
        $table_name = $wpdb->prefix . 'aieo_erp_store_orders';
        add_option('aieo_erp_store_orders_table', $table_name, '', 'no');
    }

    if (get_option('aieo_erp_store_customers_table') === false) {
        // Set the value for 'aieo_erp_store_customers_table' option
        $table_name = $wpdb->prefix . 'aieo_erp_store_customers';
        add_option('aieo_erp_store_customers_table', $table_name, '', 'no');
    }

    if (get_option('aieo_google_user_journey_table') === false) {
        // Set the value for 'aieo_google_user_journey_table' option
        $table_name = $wpdb->prefix . 'aieo_google_user_journey';
        add_option('aieo_google_user_journey_table', $table_name, '', 'no');
    }

    if (get_option('aieo_order_id') === false) {
        add_option('aieo_order_id', '1', '', 'no');
    }

    if (get_option('aieo_max_records') === false) {
        add_option('aieo_max_records', '1000', '', 'no');
    }

    if (get_option('aieo_batch_size') === false) {
        add_option('aieo_batch_size', '100', '', 'no');
    }

    if (get_option('aieo_batch_sleep') === false) {
        add_option('aieo_batch_sleep', '0', '', 'no');
    }

    if (get_option('aieo_include_text') === false) {
        add_option('aieo_include_text', '1', '', 'no');
    }

    if (get_option('aieo_include_attributes') === false) {
        add_option('aieo_include_attributes', '1', '', 'no');
    }

    // Export checkbox options - default to '0' (unchecked) so user chooses what to run
    if (get_option('aieo_create_attributes') === false) {
        add_option('aieo_create_attributes', '0', '', 'no');
    }

    if (get_option('aieo_create_needs') === false) {
        add_option('aieo_create_needs', '0', '', 'no');
    }

    if (get_option('aieo_create_need_suggestions') === false) {
        add_option('aieo_create_need_suggestions', '0', '', 'no');
    }

    if (get_option('aieo_create_catalogue') === false) {
        add_option('aieo_create_catalogue', '0', '', 'no');
    }

    if (get_option('aieo_create_orders') === false) {
        add_option('aieo_create_orders', '0', '', 'no');
    }

    if (get_option('aieo_erp_consolidation') === false) {
        add_option('aieo_erp_consolidation', '0', '', 'no');
    }

    if (get_option('aieo_erp_merge_orders') === false) {
        add_option('aieo_erp_merge_orders', '0', '', 'no');
    }

    if (get_option('aieo_include_unsold') === false) {
        add_option('aieo_include_unsold', '0', '', 'no');
    }

    if (get_option('aieo_include_aggregate_stats') === false) {
        add_option('aieo_include_aggregate_stats', '0', '', 'no');
    }

    if (get_option('aieo_include_aggregate_category_brand_stats') === false) {
        add_option('aieo_include_aggregate_category_brand_stats', '0', '', 'no');
    }

    if (get_option('aieo_include_aggregate_cust_std_stats') === false) {
        add_option('aieo_include_aggregate_cust_std_stats', '0', '', 'no');
    }

    if (get_option('aieo_include_aggregate_cust_adv_stats') === false) {
        add_option('aieo_include_aggregate_cust_adv_stats', '0', '', 'no');
    }

    if (get_option('aieo_create_uuids') === false) {
        add_option('aieo_create_uuids', '0', '', 'no');
    }

    if (get_option('aieo_create_export') === false) {
        add_option('aieo_create_export', '0', '', 'no');
    }

    if (get_option('aieo_include_full_text') === false) {
        add_option('aieo_include_full_text', '0', '', 'no');
    }

    if (get_option('aieo_include_price') === false) {
        add_option('aieo_include_price', '1', '', 'no');
    }

    if (get_option('aieo_variations_choice') === false) {
        add_option('aieo_variations_choice', '1', '', 'no');
    }

    if (get_option('aieo_include_eponymous') === false) {
        add_option('aieo_include_eponymous', '1', '', 'no');
    }

    if (get_option('aieo_include_reciprocity') === false) {
        add_option('aieo_include_reciprocity', '0', '', 'no');
    }

    if (get_option('aieo_include_seasonality') === false) {
        add_option('aieo_include_seasonality', '0', '', 'no');
    }

    if (get_option('aieo_include_restricted_seasonality') === false) {
        add_option('aieo_include_restricted_seasonality', '0', '', 'no');
    }

    if (get_option('aieo_instore_anonymous') === false) {
        add_option('aieo_instore_anonymous', '0', '', 'no');
    }

    if (get_option('aieo_activate_utm_stats') === false) {
        add_option('aieo_activate_utm_stats', '0', '', 'no');
    }

    if (get_option('aieo_google_ga4') === false) {
        add_option('aieo_google_ga4', '0', '', 'no');
    }

    if (get_option('aieo_display_upsells') === false) {
        add_option('aieo_display_upsells', '0', '', 'no');
    }

    if (get_option('aieo_display_cross_sells') === false) {
        add_option('aieo_display_cross_sells', '0', '', 'no');
    }

    if (get_option('aieo_display_related') === false) {
        add_option('aieo_display_related', '0', '', 'no');
    }

    if (get_option('aieo_display_recently_viewed_products') === false) {
        add_option('aieo_display_recently_viewed_products', '0', '', 'no');
    }

    if (get_option('aieo_display_same_needs') === false) {
        add_option('aieo_display_same_needs', '0', '', 'no');
    }

    if (get_option('aieo_display_categories_by_sales') === false) {
        add_option('aieo_display_categories_by_sales', '0', '', 'no');
    }

    if (get_option('aieo_category_sales_period') === false) {
        add_option('aieo_category_sales_period', 'all_time', '', 'no');
    }

    if (get_option('aieo_season_days_past') === false) {
        add_option('aieo_season_days_past', '15', '', 'no');
    }

    if (get_option('aieo_season_days_after') === false) {
        add_option('aieo_season_days_after', '60', '', 'no');
    }

    // Record initial activation date (only set once, never updated)
    if (get_option('aieo_initial_activation_date') === false) {
        add_option('aieo_initial_activation_date', current_time('mysql', true), '', 'no');
    }
}

register_activation_hook(__FILE__, 'aieo_set_default_options');

// -----------------------------------------------------------------------------
// MU-plugin installer
// -----------------------------------------------------------------------------
// AIEO ships two companion mu-plugins:
//   - 0-aieo-action-scheduler-core.php — AS query-limiting + retention + bypass
//     switch. The `0-` prefix forces it to load first (alphabetical order in
//     mu-plugins). Required by the new AIEO_DMM_As_Cleanup module.
//   - aieo-autoload.php — class autoloader for ai-eshop-optimizer +
//     ai-eshop-optimizer-runtime so neither plugin needs `require_once` chains
//     for AIEO_* classes.
// Both ship inside this plugin at install/mu-plugins/. On activation we copy
// them into wp-content/mu-plugins/ if either is missing OR if the bundled
// version's mtime is newer than what's already there (auto-refresh on update).
// Filesystem errors are reported via a transient admin notice so the operator
// knows to copy them by hand on hosts where mu-plugins/ isn't writable.
register_activation_hook(__FILE__, 'aieo_install_bundled_mu_plugins');

function aieo_install_bundled_mu_plugins() {
    $src_dir = plugin_dir_path(__FILE__) . 'install/mu-plugins/';
    if (!is_dir($src_dir)) { return; }

    $dst_dir = defined('WPMU_PLUGIN_DIR') ? WPMU_PLUGIN_DIR : WP_CONTENT_DIR . '/mu-plugins';

    if (!is_dir($dst_dir)) {
        // wp_mkdir_p() honours fs perms set by the host's WP config.
        if (!wp_mkdir_p($dst_dir)) {
            set_transient(
                'aieo_mu_install_error',
                sprintf('Could not create %s. Copy install/mu-plugins/* there manually.', $dst_dir),
                DAY_IN_SECONDS
            );
            return;
        }
    }

    // Files we ship, with optional `supersedes` clauses. Listed peer files
    // are legacy forks owned by the OM Optimizer Hub family. AIEO is the
    // canonical owner of these mu-plugins going forward — we install the
    // AIEO version and then DELETE the legacy peer so only one provider
    // of each function lives in mu-plugins/ at a time. AIEO's files
    // include om_* function aliases (wrapped in `function_exists()` guards
    // — see install/mu-plugins/0-aieo-action-scheduler-core.php tail) so
    // any caller of the old om_as_* names keeps working after the legacy
    // file is removed.
    $files = array(
        '0-aieo-action-scheduler-core.php' => array(
            'supersedes' => array('0-om-action-scheduler-core.php'),
        ),
        'aieo-autoload.php'                => array(),
        // Lean front-end redirect server — must-use so it survives the
        // active_plugins load-optimisation that trims the AIEO runtime/core on
        // storefront + 404 paths. Loads ONLY the bundled AIEO_SEO_Redirects
        // class (via the autoloader above), never the full plugin.
        'aieo-seo-redirects.php'           => array(),
        'aieo-as-ajax-query-protection.php' => array(
            'supersedes' => array('om-as-ajax-query-protection.php'),
        ),
        // Collapses runaway recurring AS jobs (AutomateWoo's
        // `delete_old_logs/start`, which the AS-ajax read protection above
        // would otherwise let duplicate on every admin_init/heartbeat) back to
        // a single pending action. See the file header for the full mechanism.
        'aieo-as-dedup-recurring-jobs.php'  => array(),
    );
    $failed = array();

    foreach ($files as $name => $opts) {
        $src = $src_dir . $name;
        $dst = $dst_dir . '/' . $name;
        if (!file_exists($src)) { continue; }

        // 1. Copy the bundled AIEO version into mu-plugins/ (idempotent —
        //    skip if the target is missing OR bundled is newer).
        $needs_copy = !file_exists($dst) || @filemtime($dst) < @filemtime($src);
        if ($needs_copy) {
            if (!@copy($src, $dst)) {
                $failed[] = $name;
                continue;
            }
        }

        // 2. Retire legacy forks AFTER the replacement is in place.
        //    Two-step (copy then unlink) — never leaves the site without
        //    the function definitions, even for a single request. Bundled
        //    AIEO file exposes om_as_* aliases so legacy callers keep
        //    working transparently.
        if (!empty($opts['supersedes']) && is_array($opts['supersedes'])) {
            foreach ($opts['supersedes'] as $peer) {
                if ($peer === $name) { continue; }
                $peer_path = $dst_dir . '/' . $peer;
                if (file_exists($peer_path)) {
                    @unlink($peer_path);
                }
            }
        }
    }

    if (!empty($failed)) {
        set_transient(
            'aieo_mu_install_error',
            'Could not write to ' . $dst_dir . '. Copy these by hand from install/mu-plugins/: ' . implode(', ', $failed),
            DAY_IN_SECONDS
        );
    }
}

// Surface the install error (if any) as an admin notice on the next page load.
add_action('admin_notices', function () {
    $msg = get_transient('aieo_mu_install_error');
    if (!$msg || !current_user_can('manage_options')) { return; }
    printf(
        '<div class="notice notice-warning is-dismissible"><p><strong>AIEO:</strong> %s</p></div>',
        esc_html($msg)
    );
});

// Inside your plugin's main file or in a file that gets loaded during plugin activation

// Register a hook to execute your table creation function during plugin activation
register_activation_hook(__FILE__, 'aieo_create_temp_tables_activation_hook');

// Define the activation hook function
function aieo_create_temp_tables_activation_hook() {
    global $wpdb;

    // The table creators live in the lazily-loaded SQL layer.
    aieo_load_sql_layer();

    // Enable error reporting for debugging
    $wpdb->show_errors();
    aieo_debug_log('AIEO: Starting table creation activation hook');

    // Load WordPress database manipulation functions
    require_once(ABSPATH . 'wp-admin/includes/upgrade.php');

    // CRITICAL FIX: Set options for backward compatibility with stored procedures
    // that still read these options
    aieo_set_default_options();
    aieo_debug_log('AIEO: Options set');

    // CRITICAL FIX: Call table creation functions WITHOUT parameters
    // Table names are now constructed internally like chat tables (using $wpdb->prefix directly)
    // This eliminates dependency on options and prevents race conditions
    aieo_debug_log('AIEO: Creating temp_orders table');
    aieo_create_temp_orders_table();
    aieo_debug_log('AIEO: Creating temp_store_orders table');
    aieo_create_temp_store_orders_table();
    aieo_debug_log('AIEO: Creating temp_products table');
    aieo_create_temp_products_table();
    aieo_debug_log('AIEO: Creating core_product_attributes table');
    aieo_create_core_product_attributes_table();
    aieo_debug_log('AIEO: Creating core_product_category_hierarchy table');
    aieo_create_core_product_category_hierarchy_table();
    aieo_debug_log('AIEO: Creating core_category_brand_popularity table');
    aieo_create_core_category_brand_popularity_table();
    aieo_debug_log('AIEO: Creating core_product_vitals table');
    aieo_create_core_product_vitals_table();
    aieo_debug_log('AIEO: Creating core_variation_vitals table');
    aieo_create_core_variation_vitals_table();
    aieo_debug_log('AIEO: Creating core_reciprocal_products table');
    aieo_create_core_reciprocal_products_table();
    aieo_debug_log('AIEO: Creating core_brand_vitals table');
    aieo_create_core_brand_vitals_table();

    // Create AI Chat tables
    aieo_debug_log('AIEO: Creating chat_embeddings table');
    aieo_create_chat_embeddings_table();
    aieo_debug_log('AIEO: Creating chat_history table');
    aieo_create_chat_history_table();

    // Create Session Tracker tables
    aieo_debug_log('AIEO: Creating sessions table');
    aieo_create_sessions_table();
    aieo_debug_log('AIEO: Creating session archive tables');
    aieo_create_session_archive_tables();

    // Session-analytics pipeline tables (journeys + product metrics).
    aieo_debug_log('AIEO: Creating session_journeys table');
    aieo_create_session_journeys_table();
    aieo_debug_log('AIEO: Creating product_session_metrics table');
    aieo_create_product_session_metrics_table();

    // Schedule the session-analytics cron jobs (idempotent; the cron handlers
    // are registered per-request by aieo_bootstrap_session_analytics()).
    $sa_proc_file = plugin_dir_path(__FILE__) . 'includes/class-session-analytics-processor.php';
    if (file_exists($sa_proc_file)) {
        require_once $sa_proc_file;
        if (class_exists('AIEO_Session_Analytics_Processor')) {
            AIEO_Session_Analytics_Processor::get_instance()->schedule_analytics_cron();
        }
    }

    // Create AI Models Registry table
    aieo_debug_log('AIEO: Creating ai_models table');
    aieo_create_ai_models_table();

    // Create Product Recommendations Cache table
    aieo_debug_log('AIEO: Creating product_recommendations table');
    aieo_create_product_recommendations_table();

    // Create the reco reverse-reference map (DB #34) — indexed invalidation.
    aieo_debug_log('AIEO: Creating reco_reverse_refs table');
    aieo_create_reco_reverse_refs_table();

    // Create Product Need Suggestions table
    aieo_debug_log('AIEO: Creating product_need_suggestions table');
    aieo_create_product_need_suggestions_table();

    // Create Discount & Marketplace Management (DMM) tables and migrate legacy data.
    if (function_exists('aieo_dmm_create_tables')) {
        aieo_debug_log('AIEO: Creating DMM tables');
        aieo_dmm_create_tables();
    }

    // Provision the /brand-shopping page (idempotent — only inserts if
    // missing). Loads the installer file directly so activation works
    // even before plugins_loaded fires.
    $brand_page_installer = plugin_dir_path(__FILE__) . 'includes/dmm/class-aieo-brand-page-installer.php';
    if (file_exists($brand_page_installer)) {
        require_once $brand_page_installer;
        if (class_exists('AIEO_Brand_Page_Installer')) {
            $page_id = AIEO_Brand_Page_Installer::install();
            aieo_debug_log('AIEO: Brand-shopping page install -> ' . (is_wp_error($page_id) ? $page_id->get_error_message() : (string) $page_id));
        }
    }

    // Run version-gated DB migrations now that every table exists (idempotent;
    // covers both fresh activation and the version-bump upgrade path that also
    // calls this function). Index trim (#33), reco reverse-ref backfill (#34),
    // analytics atomic-swap procs (#35).
    aieo_run_db_migrations();

    aieo_debug_log('AIEO: Table creation activation hook completed');

}


function aieo_create_stored_procedures()
{
    global $wpdb;

    // The aieo_create_sp_* / aieo_drop_sp_* installers live in the
    // lazily-loaded SQL layer.
    aieo_load_sql_layer();

    // CRITICAL FIX: Ensure options are set BEFORE trying to read them
    // This fixes the race condition where stored procedures weren't created on first activation
    aieo_set_default_options();

    $sp_prefix = get_option('aieo_site_prefix');
    $sp_orders_table = get_option('aieo_site_orders_table');
    $sp_store_orders_table = get_option('aieo_store_orders_table');
    $sp_products_table = get_option('aieo_site_products_table');
    $sp_core_product_attributes_table_name = get_option('aieo_core_product_attributes_table');
    $sp_core_product_category_hierarchy_table_name = get_option('aieo_core_product_category_hierarchy_table');
    $sp_core_product_vitals_table_name = get_option('aieo_core_product_vitals_table');
    $sp_core_variation_vitals_table_name = get_option('aieo_core_variation_vitals_table');
    $sp_erp_store_products_table_name = get_option('aieo_erp_store_products_table');
    $sp_erp_store_orders_table_name = get_option('aieo_erp_store_orders_table');
    $sp_erp_store_customers_table_name = get_option('aieo_erp_store_customers_table');
    $sp_google_user_journey_table_name = get_option('aieo_google_user_journey_table');

    $domain_name = get_site_url();

    // CRITICAL FIX: Drop all procedures BEFORE creating them
    // This prevents "PROCEDURE already exists" errors during activation
    // Suppress errors during drop (procedures might not exist on first activation)
    $wpdb->hide_errors();
    $wpdb->suppress_errors(true);

    @aieo_drop_sp_AIEO_InsertCoreProductAttributes($sp_prefix);
    @aieo_drop_sp_AIEO_InsertProductCategoryHierarchy($sp_prefix);
    @aieo_drop_sp_AIEO_InsertProductVitals($sp_prefix);
    @aieo_drop_sp_AIEO_InsertVariationVitals($sp_prefix);
    @aieo_drop_sp_AIEO_CombineVariationVitalsInProductVitals($sp_prefix);
    @aieo_drop_sp_AIEO_UpdateOrdersWithUnsold($sp_prefix);
    @aieo_drop_sp_AIEO_FindDifferences($sp_prefix);
    @aieo_drop_sp_AIEO_Create_Product_Catalogue_HPOS($sp_prefix);
    @aieo_drop_sp_AIEO_Export_Orders_HPOS($sp_prefix);
    @aieo_drop_sp_AIEO_Export_Store_Orders($sp_prefix);
    @aieo_drop_sp_AIEO_Merge_Store_Orders($sp_prefix);
    @aieo_drop_sp_AIEO_Export_Orders_non_HPOS($sp_prefix);
    @aieo_drop_sp_AIEO_RenameTable($sp_prefix);
    @aieo_drop_sp_AIEO_UpdateContentWordCount($sp_prefix);
    @aieo_drop_sp_AIEO_UpdateContentOutgoingLinks($sp_prefix);
    @aieo_drop_sp_AIEO_UpdateContentIntroPlain($sp_prefix);
    @aieo_drop_sp_AIEO_UpdateProductCentricStats($sp_prefix);
    @aieo_drop_sp_AIEO_UpdateCustomerCentricStdStats($sp_prefix);
    @aieo_drop_sp_AIEO_UpdateCustomerCentricAdvStats($sp_prefix);
    @aieo_drop_sp_AIEO_GenerateGraphDBUUIDs($sp_prefix);
    @aieo_drop_sp_AIEO_GenerateGraphDBUUIDFreqs($sp_prefix);
    @aieo_drop_sp_AIEO_PopulateProductNeeds($sp_prefix);

    // Drop MySQL functions
    @aieo_drop_function_AIEO_StripHTML();
    @aieo_drop_function_AIEO_CountWords();
    @aieo_drop_function_AIEO_CountLinks();

    // Drop legacy procedures that are no longer created but may exist from older versions
    @aieo_drop_sp_AIEO_Include_Products_Without_Sales();
    // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- DDL statement
    @$wpdb->query("DROP PROCEDURE IF EXISTS UpdateShortDescriptionForProduct");

    // Re-enable error reporting for CREATE statements
    $wpdb->show_errors();
    $wpdb->suppress_errors(false);

    // Create optimized v2 procedures
    aieo_create_sp_AIEO_InsertCoreProductAttributes($sp_prefix, $sp_core_product_attributes_table_name);
    aieo_create_sp_AIEO_InsertProductCategoryHierarchy($sp_prefix, $sp_core_product_category_hierarchy_table_name);
    aieo_create_sp_AIEO_InsertProductVitals($sp_prefix, $sp_core_product_vitals_table_name, $domain_name);
    aieo_create_sp_AIEO_InsertVariationVitals($sp_prefix, $sp_core_variation_vitals_table_name);
    aieo_create_sp_AIEO_CombineVariationVitalsInProductVitals($sp_prefix, $sp_core_product_vitals_table_name, $domain_name);

    aieo_create_sp_AIEO_UpdateOrdersWithUnsold($sp_prefix, $sp_orders_table, $sp_products_table);
    aieo_create_sp_AIEO_FindDifferences($sp_prefix);
    aieo_create_sp_AIEO_Create_Product_Catalogue_HPOS($sp_prefix, $sp_products_table);
    aieo_create_sp_AIEO_Export_Orders_HPOS($sp_prefix, $sp_orders_table);
    aieo_create_sp_AIEO_Export_Store_Orders($sp_prefix, $sp_store_orders_table, $sp_erp_store_products_table_name, $sp_erp_store_orders_table_name, $sp_erp_store_customers_table_name);
    aieo_create_sp_AIEO_Merge_Store_Orders($sp_prefix);
    aieo_create_sp_AIEO_Export_Orders_non_HPOS($sp_prefix, $sp_orders_table);
    // UNUSED - Never called in current codebase
    aieo_create_sp_AIEO_RenameTable($sp_prefix);
    aieo_create_sp_AIEO_UpdateContentWordCount($sp_prefix);
    aieo_create_sp_AIEO_UpdateContentOutgoingLinks($sp_prefix);
    aieo_create_sp_AIEO_UpdateContentIntroPlain($sp_prefix);
    aieo_create_sp_UpdateProductCentricStats($sp_prefix);
    aieo_create_sp_AIEO_UpdateCustomerCentricStdStats_Smart($sp_prefix, $sp_orders_table);
    aieo_create_sp_AIEO_UpdateCustomerCentricAdvStats($sp_prefix, $sp_orders_table);
    aieo_create_sp_AIEO_GenerateGraphDBUUIDs($sp_prefix);
    aieo_create_sp_AIEO_GenerateGraphDBUUIDFreqs($sp_prefix);
    aieo_create_sp_AIEO_PopulateProductNeeds($sp_prefix);
    // UNUSED - Never called in current codebase
    // aieo_create_sp_AIEO_Orchestrate_all_non_HPOS_SPs($sp_prefix);

    aieo_create_function_AIEO_StripHTML();
    aieo_create_function_AIEO_CountWords();
    aieo_create_function_AIEO_CountLinks();

}

function aieo_drop_stored_procedures($drop_tables = true)
{
    global $wpdb;

    // $drop_tables=false → refresh procedures/functions ONLY, leave every
    // table (and its data) in place. The version-bump upgrade path passes
    // false so a release no longer wipes the materialized working tables or
    // live session data (see aieo_check_for_plugin_update). Deactivation
    // still passes the default true and honours the per-table retention
    // options below.

    // The aieo_drop_sp_* installers live in the lazily-loaded SQL layer.
    aieo_load_sql_layer();

    aieo_debug_log('AIEO: Starting deactivation - aieo_drop_stored_procedures()');

    // Set aggressive timeouts
    set_time_limit(120);
    @ini_set('max_execution_time', 120);
    @ini_set('mysql.connect_timeout', 10);

    // Suppress errors during cleanup
    $wpdb->hide_errors();
    $wpdb->suppress_errors(true);

    $sp_prefix = get_option('aieo_site_prefix');

    // CRITICAL FIX: Construct table names directly using $wpdb->prefix (same as creation functions)
    // This ensures tables are properly dropped even if options are empty/outdated
    $sp_temp_orders_table_name = $wpdb->prefix . 'aieo_temp_orders';
    $sp_temp_store_orders_table_name = $wpdb->prefix . 'aieo_temp_store_orders';
    $sp_temp_products_table_name = $wpdb->prefix . 'aieo_temp_product_catalogue';
    $sp_core_product_attributes_table_name = $wpdb->prefix . 'aieo_core_product_attributes';
    $sp_core_product_category_hierarchy_table_name = $wpdb->prefix . 'aieo_core_product_category_hierarchy';
    $sp_core_category_brand_popularity_table_name = $wpdb->prefix . 'aieo_core_category_brand_popularity';
    $sp_core_product_vitals_table_name = $wpdb->prefix . 'aieo_core_product_vitals';
    $sp_core_variation_vitals_table_name = $wpdb->prefix . 'aieo_core_variation_vitals';
    $sp_core_reciprocal_products_table_name = $wpdb->prefix . 'aieo_core_reciprocal_products';
    $sp_core_brand_vitals_table_name = $wpdb->prefix . 'aieo_core_brand_vitals';
    $sp_ai_models_table_name = $wpdb->prefix . 'aieo_ai_models';
    $sp_performance_tuning_table_name = $wpdb->prefix . 'aieo_performance_tuning';

    aieo_debug_log('AIEO: Table names constructed. Prefix: ' . $wpdb->prefix);
    aieo_debug_log('AIEO: Will drop: ' . $sp_temp_orders_table_name);

    @aieo_drop_sp_AIEO_InsertCoreProductAttributes($sp_prefix);
    @aieo_drop_sp_AIEO_InsertProductCategoryHierarchy($sp_prefix);
    @aieo_drop_sp_AIEO_InsertProductVitals($sp_prefix);
    @aieo_drop_sp_AIEO_InsertVariationVitals($sp_prefix);
    @aieo_drop_sp_AIEO_CombineVariationVitalsInProductVitals($sp_prefix);
    @aieo_drop_sp_AIEO_UpdateOrdersWithUnsold($sp_prefix);
    @aieo_drop_sp_AIEO_FindDifferences($sp_prefix);
    @aieo_drop_sp_AIEO_Create_Product_Catalogue_HPOS($sp_prefix);
    @aieo_drop_sp_AIEO_Export_Orders_HPOS($sp_prefix);
    @aieo_drop_sp_AIEO_Export_Store_Orders($sp_prefix);
    @aieo_drop_sp_AIEO_Export_Orders_non_HPOS($sp_prefix);
    @aieo_drop_sp_AIEO_Merge_Store_Orders($sp_prefix);
    // UNUSED - Never called in current codebase
    // @aieo_drop_sp_AIEO_StripHTMLAndReplicateContentIntro($sp_prefix);
    @aieo_drop_sp_AIEO_RenameTable($sp_prefix);
    @aieo_drop_sp_AIEO_UpdateContentWordCount($sp_prefix);
    @aieo_drop_sp_AIEO_UpdateContentOutgoingLinks($sp_prefix);
    @aieo_drop_sp_AIEO_UpdateContentIntroPlain($sp_prefix);
    @aieo_drop_sp_AIEO_UpdateProductCentricStats($sp_prefix);
    // V1 statistics procedures - DEPRECATED - No longer created
    // @aieo_drop_sp_AIEO_UpdateCustomerCentricAdvStats($sp_prefix);
    @aieo_drop_sp_AIEO_UpdateCustomerCentricAdvStats($sp_prefix);
    @aieo_drop_sp_AIEO_GenerateGraphDBUUIDs($sp_prefix);
    @aieo_drop_sp_AIEO_GenerateGraphDBUUIDFreqs($sp_prefix);
    @aieo_drop_sp_AIEO_PopulateProductNeeds($sp_prefix);
    // @aieo_drop_sp_AIEO_Orchestrate_all_non_HPOS_SPs($sp_prefix);
    // @aieo_drop_sp_AIEO_UpdateCustomerCentricStdStats($sp_prefix);
    @aieo_drop_sp_AIEO_UpdateCustomerCentricStdStats_v2($sp_prefix);
    @aieo_drop_sp_AIEO_UpdateCustomerCentricStdStats($sp_prefix);

    // Drop legacy procedures that are no longer created but may exist from older versions
    @aieo_drop_sp_AIEO_Include_Products_Without_Sales();
    // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- DDL statement
    @$wpdb->query("DROP PROCEDURE IF EXISTS UpdateShortDescriptionForProduct");

    // Drop functions SECOND (they don't depend on tables)
    @aieo_drop_function_AIEO_StripHTML();
    @aieo_drop_function_AIEO_CountWords();
    @aieo_drop_function_AIEO_CountLinks();

    // Table drops are skipped entirely when $drop_tables is false (the
    // version-bump path) — procedures/functions above are refreshed, data
    // tables are preserved.
    if ($drop_tables) {

    // Drop tables LAST (after nothing references them)
    // These are "working" tables that are rebuilt on activation - safe to drop
    // SIMPLIFIED: Just drop tables directly - DROP TABLE automatically removes indexes
    // This avoids the slow index-by-index dropping that was causing hangs
    aieo_debug_log('AIEO: Dropping working tables (direct DROP TABLE)...');

    $tables_to_drop = array(
        $sp_core_product_attributes_table_name,
        $sp_core_product_category_hierarchy_table_name,
        $sp_core_category_brand_popularity_table_name,
        $sp_core_product_vitals_table_name,
        $sp_core_variation_vitals_table_name,
        $sp_core_reciprocal_products_table_name,
        $sp_core_brand_vitals_table_name,
        $sp_temp_orders_table_name,
        $sp_temp_store_orders_table_name,
        $sp_temp_products_table_name,
        $sp_ai_models_table_name,
        $sp_performance_tuning_table_name,
    );

    foreach ($tables_to_drop as $table) {
        aieo_debug_log('AIEO: Dropping table ' . $table);
        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- DDL statement with safe table name
        $wpdb->query("DROP TABLE IF EXISTS `{$table}`");
        aieo_debug_log('AIEO: Dropped ' . $table);
    }

    // Data retention based on user settings (configured in Chat Settings > Process Automation)
    // Default behavior: keep embeddings and chat history, delete session archives

    // Chat Embeddings table
    if (get_option('aieo_keep_embeddings_on_deactivation', '1') !== '1') {
        $embeddings_table = $wpdb->prefix . 'aieo_chat_embeddings';
        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- DDL statement with safe table name
        $wpdb->query("DROP TABLE IF EXISTS {$embeddings_table}");
        aieo_debug_log('AIEO: Dropped chat embeddings table (user preference)');
    }

    // Chat History table
    if (get_option('aieo_keep_chat_history_on_deactivation', '1') !== '1') {
        $chat_history_table = $wpdb->prefix . 'aieo_chat_history';
        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- DDL statement with safe table name
        $wpdb->query("DROP TABLE IF EXISTS {$chat_history_table}");
        aieo_debug_log('AIEO: Dropped chat history table (user preference)');
    }

    // Session Archive tables (default: DROP - not kept unless explicitly requested)
    if (get_option('aieo_keep_session_archive_on_deactivation', '0') !== '1') {
        $archive_table = $wpdb->prefix . 'aieo_sessions_archive';
        $archive_statics_table = $wpdb->prefix . 'aieo_sessions_statics_archive';
        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- DDL statement with safe table name
        $wpdb->query("DROP TABLE IF EXISTS {$archive_table}");
        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- DDL statement with safe table name
        $wpdb->query("DROP TABLE IF EXISTS {$archive_statics_table}");
        aieo_debug_log('AIEO: Dropped session archive tables (user preference)');
    }

    // Session Tracker tables (transient/live data)
    // Default: DROP - these are working tables that get rebuilt on activation
    // User can choose to keep them if they want to preserve session data across deactivations
    if (get_option('aieo_keep_sessions_on_deactivation', '0') !== '1') {
        $sessions_table = $wpdb->prefix . 'aieo_sessions';
        $sessions_statics_table = $wpdb->prefix . 'aieo_sessions_statics';
        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- DDL statement with safe table name
        $wpdb->query("DROP TABLE IF EXISTS {$sessions_table}");
        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- DDL statement with safe table name
        $wpdb->query("DROP TABLE IF EXISTS {$sessions_statics_table}");
        aieo_debug_log('AIEO: Dropped session tracker tables (user preference)');
    }

    aieo_debug_log('AIEO: Deactivation complete - tables handled per user data retention settings');

    } // end if ($drop_tables)

    // Re-enable errors
    $wpdb->show_errors();
    $wpdb->suppress_errors(false);
}

register_deactivation_hook(__FILE__, 'aieo_drop_stored_procedures');
register_activation_hook(__FILE__, 'aieo_create_stored_procedures');


/**
 * Version-gated DB migrations, folded in from the validated
 * BetaDistribution/db-migrations artifacts so a normal plugin update applies them
 * (keeping local dev + Apollo production aligned). Gated on the `aieo_db_version`
 * option vs AIEO_DB_VERSION; each step probes INFORMATION_SCHEMA / option state
 * before doing irreversible work, so it is safe to call on every activation and
 * version-bump. Wrapped in a connection-scoped GET_LOCK so concurrent PHP-FPM
 * workers don't run it twice (it auto-releases on connection close, so a fatal
 * mid-migration can't wedge it). See docs/DB-OPTIMIZATION-STAGED-2026-06.md.
 *
 *   v1 — #33  sessions index trim (drop left-prefix-redundant indexes)
 *   v2 — #34  reco reverse-ref table + one-time backfill + readiness flag
 *   v3 — #35  analytics atomic-swap aggregate procedures (force reinstall)
 *
 * @return void
 */
function aieo_run_db_migrations()
{
    global $wpdb;

    $target  = (int) AIEO_DB_VERSION;
    $current = (int) get_option('aieo_db_version', 0);
    if ($current >= $target) {
        return; // up to date — zero DB work on the hot path.
    }

    // Only one worker migrates; the rest bail. Auto-releases on connection close.
    $got_lock = $wpdb->get_var("SELECT GET_LOCK('aieo_db_migrations', 0)");
    if ($got_lock !== '1' && $got_lock !== 1) {
        return;
    }

    // Re-read inside the lock — another worker may have finished while we waited.
    $current = (int) get_option('aieo_db_version', 0);

    // Table/index creators + helpers live in the lazily-loaded SQL layer.
    if (function_exists('aieo_load_sql_layer')) {
        aieo_load_sql_layer();
    }

    // Each step advances aieo_db_version ONLY after its mandatory dependency was
    // available and the work actually ran — a missing dependency (partial deploy,
    // opcache, parse error) leaves the version unchanged so the step retries on the
    // next run instead of permanently marking incomplete work as done.

    // ---- v1 / #33 — sessions index trim ---------------------------------
    // idx_session_id and idx_product_id on wp_aieo_sessions(+ _archive) are pure
    // left-prefixes of wider composites (idx_session_product/idx_sequence cover
    // session_id; idx_product_event covers product_id), so dropping them removes
    // write amplification on the ~5s-per-visitor INSERT table with no read loss.
    // aieo_drop_index_if_exists() is INFORMATION_SCHEMA-guarded → idempotent.
    if ($current < 1) {
        if (function_exists('aieo_drop_index_if_exists')) {
            $trims = array(
                array($wpdb->prefix . 'aieo_sessions',         'idx_session_id'),
                array($wpdb->prefix . 'aieo_sessions',         'idx_product_id'),
                array($wpdb->prefix . 'aieo_sessions_archive', 'idx_session_id'),
                array($wpdb->prefix . 'aieo_sessions_archive', 'idx_product_id'),
            );
            foreach ($trims as $t) {
                aieo_drop_index_if_exists($t[1], $t[0]);
            }
            update_option('aieo_db_version', 1, false);
            $current = 1;
        }
        // else: SQL layer not loaded — leave at 0, retry next run.
    }

    // ---- v2 / #34 — reco reverse-ref table + RESUMABLE background backfill ----
    // Create the indexed reverse map, then hand the (potentially large) backfill
    // to a resumable background job (aieo_reco_reverse_refs_backfill). The readiness
    // flag aieo_reco_reverse_refs_ready is flipped by THAT job only when the
    // backfill is provably complete — never here — so the cache keeps using the
    // legacy LIKE path until the map is fully built. This avoids the
    // foreground-timeout / restart-loop failure mode of an inline whole-table walk.
    if ($current < 2) {
        if (function_exists('aieo_create_reco_reverse_refs_table')) {
            aieo_create_reco_reverse_refs_table();
            // Run one bounded slice now (small caches finish instantly) and let the
            // job reschedule itself for the remainder. Does NOT set the ready flag
            // unless it completes in this slice.
            aieo_reco_reverse_refs_backfill_step();
            update_option('aieo_db_version', 2, false);
            $current = 2;
        }
        // else: reverse-ref creator unavailable — leave at 1, retry next run.
    }

    // ---- v3 / #35 — analytics atomic-swap aggregate procedures ----------
    // The two aggregate procs now build into a shadow table and publish with one
    // atomic RENAME (no empty-table window for dashboard readers). They are also
    // reinstalled from the .sql files on every analytics run, but force a refresh
    // here so the fix is live immediately after the upgrade.
    if ($current < 3) {
        if (!class_exists('AIEO_Session_Analytics_Processor')) {
            $proc_file = plugin_dir_path(__FILE__) . 'includes/class-session-analytics-processor.php';
            if (file_exists($proc_file)) {
                require_once $proc_file;
            }
        }
        if (class_exists('AIEO_Session_Analytics_Processor')) {
            AIEO_Session_Analytics_Processor::get_instance()->create_stored_procedures();
            update_option('aieo_db_version', 3, false);
            $current = 3;
        }
        // else: processor class unavailable — the procs self-heal via the analytics
        // reinstall path anyway; leave at 2 to retry.
    }

    $wpdb->query("SELECT RELEASE_LOCK('aieo_db_migrations')");
}

/**
 * Resumable, bounded backfill of the reco reverse-ref map (#34). Processes one
 * budgeted slice (rows / seconds) from the persisted high-water id, reschedules
 * itself while rows remain, and flips aieo_reco_reverse_refs_ready ONLY when the
 * whole table is done. Hooked on the aieo_reco_reverse_refs_backfill cron event
 * AND called once inline by the migration so small caches finish immediately.
 * Serialized by a connection-scoped GET_LOCK so the inline call + a cron tick (or
 * two ticks) never double-process.
 *
 * @return void
 */
function aieo_reco_reverse_refs_backfill_step()
{
    global $wpdb;

    if (get_option('aieo_reco_reverse_refs_ready') === '1') {
        return; // already complete
    }
    if (!class_exists('AIEO_Recommendations_Cache')) {
        $cache_file = plugin_dir_path(__FILE__) . 'includes/class-recommendations-cache.php';
        if (file_exists($cache_file)) {
            require_once $cache_file;
        }
    }
    if (!class_exists('AIEO_Recommendations_Cache')) {
        return; // can't backfill yet — retry on the next scheduled tick
    }

    $got = $wpdb->get_var("SELECT GET_LOCK('aieo_reco_backfill', 0)");
    if ($got !== '1' && $got !== 1) {
        return; // another worker is backfilling
    }

    $start = (int) get_option('aieo_reco_reverse_refs_backfill_last_id', 0);
    // Budget per slice: 3000 rows or 8s, comfortably under any FPM/cron timeout.
    $res = AIEO_Recommendations_Cache::backfill_reverse_refs($start, 3000, 8);
    update_option('aieo_reco_reverse_refs_backfill_last_id', (int) $res['last_id'], false);

    if (!empty($res['done'])) {
        // Autoloaded scalar — read on the set()/cold-build path, so keep it free.
        update_option('aieo_reco_reverse_refs_ready', '1', true);
        delete_option('aieo_reco_reverse_refs_backfill_last_id');
    } elseif (!wp_next_scheduled('aieo_reco_reverse_refs_backfill')) {
        wp_schedule_single_event(time() + 30, 'aieo_reco_reverse_refs_backfill');
    }

    $wpdb->query("SELECT RELEASE_LOCK('aieo_reco_backfill')");
}
add_action('aieo_reco_reverse_refs_backfill', 'aieo_reco_reverse_refs_backfill_step');

// Decouple the DB-migration gate from the plugin VERSION bump: also run it on
// admin_init whenever aieo_db_version is behind AIEO_DB_VERSION, so a future
// db-version-only bump applies without needing AIEO_VERSION to increase. Cheap —
// aieo_run_db_migrations() early-returns (one option read) once up to date, and
// this never touches storefront requests.
add_action('admin_init', 'aieo_run_db_migrations');

/**
 * Bootstrap the session-analytics pipeline (journeys -> product metrics) ONLY on
 * cron / admin / WP-CLI requests, so storefront pageviews never load the
 * processor. On cron it registers the job handlers (so WP-Cron can fire the
 * scheduled events); in admin it adds the Session Analytics viewer + its rebuild
 * AJAX; on all three it re-ensures the events are scheduled.
 *
 * @return void
 */
function aieo_bootstrap_session_analytics() {
    if (!wp_doing_cron() && !is_admin() && !(defined('WP_CLI') && WP_CLI)) {
        return;
    }
    if (!class_exists('AIEO_Session_Analytics_Processor')) {
        $f = plugin_dir_path(__FILE__) . 'includes/class-session-analytics-processor.php';
        if (file_exists($f)) {
            require_once $f;
        }
    }
    if (class_exists('AIEO_Session_Analytics_Processor')) {
        AIEO_Session_Analytics_Processor::bootstrap();
    }
}
add_action('plugins_loaded', 'aieo_bootstrap_session_analytics', 20);


function aieo_check_for_plugin_update()
{
    $stored_version = get_option('aieo_version', false);

    if ($stored_version === false) {
        update_option('aieo_version', AIEO_VERSION);
        return;
    }

    if (version_compare(AIEO_VERSION, $stored_version, '>')) {
        global $wpdb;
        // Atomic, connection-scoped lock. This hook fires on every
        // plugins_loaded; during an in-place update many PHP-FPM workers see
        // the version mismatch at once. The old transient guard was
        // check-then-set, so two workers could BOTH pass get_transient()
        // before either set it and run the update in parallel (interleaved
        // DROP/CREATE → "already exists" / "Duplicate entry" log spam).
        // GET_LOCK(…, 0) returns 1 only to the first worker; the rest get 0
        // and bail. It auto-releases when the DB connection closes, so a fatal
        // mid-update can't wedge it (the transient persisted 5 minutes).
        $got_lock = $wpdb->get_var("SELECT GET_LOCK('aieo_plugin_update', 0)");
        if ($got_lock !== '1' && $got_lock !== 1) {
            return;
        }

        // Double-checked lock: re-read the version INSIDE the lock. A worker that
        // saw the old version before another worker committed the bump would
        // otherwise re-run the whole drop/recreate after the lock frees. Re-check
        // and bail if we're no longer behind.
        if (!version_compare(AIEO_VERSION, (string) get_option('aieo_version', '0'), '>')) {
            $wpdb->query("SELECT RELEASE_LOCK('aieo_plugin_update')");
            return;
        }

        // Refresh the bundled companion mu-plugins (autoloader, AS core,
        // AJAX query protection). On upgrade-in-place via the WP admin
        // uploader, register_activation_hook does NOT re-fire, so the
        // copy step we register on activation is skipped. Re-running it
        // here on version-bump covers that path. Function is idempotent
        // — it mtime-compares before copying and skips conflicting peers.
        if (function_exists('aieo_install_bundled_mu_plugins')) {
            aieo_install_bundled_mu_plugins();
        }

        // Refresh the stored PROCEDURES/FUNCTIONS only — pass false so the
        // materialized working tables (core_*_vitals, temp_*, ai_models) and
        // the LIVE session tables are NOT dropped on a version bump. The old
        // unconditional drop wiped the storefront's read substrate (and live
        // sessions, default keep=0) on every single release, leaving the shop
        // degraded until the 15–25 min data-prep pipeline repopulated it.
        // Schema changes to those tables go through their own versioned
        // migrations, not a blanket drop.
        aieo_drop_stored_procedures(false);
        // CREATE TABLE IF NOT EXISTS — creates anything missing, leaves
        // existing tables (and their data) untouched.
        aieo_create_temp_tables_activation_hook();
        aieo_create_stored_procedures();

        // Wishlist table — ensure it exists on every version bump
        // (covers 4.x → 5.0 upgrades-in-place where the activation
        // hook never fires).
        if (class_exists('AIEO_Wishlist')) {
            AIEO_Wishlist::install();
            update_option(AIEO_Wishlist::SCHEMA_OPTION, AIEO_Wishlist::SCHEMA_VERSION, false);
        }

        // Ensure initial activation date is set for existing installations upgrading to 3.3.0+
        if (get_option('aieo_initial_activation_date') === false) {
            add_option('aieo_initial_activation_date', current_time('mysql', true), '', 'no');
        }

        // Then update the version value in the database
        update_option('aieo_version', AIEO_VERSION);

        // Release the advisory lock. (Future calls short-circuit on the
        // version check above anyway; the lock would also auto-release on
        // connection close, but explicit release returns it immediately.)
        $wpdb->query("SELECT RELEASE_LOCK('aieo_plugin_update')");
    }
}
add_action('plugins_loaded', 'aieo_check_for_plugin_update');

/**
 * One-time autoload tuning (Phase-1 perf, 2026-06-13; bump the flag value to
 * re-run after changing the lists).
 *
 * The plugin seeds all its options with autoload='no'
 * (aieo_set_default_options), yet ~30 of them are read on EVERY request —
 * tracker toggles, IAPI flags, schema versions, perpetual migration flags,
 * display toggles. Without a persistent object cache each one is its own
 * `SELECT option_value …` round-trip per page view. Flipping these small
 * scalars to autoload='yes' folds them into the single alloptions query.
 *
 * Conversely, the JWT/auth + export-status payloads were written with
 * update_option() DEFAULT autoload (= yes), permanently bloating alloptions
 * with full HTTP response bodies. Those flip to 'no' (their write sites now
 * pass $autoload=false too).
 *
 * Runs once from admin_init (never costs the frontend anything); fresh
 * installs pick it up on the first wp-admin visit after activation.
 */
function aieo_autoload_tuning_v1() {
    if (get_option('aieo_autoload_tuning_done') === 'v1') {
        return;
    }
    if (!function_exists('wp_set_option_autoload_values')) {
        return; // WP < 6.4 — skip; behavior simply stays as it was.
    }

    $hot_small_scalars = array(
        // Version / perpetual flags read at plugins_loaded on every request.
        'aieo_version',
        'aieo_post_core_upgrade_purge_v1',
        'aieo_om_ppe_legacy_migration_v1',
        'aieo_chat_split_loader',
        // Session tracker — enqueue gate + loader config, every page view.
        // (custom_image_click_rules_js is usually empty; operator-authored
        // rules ride along in alloptions — acceptable, they're emitted into
        // the page anyway.)
        'aieo_session_tracking_enabled',
        'aieo_session_bypass_consent',
        'aieo_session_tracking_require_consent',
        'aieo_session_tracking_default_consent',
        'aieo_session_flush_interval',
        'aieo_track_scroll_depth',
        'aieo_track_clicks',
        'aieo_throttle_clicks',
        'aieo_track_mouse_movement',
        'aieo_session_tracker_version',
        'aieo_custom_image_click_rules_js',
        // Recommendations display toggles — frontend constructor reads.
        'aieo_display_cross_sells',
        'aieo_display_recently_viewed_products',
        'aieo_display_related',
        'aieo_display_same_needs',
        'aieo_display_upsells',
        'aieo_recommendations_display_order',
        // IAPI master + per-module flags — up to 10 reads per page view in
        // aieo_runtime_should_load_iapi().
        'roosterx_iapi_enabled',
        'aieo_dmm_pricing_iapi_enabled',
        'aieo_dmm_wishlist_iapi_enabled',
        'aieo_dmm_waitlist_iapi_enabled',
        'aieo_dmm_complementary_iapi_enabled',
        'aieo_dmm_reco_iapi_enabled',
        'aieo_dmm_ratings_iapi_enabled',
        'aieo_dmm_swatches_iapi_enabled',
        'aieo_dmm_sticky_atc_iapi_enabled',
        'aieo_dmm_side_cart_iapi_enabled',
        // Swatches runtime kill-switch — checked on every frontend request.
        'aieo_dmm_swatches_runtime_enabled',
        // Schema-version stamps checked at plugins_loaded by frontend-loaded
        // modules (admin-only modules' stamps intentionally left alone).
        'aieo_reco_events_schema_version',
        'aieo_effective_prices_schema_version',
        'aieo_effective_eligibility_schema_version',
        'aieo_effective_recos_schema_version',
        'aieo_recompute_queue_schema_version',
        'aieo_wishlist_schema_version',
        'aieo_ratings_schema_version',
        'aieo_attribution_schema_version',
        'aieo_waitlist_schema_version',
        'aieo_push_schema_version',
    );

    $blobs_out_of_alloptions = array(
        'aieo_jwt_response',
        'aieo_jwt_user_details',
        'aieo_jwt_debugging',
        'aieo_export_orders_message',
    );

    $values = array();
    foreach ($hot_small_scalars as $k) {
        $values[$k] = true;
    }
    foreach ($blobs_out_of_alloptions as $k) {
        $values[$k] = false;
    }
    // Only flips rows that exist; missing options are untouched (they adopt
    // the right autoload when first written).
    wp_set_option_autoload_values($values);

    update_option('aieo_autoload_tuning_done', 'v1', true);
}
add_action('admin_init', 'aieo_autoload_tuning_v1');

/**
 * Purge every AIEO-owned cache when WordPress core is upgraded.
 *
 * Bug context (NEXT_RELEASE_PRIORITY_BUGS.md issue #3, 2026-05-21):
 * after WP 7.0 auto-upgrade on femme-fatale.gr (prod1), every product
 * URL that carried a `?attribute_pa_<name>=<value>` query string
 * fatalled at `wp-includes/theme.php:325` (the
 * `apply_filters('template', get_option('template'))` line inside
 * `get_template()`) with the integer-underflow signature `tried to
 * allocate 4295229440 bytes (≈ 0xFFFFFFFE)`. That number is the
 * classic unsigned-underflow value PHP `unserialize()` returns when
 * it reads a corrupted length field — and the canonical way to
 * corrupt a length field is to unserialize an object whose class
 * has CHANGED SHAPE (different property set) or been REMOVED
 * between the time the blob was stored and the time it's read.
 *
 * The WP 7.0 upgrade replaces enough of `wp-includes/` that any
 * cached blob containing serialised WP/WC core class instances
 * (WP_Term, WP_Post, WC_Product, etc.) becomes a landmine. WC has
 * its own variation-prices cache (`wc_var_prices_*` transients);
 * AIEO has 81+ get_transient/wp_cache_get call sites across the
 * plugin, several of which page-cache results that may contain
 * core-class instances directly or transitively.
 *
 * Strategy: on WP-core upgrade fire-and-forget delete every AIEO
 * transient + every WC variation-prices transient + clear the
 * `aieo_dmm` wp_cache group + truncate AIEO_Recommendations_Cache.
 * Costs one DELETE on `wp_options` (with an indexed LIKE on
 * option_name) per upgrade — negligible compared to the OOM risk.
 *
 * Hooked on `upgrader_process_complete` with `$hook_extra['type']`
 * filter so the purge only fires for core upgrades; plugin and
 * theme upgrades use AIEO's own version-bump path (the
 * `aieo_check_for_plugin_update` above) instead.
 *
 * @param WP_Upgrader $upgrader_object
 * @param array       $hook_extra
 */
function aieo_purge_caches_on_core_upgrade($upgrader_object, $hook_extra) {
    if (!is_array($hook_extra)) { return; }
    $type = isset($hook_extra['type']) ? (string) $hook_extra['type'] : '';
    if ($type !== 'core') { return; }

    aieo_purge_all_persistent_caches('upgrader_process_complete:core');
}
add_action('upgrader_process_complete', 'aieo_purge_caches_on_core_upgrade', 10, 2);

/**
 * One-shot retroactive purge. Catches sites where WP core has ALREADY
 * been auto-upgraded BEFORE this fix landed in the plugin — i.e. the
 * upgrader_process_complete hook above can't help because it fires at
 * upgrade time, not at plugin-update time.
 *
 * Idempotent — guarded by `aieo_post_core_upgrade_purge_v1`. The
 * plugin's own version-bump path triggers this on the first
 * page load after 5.1 → next version lands (and on any subsequent
 * version bump, since the option is per-version).
 */
function aieo_retroactive_post_core_upgrade_purge() {
    $flag_key = 'aieo_post_core_upgrade_purge_v1';
    if (get_option($flag_key) === 'done') { return; }
    aieo_purge_all_persistent_caches('retroactive_one_shot');
    update_option($flag_key, 'done', false);
}
add_action('plugins_loaded', 'aieo_retroactive_post_core_upgrade_purge', 11);

/**
 * Implementation shared by the upgrader hook + the retroactive
 * one-shot. Deletes by option_name prefix on `wp_options` (no
 * intermediate get_option calls — direct DB query keeps the purge
 * O(rows) rather than O(rows × deserialise-cost)).
 *
 * Scope:
 *   - `_transient_aieo_*` + `_transient_timeout_aieo_*`
 *   - `_transient_wc_var_prices_*` + matching timeouts (WC's own
 *     variation-prices cache; touched by AIEO_DMM_Main_Price_Filter +
 *     AIEO_DMM_Role_Pricing via their `woocommerce_get_variation_prices_hash`
 *     filters, so corrupted blobs there transitively land in AIEO's
 *     code path)
 *   - object cache group `aieo_dmm` (if `wp_cache_flush_group` is available)
 *   - `wp_aieo_recommendations_cache` table TRUNCATE (if present)
 *
 * @param string $reason  Free-text breadcrumb for the error log.
 */
function aieo_purge_all_persistent_caches($reason = '') {
    global $wpdb;

    // 1. Transients — direct DELETE keyed on option_name. Both the
    //    blob row and its matching `_timeout_` row.
    if (isset($wpdb) && is_object($wpdb)) {
        $wpdb->query(
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- literal prefix; no user input; bypasses cache by design.
            "DELETE FROM {$wpdb->options}
              WHERE option_name LIKE '\\_transient\\_aieo\\_%' ESCAPE '\\\\'
                 OR option_name LIKE '\\_transient\\_timeout\\_aieo\\_%' ESCAPE '\\\\'
                 OR option_name LIKE '\\_transient\\__aieo\\_%' ESCAPE '\\\\'
                 OR option_name LIKE '\\_transient\\_timeout\\__aieo\\_%' ESCAPE '\\\\'
                 OR option_name LIKE '\\_transient\\_wc\\_var\\_prices\\_%' ESCAPE '\\\\'
                 OR option_name LIKE '\\_transient\\_timeout\\_wc\\_var\\_prices\\_%' ESCAPE '\\\\'"
        );
    }

    // 2. Object cache group used by AIEO_DMM cached lookups
    //    (product-needs / ratings / brand-vitals etc.).
    if (function_exists('wp_cache_flush_group')) {
        wp_cache_flush_group('aieo_dmm');
        wp_cache_flush_group('aieo_core_brand_vitals');
        wp_cache_flush_group('aieo_ratings');
    } elseif (function_exists('wp_cache_flush')) {
        // Older WP without group-scoped flush. Defensive full flush;
        // happens at most once per WP core upgrade, so acceptable
        // tradeoff against a hard OOM on the next request.
        wp_cache_flush();
    }

    // 3. Recommendations cache table — full truncate when present.
    //    The table stores serialised reco DTOs that could include
    //    WC_Product references; same unserialise-landmine risk.
    if (isset($wpdb) && is_object($wpdb)) {
        $table = $wpdb->prefix . 'aieo_recommendations_cache';
        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- literal table name; SHOW TABLES guard above prevents fatal on installs that don't ship the table.
        if ((string) $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table)) === $table) {
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- literal table name verified by SHOW TABLES.
            $wpdb->query("TRUNCATE TABLE {$table}");
        }
    }

    // 4. Bump any internal version counters the chat / brand-vitals
    //    layers use to invalidate their in-process memoisation on
    //    the NEXT request.
    if (class_exists('AIEO_Core_Brand_Vitals') && method_exists('AIEO_Core_Brand_Vitals', 'bump_version')) {
        try { AIEO_Core_Brand_Vitals::instance()->bump_version(); } catch (Throwable $e) {}
    }

    if (defined('WP_DEBUG') && WP_DEBUG && function_exists('aieo_debug_log')) {
        aieo_debug_log('AIEO cache purge fired: ' . $reason);
    }
}

/**
 * One-shot migration: copy legacy `om_ppe_*` options into their AIEO
 * counterparts so the runtime stops reading both. Without this, a
 * legacy record with `show_sku:false` / `show_barcode:false` shadows
 * the AIEO defaults forever, and the module never renders even after
 * the user toggles it on from the Product Display dashboard (which
 * writes only `{enabled:true}`).
 *
 * Merge rule: AIEO wins where it has a value, legacy fills gaps. The
 * `enabled` key is explicitly carried over from AIEO so the toggle
 * the user just flipped isn't reverted to the legacy disabled state.
 * After migration, the legacy option is deleted and a done-flag is
 * set so this never runs again.
 *
 * Idempotent — guarded by `aieo_om_ppe_legacy_migration_v1`.
 */
function aieo_migrate_om_ppe_legacy_options() {
    if (get_option('aieo_om_ppe_legacy_migration_v1') === 'done') { return; }

    $pairs = array(
        'aieo_dmm_product_meta_settings'  => 'om_ppe_product-meta',
        'aieo_dmm_payment_logos_settings' => 'om_ppe_payment-logos',
        'aieo_dmm_trust_badges_settings'  => 'om_ppe_trust-badges',
        'aieo_dmm_product_labels_settings'=> 'om_ppe_product-labels',
        'aieo_dmm_product_video_settings' => 'om_ppe_product-video',
        'aieo_dmm_badges_settings'        => 'om_ppe_badges',
        'aieo_dmm_add_to_cart_text_settings' => 'om_ppe_add-to-cart-text',
    );

    foreach ($pairs as $new_key => $legacy_key) {
        $legacy = get_option($legacy_key, false);
        if (!is_array($legacy)) { continue; }

        $new = get_option($new_key, array());
        if (!is_array($new)) { $new = array(); }

        // Two cases:
        //   (a) AIEO key has never been written → seed it from legacy
        //       so users who had OM PPE configured don't lose their
        //       labels/colours/custom_class.
        //   (b) AIEO key has been written (even just `{enabled:true}`
        //       from the dashboard toggle) → AIEO is now the source
        //       of truth; legacy is stale. Carrying legacy's boolean
        //       OPT-IN flags (show_sku/show_barcode) forward would
        //       silently shadow the AIEO defaults and the module
        //       would never render even though the user just enabled
        //       it. So in case (b) we simply delete the legacy and
        //       leave AIEO alone — defaults from get_settings() take
        //       over for missing keys.
        if (empty($new)) {
            update_option($new_key, $legacy, false);
        }
        delete_option($legacy_key);
    }

    update_option('aieo_om_ppe_legacy_migration_v1', 'done', false);
}
add_action('plugins_loaded', 'aieo_migrate_om_ppe_legacy_options', 12);

// Load text domain on init for normal page loads (WordPress 4.6+ auto-loads, but this is a fallback)
// Note: For AJAX requests, text domain is loaded at the top of this file before any classes load
add_action( 'init', function() {
    // phpcs:ignore PluginCheck.CodeAnalysis.DiscouragedFunctions.load_plugin_textdomainFound -- Fallback for non-WordPress.org hosted translations
    load_plugin_textdomain(
        'ai-eshop-optimizer',
        false,
        dirname( plugin_basename( __FILE__ ) ) . '/languages'
    );
}, 0 );


function aieo_initialize_aieo_recommendations()
{
    new aieo_display_recommendations();
}
add_action('plugins_loaded', 'aieo_initialize_aieo_recommendations');

/**
 * AIEO Table Rate Shipping — admin fallback registration.
 *
 * The runtime plugin owns this filter when present (see
 * ai-eshop-optimizer-runtime/ai-eshop-optimizer-runtime.php:158). Admin
 * registers it as a fallback so the storefront still gets the
 * `aieo_table_rate` method type — and therefore real rates for every zone
 * instance with method_id=aieo_table_rate — when an operator deactivates
 * the runtime plugin or the runtime is filtered out by an external loader.
 *
 * The class file `class-aieo-shipping-table-rate.php` lives in runtime's
 * includes/ but the mu-plugins autoloader is file-based and resolves it
 * regardless of which plugin is "active" in wp_options — so no class file
 * needs to be duplicated into admin. Only this filter registration needs
 * the gated fallback.
 *
 * The COD / free-shipping `AIEO_RUNTIME_OWNS_AJAX` gating pattern: runtime
 * defines the constant at file-load; admin skips this registration when
 * the constant exists so we never get double registration.
 */
add_action('plugins_loaded', function () {
    if (defined('AIEO_RUNTIME_OWNS_AJAX')) { return; }
    add_filter('woocommerce_shipping_methods', static function ($methods) {
        $methods['aieo_table_rate'] = 'AIEO_Shipping_Table_Rate';
        return $methods;
    });
});

/**
 * AIEO Social Login — frontend display fallback registration.
 *
 * The OAuth backend (AIEO_SSO_Rest + providers) and the admin settings tab
 * (AIEO_DMM_Social_Login) live in core. The customer-facing button SURFACES
 * (login / register / checkout / my-account / newsletter), the
 * `aieo/sso-buttons-block`, and the My-Account "Connected accounts" tab are
 * OWNED by the runtime plugin when present — it defines
 * AIEO_RUNTIME_OWNS_SSO_DISPLAY. When the runtime is absent, core renders the
 * SAME surfaces from its own bundled copies so SSO works fully standalone
 * (the proven AIEO_RUNTIME_OWNS_* gated-fallback pattern, as with table-rate
 * shipping above).
 *
 * Wired here (not in AIEO_DMM_Social_Login) because that module is skipped on
 * frontend requests for perf — but the buttons must render on the frontend.
 */
add_action('plugins_loaded', function () {
    if (defined('AIEO_RUNTIME_OWNS_SSO_DISPLAY')) { return; } // runtime renders the SSO frontend
    $base = AIEO_DIR . 'includes/social-login/';
    // Shared provider/intent config (also dual-bundled in the runtime). The
    // autoloader prefers the runtime copy; when runtime is absent we require
    // core's so the display has its config source.
    if (!class_exists('AIEO_SSO_Buttons_Data') && file_exists($base . 'class-aieo-sso-buttons-data.php')) {
        require_once $base . 'class-aieo-sso-buttons-data.php';
    }
    foreach (array(
        'class-aieo-sso-buttons.php',
        'class-aieo-sso-surfaces.php',
        'class-aieo-sso-block.php',
        'class-aieo-sso-account-tab.php',
    ) as $f) {
        if (file_exists($base . $f)) { require_once $base . $f; }
    }
    if (class_exists('AIEO_SSO_Surfaces'))    { AIEO_SSO_Surfaces::register(); }
    if (class_exists('AIEO_SSO_Account_Tab')) { AIEO_SSO_Account_Tab::register(); }
}, 11);

/**
 * AJAX handler to dismiss rotating banner
 */
function aieo_dismiss_banner_ajax_handler()
{
    // Verify nonce
    check_ajax_referer('aieo_dismiss_banner', 'nonce');

    // Get current user ID
    $user_id = get_current_user_id();

    if (!$user_id) {
        wp_send_json_error('User not logged in');
        return;
    }

    // Save dismissal preference
    update_user_meta($user_id, 'aieo_banner_dismissed', '1');

    wp_send_json_success('Banner dismissed');
}
add_action('wp_ajax_aieo_dismiss_banner', 'aieo_dismiss_banner_ajax_handler');

// Plugins-page row: direct "Settings" link to the AIEO admin page.
add_filter('plugin_action_links_' . plugin_basename(__FILE__), function ($links) {
    $url = admin_url('admin.php?page=ai-eshop-optimizer');
    $settings_link = '<a href="' . esc_url($url) . '">' . esc_html__('Settings', 'ai-eshop-optimizer') . '</a>';
    array_unshift($links, $settings_link);
    return $links;
});
