<?php

/*
Plugin Name: VMO AI Website Builder Client
Plugin URI: https://getvmo.com
Description: VMO client plugin for QuickLaunch, Direct WordPress, and staging installs. Routes provider work through the Kinsta Master/API authority and self-syncs entitlements, discounts, and updates.
Version: 1.0.0
Author: Jerreme Josephson LLC
Author URI: https://virtualmediaoffice.com
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Text Domain: vmo-ai-builder
Domain Path: /languages
Requires at least: 6.0
Requires PHP: 7.4
*/


// ===============================================================
// VMO CLIENT VERSION - Vultr client instances route through Kinsta
// ===============================================================
if (!defined('VMO_CLIENT_MODE')) {
    define('VMO_CLIENT_MODE', true);
}
if (!defined('VMO_MASTER_VERSION')) {
    define('VMO_MASTER_VERSION', false);
}
if (!defined('VMO_MASTER_PROXY_URL')) {
    define('VMO_MASTER_PROXY_URL', 'https://getvmo.com/wp-json/vmo-builder/v1/proxy');
}
if (!defined('VMO_CLIENT_DISTRIBUTION_CHANNEL')) {
    define('VMO_CLIENT_DISTRIBUTION_CHANNEL', 'direct_wordpress');
}

if (!function_exists('vmo_client_allowed_admin_pages')) {
    function vmo_client_allowed_admin_pages() {
        return ['vmo-ai-builder-main', 'vmo-ai-dashboard', 'vmo-my-plan', 'vmo-html-to-pdf'];
    }
}

if (!function_exists('vmo_client_is_allowed_admin_page')) {
    function vmo_client_is_allowed_admin_page($page) {
        return in_array($page, vmo_client_allowed_admin_pages(), true);
    }
}

add_action('admin_menu', function() {
    if (!defined('VMO_CLIENT_MODE') || VMO_CLIENT_MODE !== true) {
        return;
    }

    global $submenu;

    if (!empty($submenu['vmo-ai-builder-main']) && is_array($submenu['vmo-ai-builder-main'])) {
        foreach ($submenu['vmo-ai-builder-main'] as $index => $item) {
            $slug = $item[2] ?? '';
            if ($slug !== '' && !vmo_client_is_allowed_admin_page($slug)) {
                unset($submenu['vmo-ai-builder-main'][$index]);
            }
        }
        $submenu['vmo-ai-builder-main'] = array_values($submenu['vmo-ai-builder-main']);
    }

    foreach (['vmo-ai-settings', 'vmo-master-admin', 'vmo-twilio-settings'] as $restrictedSlug) {
        remove_submenu_page('vmo-ai-builder-main', $restrictedSlug);
        remove_submenu_page('vmo-dashboard', $restrictedSlug);
    }
}, 999);

add_action('admin_init', function() {
    if (!defined('VMO_CLIENT_MODE') || VMO_CLIENT_MODE !== true || !is_admin()) {
        return;
    }

    $page = isset($_GET['page']) ? sanitize_text_field(wp_unslash($_GET['page'])) : '';
    if ($page === '' || strpos($page, 'vmo') !== 0) {
        return;
    }

    if (vmo_client_is_allowed_admin_page($page)) {
        return;
    }

    wp_safe_redirect(admin_url('admin.php?page=vmo-ai-dashboard'));
    exit;
}, 1);

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

if (!defined('VMO_VERSION')) {
    define('VMO_VERSION', '1.0.29');
}

if (!defined('VMO_PROMOTION_FEATURE_ENABLED')) {
    define('VMO_PROMOTION_FEATURE_ENABLED', true);
}


// VMO_PART3_FULL_PLUGIN_CLIENT_WEBRTC_BRIDGE_v1
// Embedded Premium Live Voice client bridge. It auto-runs only on hosted
// QuickLaunch/client installs, leaving the master WebRTC behavior untouched.
$__vmo_premium_client_voice_bridge_file = __DIR__ . '/includes/vmo-premium-client-voice-bridge.php';
if (file_exists($__vmo_premium_client_voice_bridge_file)) {
    require_once $__vmo_premium_client_voice_bridge_file;
}
unset($__vmo_premium_client_voice_bridge_file);

// VMO_QUIET_FAVICON_404_v1
// Prevent a missing /favicon.ico request on fresh QuickLaunch tenants from
// showing as a red 404 console error during onboarding/plugin entry.
add_action('init', function () {
    $path = isset($_SERVER['REQUEST_URI']) ? (string) parse_url((string) $_SERVER['REQUEST_URI'], PHP_URL_PATH) : '';
    if ($path === '/favicon.ico') {
        status_header(204);
        nocache_headers();
        header('Content-Length: 0');
        exit;
    }
}, 0);

if (!function_exists('str_ends_with')) {
    function str_ends_with($haystack, $needle) {
        $haystack = (string) $haystack;
        $needle = (string) $needle;
        return $needle === '' || substr($haystack, -strlen($needle)) === $needle;
    }
}
// ── VMO Table Existence Cache ──
// Caches SHOW TABLES results in a static array for the request lifecycle.
// Eliminates ~200+ redundant SHOW TABLES queries per page load.

// ── VMO AI Rate Limiter ──
// Prevents excessive OpenAI API calls (cost protection + rate limit avoidance)
if (!function_exists('vmo_check_ai_rate_limit')) {
    function vmo_check_ai_rate_limit($user_id, $action = 'ai_call', $max_per_minute = 5, $max_per_hour = 30) {
        $minute_key = "vmo_rate_{$action}_{$user_id}_min";
        $hour_key   = "vmo_rate_{$action}_{$user_id}_hr";
        
        $minute_count = (int) get_transient($minute_key);
                $hour_count   = (int) get_transient($hour_key);
            // exit; // Commented out for debugging
        
        if ($minute_count >= $max_per_minute) {
            return new WP_Error('rate_limited', 'Too many AI requests. Please wait a moment before trying again.', ['retry_after' => 60]);
        }
        if ($hour_count >= $max_per_hour) {
            return new WP_Error('rate_limited', 'Hourly AI request limit reached. Please try again later.', ['retry_after' => 3600]);
        }
        
        // Increment counters
        set_transient($minute_key, $minute_count + 1, 60);
        set_transient($hour_key, $hour_count + 1, 3600);
        
        return true; // Allowed
    }
}

if (!function_exists('vmo_table_exists')) {
    function vmo_table_exists($table_name) {
        static $cache = [];
        if (isset($cache[$table_name])) {
            return $cache[$table_name];
        }
        global $wpdb;
        $result = (bool) $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table_name));
        $cache[$table_name] = $result;
        return $result;
    }
}

// ── WP_Filesystem helper ──
// Returns the global $wp_filesystem object, initializing it if needed.
// Used instead of file_put_contents / file_get_contents for WP.org compliance.
if (!function_exists('vmo_get_filesystem')) {
    function vmo_get_filesystem() {
        global $wp_filesystem;
        if (empty($wp_filesystem)) {
            require_once ABSPATH . 'wp-admin/includes/file.php';
            WP_Filesystem();
        }
        return $wp_filesystem;
    }
}

// ── Universal CRM Contact Helpers ──
if ( ! function_exists( 'vmo_crm_normalize_phone' ) ) {
    function vmo_crm_normalize_phone( $phone ): string {
        $digits = preg_replace( '/\D+/', '', (string) $phone );
        if ( strlen( $digits ) === 11 && strpos( $digits, '1' ) === 0 ) {
            $digits = substr( $digits, 1 );
        }
        return $digits ?: '';
    }
}

if ( ! function_exists( 'vmo_crm_normalize_tags' ) ) {
    function vmo_crm_normalize_tags( $tags ): string {
        if ( is_string( $tags ) ) {
            $tags = preg_split( '/[,|]/', $tags );
        }
        if ( ! is_array( $tags ) ) {
            return '';
        }
        $clean_tags = [];
        foreach ( $tags as $tag_value ) {
            $tag_value = sanitize_text_field( trim( (string) $tag_value ) );
            if ( $tag_value !== '' ) {
                $clean_tags[ strtolower( $tag_value ) ] = $tag_value;
            }
        }
        return implode( ',', array_values( $clean_tags ) );
    }
}

if ( ! function_exists( 'vmo_crm_valid_source' ) ) {
    function vmo_crm_valid_source( $source ): string {
        $source = sanitize_key( (string) $source );
        $source_map = [
            'form'              => 'website_form',
            'contact_form'      => 'website_form',
            'booking_form'      => 'website_form',
            'website'           => 'website_form',
            'website_visit'     => 'website_form',
            'sms'               => 'other',
            'payment'           => 'other',
            'stripe'            => 'other',
            'vmo_pay'           => 'other',
            'phone'             => 'phone_call',
            'call'              => 'phone_call',
            'email'             => 'email_marketing',
            'inbound_email'     => 'email_marketing',
            'outbound_email'    => 'email_marketing',
        ];
        if ( strpos( $source, 'form_' ) === 0 || substr( $source, -5 ) === '_form' ) {
            return 'website_form';
        }
        if ( isset( $source_map[ $source ] ) ) {
            return $source_map[ $source ];
        }
        $valid_sources = [ 'lead_generation', 'phone_call', 'manual_entry', 'website_form', 'social_media', 'email_marketing', 'referral', 'other' ];
        return in_array( $source, $valid_sources, true ) ? $source : 'other';
    }
}

if ( ! function_exists( 'vmo_crm_valid_lifecycle_stage' ) ) {
    function vmo_crm_valid_lifecycle_stage( $stage, $fallback = 'new' ): string {
        $stage = sanitize_key( (string) $stage );
        $valid_stages = [ 'new', 'engaged', 'nurturing', 'active_relationship', 'customer', 'dormant' ];
        return in_array( $stage, $valid_stages, true ) ? $stage : $fallback;
    }
}

if ( ! function_exists( 'vmo_crm_valid_level' ) ) {
    function vmo_crm_valid_level( $level, $fallback = 'medium' ): string {
        $level = sanitize_key( (string) $level );
        return in_array( $level, [ 'high', 'medium', 'low' ], true ) ? $level : $fallback;
    }
}

if ( ! function_exists( 'vmo_crm_flatten_list' ) ) {
    function vmo_crm_flatten_list( $value ): array {
        $items = [];
        if ( is_string( $value ) ) {
            $parts = preg_split( '/[,;\n|]+/', $value );
            foreach ( (array) $parts as $part ) {
                $clean = sanitize_text_field( trim( (string) $part ) );
                if ( $clean !== '' ) {
                    $items[] = $clean;
                }
            }
        } elseif ( is_array( $value ) ) {
            foreach ( $value as $entry ) {
                if ( is_array( $entry ) ) {
                    $name = $entry['name'] ?? $entry['title'] ?? $entry['label'] ?? $entry['segment'] ?? '';
                    if ( is_string( $name ) && trim( $name ) !== '' ) {
                        $items[] = sanitize_text_field( trim( $name ) );
                    } else {
                        $items = array_merge( $items, vmo_crm_flatten_list( array_values( $entry ) ) );
                    }
                } else {
                    $items = array_merge( $items, vmo_crm_flatten_list( $entry ) );
                }
            }
        }

        $deduped = [];
        foreach ( $items as $item ) {
            $key = strtolower( $item );
            if ( $key !== '' ) {
                $deduped[ $key ] = $item;
            }
        }
        return array_values( $deduped );
    }
}

if ( ! function_exists( 'vmo_crm_tag_key' ) ) {
    function vmo_crm_tag_key( $label ): string {
        $key = sanitize_key( str_replace( ' ', '_', (string) $label ) );
        return $key !== '' ? $key : 'general';
    }
}

if ( ! function_exists( 'vmo_crm_build_consultation_blueprint' ) ) {
    function vmo_crm_build_consultation_blueprint( int $user_id, $session_id, array $business_context, array $consultation_data = [], array $research_results = [] ): array {
        $business_profile = is_array( $consultation_data['business_profile'] ?? null ) ? $consultation_data['business_profile'] : [];
        $audience = is_array( $business_profile['audience'] ?? null ) ? $business_profile['audience'] : [];
        $identity = is_array( $business_profile['identity'] ?? null ) ? $business_profile['identity'] : [];
        $research_customers = is_array( $research_results['customers'] ?? null ) ? $research_results['customers'] : [];

        $business_type = sanitize_text_field( (string) ( $business_context['business_type'] ?? $identity['business_type'] ?? 'general_business' ) );
        $industry = sanitize_text_field( (string) ( $business_context['industry'] ?? $identity['industry'] ?? '' ) );
        $business_name = sanitize_text_field( (string) ( $business_context['business_name'] ?? $identity['business_name'] ?? '' ) );
        $location = sanitize_text_field( (string) ( $business_context['location'] ?? $identity['location'] ?? '' ) );

        $segments = array_merge(
            vmo_crm_flatten_list( $business_context['target_audience'] ?? [] ),
            vmo_crm_flatten_list( $audience['primary_segments'] ?? [] ),
            vmo_crm_flatten_list( $research_customers['segments'] ?? [] )
        );
        $segments = array_values( array_unique( array_filter( $segments ) ) );
        if ( empty( $segments ) ) {
            $segments = [ $industry ? $industry . ' prospects' : 'Qualified prospects' ];
        }

        $offers = array_merge(
            vmo_crm_flatten_list( $business_context['offerings'] ?? [] ),
            vmo_crm_flatten_list( $business_context['products_inventory'] ?? [] ),
            vmo_crm_flatten_list( $business_context['services_inventory'] ?? [] ),
            vmo_crm_flatten_list( $business_profile['offers'] ?? [] )
        );
        $offers = array_values( array_unique( array_filter( $offers ) ) );
        if ( empty( $offers ) ) {
            $offers = [ $business_type ? ucwords( str_replace( '_', ' ', $business_type ) ) : 'Core offering' ];
        }

        $type_lower = strtolower( $business_type . ' ' . $industry );
        $is_commerce = strpos( $type_lower, 'ecommerce' ) !== false || strpos( $type_lower, 'retail' ) !== false || strpos( $type_lower, 'store' ) !== false || strpos( $type_lower, 'shop' ) !== false;
        $is_service = ! $is_commerce || strpos( $type_lower, 'service' ) !== false || strpos( $type_lower, 'consult' ) !== false;

        $relationship_categories = [
            [ 'key' => 'lead', 'label' => 'Lead', 'description' => 'New contact with possible interest.', 'customer_evidence_required' => false ],
            [ 'key' => 'prospect', 'label' => 'Prospect', 'description' => 'Qualified opportunity being evaluated.', 'customer_evidence_required' => false ],
            [ 'key' => 'partner', 'label' => 'Partner', 'description' => 'Strategic, channel, or referral relationship.', 'customer_evidence_required' => false ],
            [ 'key' => 'vendor', 'label' => 'Vendor', 'description' => 'Supplier or operations relationship.', 'customer_evidence_required' => false ],
            [ 'key' => 'support', 'label' => 'Support Contact', 'description' => 'Person connected to an issue, ticket, or service request.', 'customer_evidence_required' => false ],
            [ 'key' => 'customer', 'label' => 'Customer', 'description' => 'Contact with purchase, paid invoice, completed order, or manual owner confirmation.', 'customer_evidence_required' => true ],
        ];
        if ( $is_commerce ) {
            $relationship_categories[] = [ 'key' => 'repeat_buyer', 'label' => 'Repeat Buyer', 'description' => 'Customer with repeat order potential.', 'customer_evidence_required' => true ];
            $relationship_categories[] = [ 'key' => 'wholesale_buyer', 'label' => 'Wholesale Buyer', 'description' => 'Bulk or account-based buyer.', 'customer_evidence_required' => true ];
        }
        if ( $is_service ) {
            $relationship_categories[] = [ 'key' => 'active_client', 'label' => 'Active Client', 'description' => 'Customer in active service delivery.', 'customer_evidence_required' => true ];
            $relationship_categories[] = [ 'key' => 'referral_source', 'label' => 'Referral Source', 'description' => 'Person or organization sending qualified opportunities.', 'customer_evidence_required' => false ];
        }

        $personas = [];
        foreach ( array_slice( $segments, 0, 8 ) as $segment ) {
            $personas[] = [
                'name' => $segment,
                'description' => 'Consultation-derived audience segment for ' . ( $business_name ?: 'this business' ) . '.',
                'recommended_tags' => [ 'segment_' . vmo_crm_tag_key( $segment ) ],
                'recommended_stage' => 'new',
                'follow_up_focus' => 'Qualify need, fit, timing, budget, and next action.',
            ];
        }

        $interest_categories = [];
        foreach ( array_slice( $offers, 0, 12 ) as $offer ) {
            $interest_categories[] = [
                'key' => 'interest_' . vmo_crm_tag_key( $offer ),
                'label' => $offer,
                'description' => 'Interest category derived from consultation offering data.',
            ];
        }

        $segment_tags = array_map( function( $segment ) {
            return 'segment_' . vmo_crm_tag_key( $segment );
        }, $segments );
        $interest_tags = array_map( function( $offer ) {
            return 'interest_' . vmo_crm_tag_key( $offer );
        }, $offers );

        return [
            'blueprint_id' => md5( $user_id . '|' . (string) $session_id . '|' . wp_json_encode( [ $business_type, $industry, $segments, $offers ] ) ),
            'user_id' => $user_id,
            'session_id' => sanitize_text_field( (string) $session_id ),
            'source' => 'consultation_initialization',
            'business' => [
                'name' => $business_name,
                'type' => $business_type,
                'industry' => $industry,
                'location' => $location,
            ],
            'relationship_categories' => $relationship_categories,
            'contact_types' => array_values( array_unique( array_map( function( $category ) { return $category['key']; }, $relationship_categories ) ) ),
            'lifecycle_stages' => [
                [ 'key' => 'new', 'label' => 'New', 'purpose' => 'Captured but not yet qualified.' ],
                [ 'key' => 'engaged', 'label' => 'Engaged', 'purpose' => 'Has responded, called, submitted a form, or opened a meaningful thread.' ],
                [ 'key' => 'nurturing', 'label' => 'Nurturing', 'purpose' => 'Needs education, timing, or repeat follow-up.' ],
                [ 'key' => 'active_relationship', 'label' => 'Active Relationship', 'purpose' => 'Active sales, support, partner, vendor, or service relationship.' ],
                [ 'key' => 'customer', 'label' => 'Customer', 'purpose' => 'Requires purchase evidence, paid invoice, completed order, or manual owner confirmation.' ],
                [ 'key' => 'dormant', 'label' => 'Dormant', 'purpose' => 'No meaningful activity recently.' ],
            ],
            'segments' => $segments,
            'personas' => $personas,
            'interest_categories' => $interest_categories,
            'recommended_tags' => array_values( array_unique( array_merge(
                [ 'consultation_initialized', 'website_form', 'phone_call', 'inbound_email', 'sms', 'payment_evidence', 'manual_entry', 'csv_import' ],
                $segment_tags,
                $interest_tags
            ) ) ),
            'capture_sources' => [ 'consultation', 'phone_call', 'email', 'sms', 'website_form', 'lead_discovery', 'payment_order', 'manual_entry', 'csv_import' ],
            'import_contract' => [
                'requires_mapping' => true,
                'supported_fields' => [ 'full_name', 'company', 'email', 'phone', 'address', 'city', 'state', 'zip', 'country', 'contact_type', 'lifecycle_stage', 'source', 'tags', 'notes' ],
                'duplicate_detection' => 'strong email and normalized phone matching before merge',
            ],
            'manual_entry_contract' => [
                'editable_fields' => [ 'full_name', 'company', 'email', 'phone', 'address', 'city', 'state', 'zip', 'country', 'contact_type', 'lifecycle_stage', 'source', 'tags', 'notes', 'follow_up_title', 'follow_up_date' ],
                'customer_rule' => 'Customer status is set only by purchase evidence, paid invoice/order evidence, or manual owner confirmation.',
            ],
            'conversion_rules' => [
                'customer_requires_evidence' => true,
                'valid_evidence' => [ 'manual', 'stripe_checkout_completed', 'invoice_paid', 'paid_invoice', 'order_completed', 'purchase_completed', 'confirmed_transaction' ],
                'disallowed_evidence' => [ 'call_intent', 'ai_guess', 'email_interest', 'form_interest', 'sms_interest' ],
            ],
            'follow_up_rules' => [
                [ 'trigger' => 'new_form_or_call', 'recommended_delay' => 'same_day', 'action' => 'Qualify and schedule next step.' ],
                [ 'trigger' => 'engaged_no_purchase', 'recommended_delay' => '2_days', 'action' => 'Send helpful follow-up tied to the expressed need.' ],
                [ 'trigger' => 'purchase_completed', 'recommended_delay' => '7_days', 'action' => 'Check satisfaction, ask for feedback, and identify retention or upsell path.' ],
                [ 'trigger' => 'dormant_relationship', 'recommended_delay' => '30_days', 'action' => 'Run reactivation or referral outreach.' ],
            ],
            'retention_logic' => [
                'watch_signals' => [ 'support tickets', 'missed follow-ups', 'refunds', 'inactive customers', 'repeat buying intervals' ],
                'recommended_actions' => [ 'satisfaction check', 'service review', 'renewal reminder', 'reorder reminder', 'referral ask' ],
            ],
            'creates_placeholder_contacts' => false,
            'initialized_at' => current_time( 'mysql' ),
            'last_updated' => current_time( 'mysql' ),
        ];
    }
}

if ( ! function_exists( 'vmo_crm_initialize_from_consultation' ) ) {
    function vmo_crm_initialize_from_consultation( int $user_id, $session_id, array $business_context, array $consultation_data = [], array $research_results = [] ): array {
        if ( $user_id <= 0 ) {
            return [];
        }

        $blueprint = vmo_crm_build_consultation_blueprint( $user_id, $session_id, $business_context, $consultation_data, $research_results );
        update_user_meta( $user_id, 'vmo_crm_blueprint', $blueprint );
        update_user_meta( $user_id, 'vmo_crm_structure', $blueprint );
        if ( (string) $session_id !== '' ) {
            update_user_meta( $user_id, 'vmo_crm_blueprint_' . sanitize_key( (string) $session_id ), $blueprint );
            update_user_meta( $user_id, 'vmo_crm_structure_' . sanitize_key( (string) $session_id ), $blueprint );
        }

        $legacy_config = [
            'user_id' => $user_id,
            'business_type' => $blueprint['business']['type'] ?? '',
            'business_context' => $business_context,
            'contact_categories' => $blueprint['relationship_categories'] ?? [],
            'customer_personas' => $blueprint['personas'] ?? [],
            'lead_stages' => $blueprint['lifecycle_stages'] ?? [],
            'recommended_tags' => $blueprint['recommended_tags'] ?? [],
            'auto_categorization' => true,
            'ai_learning_enabled' => true,
            'data_sources' => $blueprint['capture_sources'] ?? [],
            'crm_blueprint_id' => $blueprint['blueprint_id'] ?? '',
            'created_at' => $blueprint['initialized_at'] ?? current_time( 'mysql' ),
            'last_updated' => current_time( 'mysql' ),
        ];
        update_option( 'vmo_contact_management_' . $user_id, $legacy_config );

        return [
            'status' => 'ready',
            'blueprint_id' => $blueprint['blueprint_id'] ?? '',
            'creates_placeholder_contacts' => false,
            'relationship_categories' => count( $blueprint['relationship_categories'] ?? [] ),
            'segments' => $blueprint['segments'] ?? [],
            'interest_categories' => $blueprint['interest_categories'] ?? [],
            'stored_meta' => [ 'vmo_crm_blueprint', 'vmo_crm_structure' ],
            'updated_at' => current_time( 'mysql' ),
        ];
    }
}

if ( ! function_exists( 'vmo_crm_find_contact' ) ) {
    function vmo_crm_find_contact( int $user_id, array $identity ): array {
        global $wpdb;
        $contacts_table = $wpdb->prefix . 'vmo_contacts';
        if ( ! vmo_table_exists( $contacts_table ) ) {
            return [];
        }

        $contact_row = [];
        $numeric_id = isset( $identity['id'] ) ? absint( $identity['id'] ) : absint( $identity['contact_db_id'] ?? 0 );
        if ( $numeric_id > 0 ) {
            $contact_row = $wpdb->get_row( $wpdb->prepare(
                "SELECT * FROM {$contacts_table} WHERE user_id = %d AND id = %d LIMIT 1",
                $user_id,
                $numeric_id
            ), ARRAY_A );
            if ( is_array( $contact_row ) && $contact_row ) {
                return $contact_row;
            }
        }

        $contact_id = sanitize_text_field( $identity['contact_id'] ?? '' );
        if ( $contact_id !== '' ) {
            $contact_row = $wpdb->get_row( $wpdb->prepare(
                "SELECT * FROM {$contacts_table} WHERE user_id = %d AND contact_id = %s LIMIT 1",
                $user_id,
                $contact_id
            ), ARRAY_A );
            if ( is_array( $contact_row ) && $contact_row ) {
                return $contact_row;
            }
        }

        $email = sanitize_email( $identity['email'] ?? '' );
        if ( $email !== '' ) {
            $contact_row = $wpdb->get_row( $wpdb->prepare(
                "SELECT * FROM {$contacts_table} WHERE user_id = %d AND email = %s LIMIT 1",
                $user_id,
                $email
            ), ARRAY_A );
            if ( is_array( $contact_row ) && $contact_row ) {
                return $contact_row;
            }
        }

        $normalized_phone = vmo_crm_normalize_phone( $identity['phone'] ?? '' );
        if ( $normalized_phone !== '' ) {
            $raw_phone = sanitize_text_field( $identity['phone'] ?? '' );
            if ( $raw_phone !== '' ) {
                $contact_row = $wpdb->get_row( $wpdb->prepare(
                    "SELECT * FROM {$contacts_table} WHERE user_id = %d AND phone = %s LIMIT 1",
                    $user_id,
                    $raw_phone
                ), ARRAY_A );
                if ( is_array( $contact_row ) && $contact_row ) {
                    return $contact_row;
                }
            }

            $phone_tail = substr( $normalized_phone, -7 );
            $candidate_rows = $wpdb->get_results( $wpdb->prepare(
                "SELECT * FROM {$contacts_table} WHERE user_id = %d AND phone LIKE %s LIMIT 25",
                $user_id,
                '%' . $wpdb->esc_like( $phone_tail ) . '%'
            ), ARRAY_A );
            foreach ( (array) $candidate_rows as $candidate_row ) {
                if ( vmo_crm_normalize_phone( $candidate_row['phone'] ?? '' ) === $normalized_phone ) {
                    return $candidate_row;
                }
            }
        }

        return [];
    }
}

if ( ! function_exists( 'vmo_crm_find_possible_duplicates' ) ) {
    function vmo_crm_find_possible_duplicates( int $user_id, array $identity, int $limit = 5 ): array {
        global $wpdb;
        $contacts_table = $wpdb->prefix . 'vmo_contacts';
        if ( ! vmo_table_exists( $contacts_table ) ) {
            return [];
        }

        $full_name = sanitize_text_field( $identity['full_name'] ?? $identity['name'] ?? '' );
        $company = sanitize_text_field( $identity['company'] ?? '' );
        $city = sanitize_text_field( $identity['city'] ?? '' );
        $state = sanitize_text_field( $identity['state'] ?? '' );
        $limit = max( 1, min( 25, $limit ) );
        $columns = function_exists('vmo_crm_contact_columns') ? vmo_crm_contact_columns($contacts_table) : [];

        $clauses = [];
        $params = [ $user_id ];
        if ( $full_name !== '' && $company !== '' && in_array('full_name', $columns, true) && in_array('company', $columns, true) ) {
            $clauses[] = '(full_name = %s AND company = %s)';
            $params[] = $full_name;
            $params[] = $company;
        }
        if ( $company !== '' && $city !== '' && in_array('company', $columns, true) && in_array('city', $columns, true) ) {
            $clauses[] = '(company = %s AND city = %s)';
            $params[] = $company;
            $params[] = $city;
        }
        if ( $full_name !== '' && $city !== '' && in_array('full_name', $columns, true) && in_array('city', $columns, true) ) {
            $clauses[] = '(full_name = %s AND city = %s)';
            $params[] = $full_name;
            $params[] = $city;
        }
        if ( $company !== '' && $state !== '' && in_array('company', $columns, true) && in_array('state', $columns, true) ) {
            $clauses[] = '(company = %s AND state = %s)';
            $params[] = $company;
            $params[] = $state;
        }
        if ( empty( $clauses ) ) {
            return [];
        }

        $params[] = $limit;
        $order_column = in_array('updated_at', $columns, true) ? 'updated_at' : (in_array('created_at', $columns, true) ? 'created_at' : 'id');
        $sql = $wpdb->prepare(
            "SELECT * FROM {$contacts_table} WHERE user_id = %d AND (" . implode( ' OR ', $clauses ) . ") ORDER BY {$order_column} DESC LIMIT %d",
            $params
        );
        $rows = $wpdb->get_results( $sql, ARRAY_A );
        return is_array( $rows ) ? $rows : [];
    }
}

if ( ! function_exists( 'vmo_crm_upsert_contact' ) ) {
    function vmo_crm_upsert_contact( int $user_id, array $identity, array $context = [] ): array {
        global $wpdb;
        $contacts_table = $wpdb->prefix . 'vmo_contacts';
        if ( ! vmo_table_exists( $contacts_table ) ) {
            return [];
        }

        $data = array_merge( $identity, $context );
        $email = sanitize_email( $data['email'] ?? '' );
        $phone = sanitize_text_field( $data['phone'] ?? '' );
        $full_name = sanitize_text_field( $data['full_name'] ?? $data['name'] ?? '' );
        $company = sanitize_text_field( $data['company'] ?? '' );

        if ( $email === '' && $phone === '' && $full_name === '' && $company === '' ) {
            return [];
        }

        $first_name = sanitize_text_field( $data['first_name'] ?? '' );
        $last_name = sanitize_text_field( $data['last_name'] ?? '' );
        if ( $full_name !== '' && $first_name === '' ) {
            $name_parts = explode( ' ', $full_name, 2 );
            $first_name = $name_parts[0];
            $last_name = $name_parts[1] ?? '';
        }
        if ( $full_name === '' && $first_name !== '' ) {
            $full_name = trim( $first_name . ' ' . $last_name );
        }

        $existing_contact = vmo_crm_find_contact( $user_id, $data );
        $source = vmo_crm_valid_source( $data['source'] ?? 'manual_entry' );
        $lifecycle = vmo_crm_valid_lifecycle_stage( $data['lifecycle_stage'] ?? 'new' );
        $primary_category = sanitize_text_field( $data['primary_category'] ?? ( $lifecycle === 'customer' ? 'customer' : 'lead' ) );
        $tags = vmo_crm_normalize_tags( $data['tags'] ?? '' );
        $now = current_time( 'mysql' );
        $contact_columns = function_exists('vmo_crm_contact_columns') ? vmo_crm_contact_columns($contacts_table) : $wpdb->get_col( "SHOW COLUMNS FROM {$contacts_table}", 0 );
        $contact_columns = is_array( $contact_columns ) ? $contact_columns : [];
        if (empty($contact_columns)) {
            return [];
        }

        $acquisition_details = $data['acquisition_details'] ?? [];
        $lead_source_raw = $data['lead_source'] ?? ( $data['tier'] ?? '' );
        if ( $lead_source_raw === '' && is_array( $acquisition_details ) && ! empty( $acquisition_details['lead_source'] ) ) {
            $lead_source_raw = $acquisition_details['lead_source'];
        }
        $lead_source = sanitize_key( (string) $lead_source_raw );
        if ( ! in_array( $lead_source, [ 'primary', 'secondary', 'tertiary' ], true ) ) {
            $lead_source = '';
        }

        $lead_verified = null;
        if ( array_key_exists( 'lead_verified', $data ) ) {
            $lead_verified = ! empty( $data['lead_verified'] ) ? 1 : 0;
        } elseif ( is_array( $acquisition_details ) && array_key_exists( 'lead_verified', $acquisition_details ) ) {
            $lead_verified = ! empty( $acquisition_details['lead_verified'] ) ? 1 : 0;
        }

        $row = [
            'source'           => $source,
            'primary_category' => $primary_category ?: 'lead',
            'secondary_category' => sanitize_text_field( $data['secondary_category'] ?? '' ),
            'confidence_score' => max( 0, min( 100, intval( $data['confidence_score'] ?? 0 ) ) ),
            'priority_level'   => vmo_crm_valid_level( $data['priority_level'] ?? 'medium' ),
            'lifecycle_stage'  => $lifecycle,
            'first_name'       => $first_name ?: null,
            'last_name'        => $last_name ?: null,
            'full_name'        => $full_name ?: null,
            'email'            => $email ?: null,
            'phone'            => $phone ?: null,
            'company'          => $company ?: null,
            'job_title'        => sanitize_text_field( $data['job_title'] ?? '' ) ?: null,
            'address'          => sanitize_textarea_field( $data['address'] ?? '' ) ?: null,
            'city'             => sanitize_text_field( $data['city'] ?? '' ) ?: null,
            'state'            => sanitize_text_field( $data['state'] ?? '' ) ?: null,
            'zip'              => sanitize_text_field( $data['zip'] ?? '' ) ?: null,
            'country'          => sanitize_text_field( $data['country'] ?? '' ) ?: null,
            'lead_score'       => max( 0, min( 100, intval( $data['lead_score'] ?? 50 ) ) ),
            'engagement_level' => vmo_crm_valid_level( $data['engagement_level'] ?? 'medium' ),
            'value_potential'  => max( 0, min( 100, intval( $data['value_potential'] ?? 50 ) ) ),
            'tags'             => $tags ?: null,
            'notes'            => sanitize_textarea_field( $data['notes'] ?? '' ) ?: null,
            'last_contact'     => $now,
            'updated_at'       => $now,
        ];

        if ( $lead_source !== '' && in_array( 'lead_source', $contact_columns, true ) ) {
            $row['lead_source'] = $lead_source;
        }
        if ( $lead_verified !== null && in_array( 'lead_verified', $contact_columns, true ) ) {
            $row['lead_verified'] = $lead_verified;
        }

        if ( ! empty( $data['acquisition_details'] ) ) {
            $row['acquisition_details'] = is_string( $data['acquisition_details'] ) ? wp_kses_post( $data['acquisition_details'] ) : wp_json_encode( $data['acquisition_details'] );
        }
        if ( ! empty( $data['categorization_analysis'] ) ) {
            $row['categorization_analysis'] = is_string( $data['categorization_analysis'] ) ? wp_kses_post( $data['categorization_analysis'] ) : wp_json_encode( $data['categorization_analysis'] );
        }
        if ( ! empty( $data['engagement_strategy'] ) ) {
            $row['engagement_strategy'] = is_string( $data['engagement_strategy'] ) ? wp_kses_post( $data['engagement_strategy'] ) : wp_json_encode( $data['engagement_strategy'] );
        }
        if (!empty($contact_columns)) {
            $row = array_intersect_key($row, array_flip($contact_columns));
        }

        if ( $existing_contact ) {
            $update_row = [];
            if (in_array('last_contact', $contact_columns, true)) {
                $update_row['last_contact'] = $now;
            }
            if (in_array('updated_at', $contact_columns, true)) {
                $update_row['updated_at'] = $now;
            }
            foreach ( $row as $field_name => $field_value ) {
                if ( $field_value === null || $field_value === '' ) {
                    continue;
                }
                if ( $field_name === 'source' && ! empty( $existing_contact['source'] ) && $existing_contact['source'] !== 'other' ) {
                    continue;
                }
                if ( $field_name === 'lifecycle_stage' && ( $existing_contact['lifecycle_stage'] ?? '' ) === 'customer' && $field_value !== 'customer' ) {
                    continue;
                }
                if ( $field_name === 'primary_category' && ( $existing_contact['primary_category'] ?? '' ) === 'customer' && $field_value !== 'customer' ) {
                    continue;
                }
                if ( $field_name === 'tags' ) {
                    $merged_tags = vmo_crm_normalize_tags( array_merge(
                        explode( ',', (string) ( $existing_contact['tags'] ?? '' ) ),
                        explode( ',', (string) $field_value )
                    ) );
                    $update_row['tags'] = $merged_tags ?: null;
                    continue;
                }
                $update_row[ $field_name ] = $field_value;
            }
            $wpdb->update( $contacts_table, $update_row, [ 'id' => (int) $existing_contact['id'], 'user_id' => $user_id ] );
            $updated_contact = vmo_crm_find_contact( $user_id, [ 'id' => (int) $existing_contact['id'] ] );
            do_action( 'vmo_contact_updated', $user_id, (string) ( $updated_contact['contact_id'] ?? $existing_contact['contact_id'] ), $data );
            return $updated_contact ?: $existing_contact;
        }

        $provided_contact_id = sanitize_text_field( $data['contact_id'] ?? '' );
        $id_seed = $email ?: ( vmo_crm_normalize_phone( $phone ) ?: ( $full_name . '|' . $company . '|' . wp_rand( 1000, 9999 ) ) );
        $contact_uuid = $provided_contact_id !== '' ? $provided_contact_id : md5( strtolower( trim( $id_seed ) ) . '|' . $user_id );
        if ( vmo_crm_find_contact( $user_id, [ 'contact_id' => $contact_uuid ] ) ) {
            $contact_uuid = function_exists( 'wp_generate_uuid4' ) ? wp_generate_uuid4() : ( 'c_' . $user_id . '_' . time() . '_' . wp_rand( 1000, 9999 ) );
        }
        if (in_array('user_id', $contact_columns, true)) {
            $row['user_id'] = $user_id;
        }
        if (in_array('contact_id', $contact_columns, true)) {
            $row['contact_id'] = $contact_uuid;
        }
        if (in_array('created_at', $contact_columns, true)) {
            $row['created_at'] = $now;
        }
        $wpdb->insert( $contacts_table, $row );
        $inserted_id = (int) $wpdb->insert_id;
        $created_contact = $inserted_id ? vmo_crm_find_contact( $user_id, [ 'id' => $inserted_id ] ) : [];
        do_action( 'vmo_contact_created', $user_id, $contact_uuid, $data );
        return $created_contact;
    }
}

if ( ! function_exists( 'vmo_upsert_contact' ) ) {
    function vmo_upsert_contact( int $user_id, array $data ): string {
        $contact = vmo_crm_upsert_contact( $user_id, $data );
        return (string) ( $contact['contact_id'] ?? '' );
    }
}

// ── CRM Interaction Logger ──
if ( ! function_exists( 'vmo_log_interaction' ) ) {
    function vmo_log_interaction( int $user_id, string $contact_id, array $data ): bool {
        global $wpdb;
        $table = $wpdb->prefix . 'vmo_contact_interactions';
        if ( ! vmo_table_exists( $table ) ) return false;

        $valid_types = [ 'phone_call', 'email', 'meeting', 'website_visit', 'purchase', 'support_ticket', 'follow_up', 'automated_engagement', 'manual_note', 'other' ];
        $raw_type = sanitize_key( $data['interaction_type'] ?? $data['type'] ?? '' );
        $subtype = sanitize_key( $data['subtype'] ?? '' );
        if ( $raw_type === 'sms_campaign' ) {
            $type = 'automated_engagement';
            $subtype = $subtype ?: 'sms_campaign';
        } elseif ( in_array( $raw_type, $valid_types, true ) ) {
            $type = $raw_type;
        } else {
            $type = 'other';
            $subtype = $subtype ?: $raw_type;
        }

        $summary = $data['summary'] ?? $data['notes'] ?? '';
        $full_details = $data['full_details'] ?? null;
        if ( is_array( $full_details ) || is_object( $full_details ) ) {
            $full_details = wp_json_encode( $full_details );
        }
        $metadata = $data['metadata'] ?? null;
        if ( is_array( $metadata ) || is_object( $metadata ) ) {
            $metadata = wp_json_encode( $metadata );
        }
        $attachments = $data['attachments'] ?? null;
        if ( is_array( $attachments ) || is_object( $attachments ) ) {
            $attachments = wp_json_encode( $attachments );
        }

        $outcome = sanitize_key( $data['outcome'] ?? 'pending' );
        if ( $outcome === 'neutral' ) {
            $outcome = 'pending';
        }
        $valid_outcomes = [ 'successful', 'needs_follow_up', 'no_response', 'converted', 'lost', 'pending' ];
        $direction = sanitize_key( $data['direction'] ?? '' );

        $row = [
            'user_id'              => $user_id,
            'contact_id'           => $contact_id,
            'interaction_type'     => $type,
            'interaction_subtype'  => sanitize_text_field( $subtype ),
            'direction'            => in_array( $direction, [ 'inbound', 'outbound', 'bidirectional' ], true ) ? $direction : null,
            'subject'              => sanitize_text_field( $data['subject'] ?? '' ),
            'summary'              => sanitize_textarea_field( $summary ),
            'full_details'         => is_string( $full_details ) ? wp_kses_post( $full_details ) : null,
            'outcome'              => in_array( $outcome, $valid_outcomes, true ) ? $outcome : 'pending',
            'next_action'          => sanitize_text_field( $data['next_action'] ?? '' ),
            'follow_up_date'       => ! empty( $data['follow_up_date'] ) ? sanitize_text_field( $data['follow_up_date'] ) : null,
            'duration_minutes'     => ! empty( $data['duration_minutes'] ) ? intval( $data['duration_minutes'] ) : 0,
            'ai_analysis'          => ! empty( $data['ai_analysis'] ) ? wp_kses_post( is_string( $data['ai_analysis'] ) ? $data['ai_analysis'] : wp_json_encode( $data['ai_analysis'] ) ) : null,
            'satisfaction_score'   => isset( $data['satisfaction_score'] ) ? intval( $data['satisfaction_score'] ) : null,
            'conversion_value'     => ! empty( $data['conversion_value'] ) ? floatval( $data['conversion_value'] ) : null,
            'related_campaign'     => sanitize_text_field( $data['related_campaign'] ?? '' ),
            'attachments'          => is_string( $attachments ) ? wp_kses_post( $attachments ) : null,
            'metadata'             => is_string( $metadata ) ? wp_kses_post( $metadata ) : null,
            'created_at'           => current_time( 'mysql' ),
            'updated_at'           => current_time( 'mysql' ),
        ];

        $columns = $wpdb->get_col( "SHOW COLUMNS FROM {$table}", 0 );
        if ( is_array( $columns ) && $columns ) {
            $columns = array_map( 'strval', $columns );
            $row = array_intersect_key( $row, array_flip( $columns ) );
        }

        $inserted = (bool) $wpdb->insert( $table, $row );
        if ( $inserted && function_exists( 'vmo_record_attribution_event' ) ) {
            $attribution_type = $type;
            if ( $type === 'purchase' ) $attribution_type = 'purchase';
            if ( $type === 'phone_call' ) $attribution_type = 'phone_call';
            if ( in_array( $type, [ 'phone_call', 'email', 'purchase', 'support_ticket', 'automated_engagement' ], true ) ) {
                vmo_record_attribution_event( $user_id, $attribution_type, array_merge( is_array( $data ) ? $data : [], [
                    'contact_id' => $contact_id,
                    'related_call_id' => $type === 'phone_call' ? (int) $wpdb->insert_id : 0,
                    'conversion_value' => ! empty( $data['conversion_value'] ) ? (float) $data['conversion_value'] : 0,
                ] ) );
            }
        }
        return $inserted;
    }
}

if ( ! function_exists( 'vmo_crm_log_interaction' ) ) {
    function vmo_crm_log_interaction( int $user_id, string $contact_id, array $interaction ): bool {
        return vmo_log_interaction( $user_id, $contact_id, $interaction );
    }
}

if ( ! function_exists( 'vmo_crm_convert_to_customer' ) ) {
    function vmo_crm_convert_to_customer( int $user_id, $contact_reference, array $evidence = [] ): bool {
        global $wpdb;
        $contacts_table = $wpdb->prefix . 'vmo_contacts';
        if ( ! vmo_table_exists( $contacts_table ) ) {
            return false;
        }
        $contact = is_numeric( $contact_reference )
            ? vmo_crm_find_contact( $user_id, [ 'id' => absint( $contact_reference ) ] )
            : vmo_crm_find_contact( $user_id, [ 'contact_id' => sanitize_text_field( $contact_reference ) ] );
        if ( ! $contact ) {
            return false;
        }

        $evidence_type = sanitize_key( $evidence['type'] ?? '' );
        $allowed_evidence = [ 'manual', 'stripe_checkout_completed', 'invoice_paid', 'paid_invoice', 'order_completed', 'purchase_completed', 'confirmed_transaction' ];
        if ( ! in_array( $evidence_type, $allowed_evidence, true ) ) {
            return false;
        }

        $wpdb->update(
            $contacts_table,
            [
                'primary_category' => 'customer',
                'lifecycle_stage'  => 'customer',
                'lead_score'       => 100,
                'engagement_level' => 'high',
                'last_contact'     => current_time( 'mysql' ),
                'updated_at'       => current_time( 'mysql' ),
            ],
            [ 'id' => (int) $contact['id'], 'user_id' => $user_id ]
        );

        vmo_crm_log_interaction( $user_id, (string) $contact['contact_id'], [
            'interaction_type' => 'manual_note',
            'subtype'          => 'customer_conversion',
            'subject'          => 'Customer status confirmed',
            'summary'          => sanitize_textarea_field( $evidence['summary'] ?? 'Customer status confirmed with evidence.' ),
            'outcome'          => 'converted',
            'metadata'         => $evidence,
        ] );

        return true;
    }
}

if ( ! function_exists( 'vmo_crm_record_purchase' ) ) {
    function vmo_crm_record_purchase( int $user_id, array $buyer, array $purchase ): array {
        global $wpdb;
        $purchases_table = $wpdb->prefix . 'vmo_contact_purchases';
        if ( ! vmo_table_exists( $purchases_table ) ) {
            return [];
        }

        $contact = vmo_crm_upsert_contact( $user_id, $buyer, [
            'source'           => $buyer['source'] ?? 'other',
            'primary_category' => 'customer',
            'lifecycle_stage'  => 'customer',
            'lead_score'       => 100,
            'engagement_level' => 'high',
        ] );
        if ( ! $contact ) {
            return [];
        }

        $transaction_id = sanitize_text_field( $purchase['transaction_id'] ?? $purchase['session_id'] ?? $purchase['payment_intent'] ?? '' );
        if ( $transaction_id !== '' ) {
            $existing_purchase_id = (int) $wpdb->get_var( $wpdb->prepare(
                "SELECT id FROM {$purchases_table} WHERE user_id = %d AND transaction_id = %s LIMIT 1",
                $user_id,
                $transaction_id
            ) );
            if ( $existing_purchase_id > 0 ) {
                vmo_crm_convert_to_customer( $user_id, (string) $contact['contact_id'], [
                    'type'    => 'purchase_completed',
                    'summary' => 'Existing purchase evidence reconfirmed customer status.',
                    'purchase_id' => $existing_purchase_id,
                    'transaction_id' => $transaction_id,
                ] );
                return [ 'contact' => $contact, 'purchase_id' => $existing_purchase_id, 'duplicate' => true ];
            }
        }

        $purchase_type = sanitize_key( $purchase['purchase_type'] ?? 'one_time' );
        if ( ! in_array( $purchase_type, [ 'one_time', 'subscription', 'recurring', 'service', 'consultation' ], true ) ) {
            $purchase_type = 'one_time';
        }
        $status = sanitize_key( $purchase['status'] ?? 'completed' );
        if ( ! in_array( $status, [ 'completed', 'pending', 'cancelled', 'refunded' ], true ) ) {
            $status = 'pending';
        }
        $product_service = sanitize_text_field( $purchase['product_service'] ?? $purchase['product_name'] ?? $purchase['description'] ?? 'Purchase' );
        $amount = floatval( $purchase['amount'] ?? 0 );
        $currency = strtoupper( sanitize_text_field( $purchase['currency'] ?? 'USD' ) );
        $purchase_date = sanitize_text_field( $purchase['purchase_date'] ?? current_time( 'mysql' ) );
        $subscription_details = $purchase['subscription_details'] ?? null;
        if ( is_array( $subscription_details ) || is_object( $subscription_details ) ) {
            $subscription_details = wp_json_encode( $subscription_details );
        }

        $wpdb->insert( $purchases_table, [
            'user_id'              => $user_id,
            'contact_id'           => (string) $contact['contact_id'],
            'transaction_id'       => $transaction_id ?: null,
            'product_service'      => $product_service,
            'purchase_type'        => $purchase_type,
            'amount'               => $amount,
            'currency'             => $currency ?: 'USD',
            'status'               => $status,
            'purchase_date'        => $purchase_date,
            'subscription_details' => is_string( $subscription_details ) ? wp_kses_post( $subscription_details ) : null,
            'payment_method'       => sanitize_text_field( $purchase['payment_method'] ?? '' ) ?: null,
            'notes'                => sanitize_textarea_field( $purchase['notes'] ?? '' ) ?: null,
            'ai_insights'          => ! empty( $purchase['ai_insights'] ) ? wp_kses_post( is_string( $purchase['ai_insights'] ) ? $purchase['ai_insights'] : wp_json_encode( $purchase['ai_insights'] ) ) : null,
            'created_at'           => current_time( 'mysql' ),
            'updated_at'           => current_time( 'mysql' ),
        ] );
        $purchase_id = (int) $wpdb->insert_id;

        vmo_crm_log_interaction( $user_id, (string) $contact['contact_id'], [
            'interaction_type' => 'purchase',
            'subtype'          => $status === 'completed' ? 'completed_purchase' : $status . '_purchase',
            'direction'        => 'inbound',
            'subject'          => $product_service,
            'summary'          => sprintf( 'Purchase recorded: %s (%s %0.2f).', $product_service, $currency ?: 'USD', $amount ),
            'outcome'          => $status === 'completed' ? 'converted' : 'pending',
            'conversion_value' => $amount,
            'metadata'         => array_merge( $purchase, [ 'purchase_id' => $purchase_id ] ),
        ] );

        if ( $status === 'completed' ) {
            vmo_crm_convert_to_customer( $user_id, (string) $contact['contact_id'], [
                'type'           => 'purchase_completed',
                'summary'        => 'Customer status confirmed by completed purchase.',
                'purchase_id'    => $purchase_id,
                'transaction_id' => $transaction_id,
            ] );

            if ( function_exists( 'vmo_cfo_record_financial_event' ) && empty( $purchase['cfo_skip_event'] ) ) {
                vmo_cfo_record_financial_event( $user_id, [
                    'event_type'       => 'payment',
                    'source'           => sanitize_key( $purchase['source'] ?? 'crm_purchase' ),
                    'source_reference' => $transaction_id ?: ( 'purchase_' . $purchase_id ),
                    'direction'        => 'income',
                    'amount'           => $amount,
                    'currency'         => $currency ?: 'USD',
                    'payment_status'   => 'completed',
                    'event_status'     => 'confirmed',
                    'confidence'       => 1,
                    'customer_name'    => $contact['full_name'] ?? '',
                    'customer_email'   => $contact['email'] ?? '',
                    'customer_phone'   => $contact['phone'] ?? '',
                    'payment_method'   => $purchase['payment_method'] ?? $purchase['source'] ?? 'stripe',
                    'category'         => 'sales',
                    'product_service'  => $product_service,
                    'transaction_date' => $purchase_date,
                    'contact_id'       => (string) $contact['contact_id'],
                    'crm_contact_db_id'=> (int) ( $contact['id'] ?? 0 ),
                    'purchase_id'      => $purchase_id,
                    'recommended_action' => 'posted_to_ledger',
                    'metadata'         => [ 'skip_crm_sync' => true, 'purchase' => $purchase ],
                    'auto_post'        => true,
                ] );
            }
        }

        return [ 'contact' => $contact, 'purchase_id' => $purchase_id, 'duplicate' => false ];
    }
}

if ( ! function_exists( 'vmo_crm_generate_retention_recommendation' ) ) {
    function vmo_crm_generate_retention_recommendation( int $user_id, $contact_reference ): string {
        global $wpdb;
        $contact = is_numeric( $contact_reference )
            ? vmo_crm_find_contact( $user_id, [ 'id' => absint( $contact_reference ) ] )
            : vmo_crm_find_contact( $user_id, [ 'contact_id' => sanitize_text_field( $contact_reference ) ] );
        if ( ! $contact ) {
            return 'Review the contact history, confirm the next useful step, and log the outcome after outreach.';
        }

        $contact_id = (string) $contact['contact_id'];
        $purchases_table = $wpdb->prefix . 'vmo_contact_purchases';
        $interactions_table = $wpdb->prefix . 'vmo_contact_interactions';
        $purchase_count = 0;
        $total_spent = 0.0;
        $last_purchase_date = '';
        if ( vmo_table_exists( $purchases_table ) ) {
            $purchase_stats = $wpdb->get_row( $wpdb->prepare(
                "SELECT COUNT(*) AS purchase_count, COALESCE(SUM(amount), 0) AS total_spent, MAX(purchase_date) AS last_purchase_date FROM {$purchases_table} WHERE user_id = %d AND contact_id = %s AND status = 'completed'",
                $user_id,
                $contact_id
            ), ARRAY_A );
            $purchase_count = intval( $purchase_stats['purchase_count'] ?? 0 );
            $total_spent = floatval( $purchase_stats['total_spent'] ?? 0 );
            $last_purchase_date = (string) ( $purchase_stats['last_purchase_date'] ?? '' );
        }

        $last_interaction_date = (string) ( $contact['last_contact'] ?? '' );
        if ( vmo_table_exists( $interactions_table ) ) {
            $interaction_date = $wpdb->get_var( $wpdb->prepare(
                "SELECT MAX(created_at) FROM {$interactions_table} WHERE user_id = %d AND contact_id = %s",
                $user_id,
                $contact_id
            ) );
            if ( $interaction_date ) {
                $last_interaction_date = (string) $interaction_date;
            }
        }

        $days_since_contact = $last_interaction_date ? floor( ( time() - strtotime( $last_interaction_date ) ) / DAY_IN_SECONDS ) : null;
        if ( $purchase_count > 1 || $total_spent >= 500 ) {
            return 'High-value relationship: send a personalized check-in, reference their last purchase, and offer the next service or renewal path.';
        }
        if ( $purchase_count === 1 ) {
            return 'New customer: follow up on satisfaction, confirm the outcome they needed, and suggest the next helpful product or service.';
        }
        if ( $days_since_contact !== null && $days_since_contact >= 30 ) {
            return 'Dormant lead: restart the relationship with a short message tied to their original interest and offer one clear next step.';
        }
        if ( ! empty( $contact['phone'] ) ) {
            return 'Active lead: call or text with a concise next step, then log the response and schedule follow-up if they are not ready.';
        }
        if ( ! empty( $contact['email'] ) ) {
            return 'Active lead: send a focused email with one recommended offer or booking link, then watch for opens, replies, or form activity.';
        }
        return 'Incomplete contact: add a reliable email or phone number before starting outreach.';
    }
}


// ═══════════════════════════════════════════════════════════════════
// PER-EMAIL METERING (May 2026)
// Every outbound wp_mail() in CLIENT_MODE costs VMO real money (SMTP2GO
// + mailcow + Vultr shard usage). We now meter EACH send against the
// user's wallet at the tiny-markup floor (~15 tokens / send). If the
// wallet cannot cover the send we refuse it — the plugin is prepaid,
// like a debit card. The master plugin is excluded so internal dev/test
// continues unmetered.
// ═══════════════════════════════════════════════════════════════════
if (!function_exists('vmo_meter_wp_mail_outbound')) {
    function vmo_meter_wp_mail_outbound($return, $atts) {
        // Already short-circuited by an earlier filter? Honor it.
        if ($return !== null) {
            return $return;
        }
        // Master build is unmetered for dev/test.
        if (defined('VMO_MASTER_VERSION') && VMO_MASTER_VERSION === true && !(defined('VMO_CLIENT_MODE') && VMO_CLIENT_MODE === true)) {
            return null;
        }
        if (!function_exists('vmo_deduct_tokens')) {
            return null;
        }
        $owner_id = (int) (get_current_user_id() ?: get_option('vmo_master_user_id', 0));
        if ($owner_id <= 0) {
            // No metered owner; fall through (e.g. cron pre-login).
            return null;
        }
        // Internal/system mails (password reset to admin, etc.) — opt out via filter.
        if (apply_filters('vmo_wp_mail_skip_metering', false, $atts, $owner_id)) {
            return null;
        }
        $cost = (int) apply_filters('vmo_wp_mail_token_cost', 15, $atts, $owner_id);
        $to = is_array($atts['to'] ?? null) ? implode(',', $atts['to']) : (string) ($atts['to'] ?? '');
        $charged = vmo_deduct_tokens($owner_id, $cost, 'email_send', 'Outbound email to ' . wp_strip_all_tags(substr($to, 0, 120)));
        if (!$charged) {
            error_log('VMO wp_mail blocked (insufficient credit) for user ' . $owner_id . ' to=' . substr($to, 0, 80));
            // Returning false short-circuits pre_wp_mail and causes wp_mail to return false.
            return false;
        }
        return null; // Continue normal send pipeline.
    }
    // Priority 1 so we charge BEFORE the QuickLaunch master-relay filter (default 10).
    add_filter('pre_wp_mail', 'vmo_meter_wp_mail_outbound', 1, 2);
}

// ═══════════════════════════════════════════════════════════════════
// Global wp_mail From Name & Address
// Ensures ALL emails from the plugin display the site name instead
// of the default "WordPress".  Uses the site title + admin email.
// ═══════════════════════════════════════════════════════════════════
add_filter('wp_mail_from_name', function ($name) {
    // Only override the default "WordPress" (or blank) — respect
    // explicit overrides from other plugins / SMTP plugins.
    if ($name === 'WordPress' || $name === '' || $name === 'wordpress') {
        $site = get_bloginfo('name');
        return $site ? $site : 'VMO AI';
    }
    return $name;
});
add_filter('wp_mail_from', function ($email) {
    // Replace the default wordpress@domain with the site admin email
    if (strpos($email, 'wordpress@') === 0) {
        $admin = get_option('admin_email');
        return $admin ? $admin : $email;
    }
    return $email;
});

/**
 * Public Twilio endpoints fallback.
 *
 * On getvmo.com, REST (`/wp-json` and `rest_route`) and sometimes admin-ajax can be
 * rewritten/blocked by hosting or security rules, which causes Twilio to fetch HTML
 * (homepage) and announce "application error".
 *
 * These endpoints are query-param based and run before theme output.
 * Example: https://getvmo.com/?vmo_twilio=power_dialer_twiml&twiml_action=conference&conference=test
 */
add_action('init', function () {
    if (empty($_GET['vmo_twilio'])) {
        return;
    }

    $kind = sanitize_key((string) $_GET['vmo_twilio']);

    if (!class_exists('VMO_Twilio_Manager')) {
        // If the manager isn't available, we can't serve TwiML.
        status_header(500);
        header('Content-Type: text/plain; charset=UTF-8');
        echo 'Twilio manager unavailable';
        exit;
    }

    $manager = VMO_Twilio_Manager::get_instance();

    // Minimal REST-like request wrapper.
    $params = wp_unslash($_REQUEST);
    $request = new class($params) {
        private array $params;
        public function __construct(array $params) { $this->params = $params; }
        public function get_params(): array { return $this->params; }
        public function get_json_params(): array { return array(); }
    };

    // Never cache webhook responses.
    if (function_exists('nocache_headers')) {
        nocache_headers();
    }

    try {
        switch ($kind) {
            case 'ping':
                header('Content-Type: text/plain; charset=UTF-8');
                echo 'pong';
                exit;

            case 'voice':
                $manager->handle_voice_webhook($request);
                exit;

            case 'browser_call':
                $manager->handle_browser_call($request);
                exit;

            case 'pd_twiml':
            case 'power_dialer_twiml':
                $manager->power_dialer_twiml($request);
                exit;

            case 'status':
            case 'status_callback':
                ob_start();
                $resp = $manager->handle_status_callback($request);
                header('Content-Type: application/json; charset=UTF-8');
                if ($resp instanceof WP_REST_Response) {
                    status_header($resp->get_status());
                    echo wp_json_encode($resp->get_data());
                } elseif (is_wp_error($resp)) {
                    status_header(500);
                    echo wp_json_encode(array('success' => false, 'message' => $resp->get_error_message()));
                } else {
                    status_header(200);
                    echo wp_json_encode(is_array($resp) ? $resp : array('success' => true));
                }
                // Flush response so post-response work runs after client disconnects
                if (!headers_sent()) { header('Connection: close'); }
                $sz = ob_get_length(); if ($sz !== false && !headers_sent()) { header('Content-Length: ' . $sz); }
                while (ob_get_level()) { ob_end_flush(); } flush();
                if (function_exists('fastcgi_finish_request')) { fastcgi_finish_request(); }
                elseif (function_exists('litespeed_finish_request')) { litespeed_finish_request(); }
                ignore_user_abort(true);
                if (function_exists('set_time_limit')) { set_time_limit(120); }
                // Post-response: run summary + email pipeline directly
                $gw_call_sid = sanitize_text_field(wp_unslash($_REQUEST['CallSid'] ?? ''));
                $gw_call_status = sanitize_text_field(wp_unslash($_REQUEST['CallStatus'] ?? ''));
                $gw_recording = esc_url_raw(wp_unslash($_REQUEST['RecordingUrl'] ?? ''));
                if ($gw_call_sid !== '' && $gw_call_status === 'completed') {
                    try {
                        if (!empty($gw_recording)) {
                            $manager->generate_call_summary($gw_call_sid, $gw_recording);
                        }
                        $manager->send_post_call_summary_email($gw_call_sid);
                    } catch (\Throwable $e) {
                        error_log('[VMO GW Status] Post-response pipeline failed: ' . $e->getMessage());
                    }
                }
                exit;

            case 'sms':
            case 'sms_webhook':
                $resp = $manager->handle_sms_webhook($request);
                header('Content-Type: application/json; charset=UTF-8');
                if ($resp instanceof WP_REST_Response) {
                    status_header($resp->get_status());
                    echo wp_json_encode($resp->get_data());
                } else {
                    status_header(200);
                    echo wp_json_encode(array('success' => true));
                }
                exit;

            case 'dial_status':
                $resp = $manager->handle_dial_status($request);
                header('Content-Type: application/json; charset=UTF-8');
                if ($resp instanceof WP_REST_Response) {
                    status_header($resp->get_status());
                    echo wp_json_encode($resp->get_data());
                } else {
                    status_header(200);
                    echo wp_json_encode(array('success' => true));
                }
                exit;

            case 'transcription':
                $resp = $manager->handle_transcription_callback($request);
                header('Content-Type: application/json; charset=UTF-8');
                if ($resp instanceof WP_REST_Response) {
                    status_header($resp->get_status());
                    echo wp_json_encode($resp->get_data());
                } else {
                    status_header(200);
                    echo wp_json_encode(array('success' => true));
                }
                exit;

            case 'recording':
                ob_start();
                $resp = $manager->handle_recording_callback($request);
                header('Content-Type: application/json; charset=UTF-8');
                status_header(200);
                echo wp_json_encode(is_array($resp) ? $resp : array('ok' => true));
                // Flush response so post-response work runs after client disconnects
                if (!headers_sent()) { header('Connection: close'); }
                $sz = ob_get_length(); if ($sz !== false && !headers_sent()) { header('Content-Length: ' . $sz); }
                while (ob_get_level()) { ob_end_flush(); } flush();
                if (function_exists('fastcgi_finish_request')) { fastcgi_finish_request(); }
                elseif (function_exists('litespeed_finish_request')) { litespeed_finish_request(); }
                ignore_user_abort(true);
                if (function_exists('set_time_limit')) { set_time_limit(120); }
                // Post-response: run summary + email pipeline directly
                $gw_rec_call_sid = sanitize_text_field(wp_unslash($_REQUEST['CallSid'] ?? ''));
                $gw_rec_url = esc_url_raw(wp_unslash($_REQUEST['RecordingUrl'] ?? ''));
                $gw_rec_status = sanitize_text_field(wp_unslash($_REQUEST['RecordingStatus'] ?? ''));
                if ($gw_rec_call_sid !== '' && !empty($gw_rec_url) && ($gw_rec_status === '' || $gw_rec_status === 'completed')) {
                    try {
                        $manager->generate_call_summary($gw_rec_call_sid, $gw_rec_url);
                        $manager->send_post_call_summary_email($gw_rec_call_sid);
                    } catch (\Throwable $e) {
                        error_log('[VMO GW Recording] Post-response pipeline failed: ' . $e->getMessage());
                    }
                }
                exit;

            case 'pd_outbound_callback':
                $resp = $manager->power_dialer_outbound_callback($request);
                header('Content-Type: application/json; charset=UTF-8');
                if ($resp instanceof WP_REST_Response) {
                    status_header($resp->get_status());
                    echo wp_json_encode($resp->get_data());
                } else {
                    status_header(200);
                    echo wp_json_encode(is_array($resp) ? $resp : array('success' => true));
                }
                exit;

            case 'ext_ring':
                $manager->handle_ext_ring($request);
                exit;

            case 'ext_listen_action':
                $manager->handle_ext_listen_action($request);
                exit;

            case 'ext_monitor':
                $manager->handle_ext_monitor($request);
                exit;

            default:
                status_header(404);
                header('Content-Type: text/plain; charset=UTF-8');
                echo 'Unknown endpoint';
                exit;
        }
    } catch (Throwable $e) {
        error_log('[VMO Phone Gateway] kind=' . $kind . ' failed: ' . $e->getMessage());
        if (in_array($kind, array('ext_ring', 'ext_listen_action', 'ext_monitor'), true)) {
            status_header(200);
            header('Content-Type: text/xml; charset=UTF-8');
            echo '<?xml version="1.0" encoding="UTF-8"?>';
            echo '<Response><Say>We could not connect the call menu. The A I will continue handling the call. Goodbye.</Say><Hangup/></Response>';
            exit;
        }
        status_header(500);
        header('Content-Type: text/plain; charset=UTF-8');
        echo 'Endpoint error';
        exit;
    }
}, 0);

if (!function_exists('vmo_is_generated_wp_username')) {
    function vmo_is_generated_wp_username($value): bool {
        $compact = strtolower(preg_replace('/[^a-z0-9]/', '', (string) $value));
        if ($compact === '') {
            return false;
        }

        // QuickLaunch/Vultr marketplace WordPress images can create random
        // administrator logins such as wpauserm3DngZzb. Those are machine IDs,
        // not customer names, and must never be spoken or displayed as greetings.
        if (preg_match('/^(?:wpauser|wpuser|wpadmin|wpcli|wpsite)[a-z0-9]{6,}$/i', $compact)) {
            return true;
        }

        return false;
    }
}

if (!function_exists('vmo_normalize_personal_greeting_name')) {
    function vmo_normalize_personal_greeting_name($name): string {
        $name = trim(wp_strip_all_tags((string) $name));
        if ($name === '') {
            return '';
        }

        $lower = strtolower($name);
        if (is_email($name) || strpos($name, '@') !== false || strpos($lower, 'mailto:') !== false) {
            return '';
        }
        if (preg_match('/\b[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,}\b/i', $name)) {
            return '';
        }
        if (preg_match('#^https?://#i', $name)) {
            return '';
        }
        $compact = preg_replace('/\s+/', '', $lower);
        if (function_exists('vmo_is_generated_wp_username') && vmo_is_generated_wp_username($compact)) {
            return '';
        }
        if (preg_match('/[a-z0-9._%+\-]+(?:gmail|googlemail|yahoo|icloud|outlook|hotmail|aol|protonmail|live|msn)\.com$/i', $compact)) {
            return '';
        }
        if (preg_match('/^[a-z0-9._%+\-]+\.(?:com|net|org|co|io|app|dev)$/i', $compact)) {
            return '';
        }

        return $name;
    }
}

if (!function_exists('vmo_quicklaunch_get_welcome_name')) {
    function vmo_quicklaunch_get_welcome_name($user_id) {
        $user_id = (int) $user_id;

        // PART3 IDENTITY FIX: the onboarding-submitted username is the only
        // customer-facing source of truth. Marketplace/Vultr/WP bootstrap logins
        // such as wpauserEb95yG5B must never win a fallback race and become the
        // name Jarvis speaks.
        $meta_keys = array(
            'vmo_quicklaunch_onboarding_username',
            'vmo_quicklaunch_customer_username',
            'vmo_quicklaunch_welcome_name',
            'vmo_quicklaunch_username',
            'vmo_onboarding_username',
            'first_name',
            'nickname',
        );
        foreach ($meta_keys as $key) {
            $stored_name = vmo_normalize_personal_greeting_name(get_user_meta($user_id, $key, true));
            if ($stored_name !== '') {
                return $stored_name;
            }
        }

        $option_keys = array(
            'vmo_quicklaunch_onboarding_username',
            'vmo_quicklaunch_customer_username',
            'vmo_quicklaunch_welcome_name',
            'vmo_onboarding_username',
        );
        foreach ($option_keys as $key) {
            $stored_name = vmo_normalize_personal_greeting_name(get_option($key, ''));
            if ($stored_name !== '') {
                return $stored_name;
            }
        }

        $user = get_userdata($user_id);
        if ($user) {
            foreach (array($user->first_name, $user->nickname, $user->display_name) as $candidate) {
                $candidate = vmo_normalize_personal_greeting_name($candidate);
                if ($candidate !== '') {
                    return $candidate;
                }
            }
        }

        return '';
    }
}

if (!function_exists('vmo_quicklaunch_repair_current_user_greeting_identity')) {
    function vmo_quicklaunch_repair_current_user_greeting_identity() {
        if (!is_user_logged_in()) {
            return;
        }
        $user_id = get_current_user_id();
        if ($user_id <= 0) {
            return;
        }

        $canonical = '';
        foreach (array(
            'vmo_quicklaunch_onboarding_username',
            'vmo_quicklaunch_customer_username',
            'vmo_quicklaunch_welcome_name',
            'vmo_quicklaunch_username',
            'vmo_onboarding_username',
        ) as $key) {
            $canonical = vmo_normalize_personal_greeting_name(get_user_meta($user_id, $key, true));
            if ($canonical !== '') { break; }
        }
        if ($canonical === '') {
            foreach (array(
                'vmo_quicklaunch_onboarding_username',
                'vmo_quicklaunch_customer_username',
                'vmo_quicklaunch_welcome_name',
                'vmo_onboarding_username',
            ) as $key) {
                $canonical = vmo_normalize_personal_greeting_name(get_option($key, ''));
                if ($canonical !== '') { break; }
            }
        }
        if ($canonical === '') {
            return;
        }

        // Persist the canonical name under every key different dashboards/agents
        // might read. This also self-heals existing client installs on the next page load.
        update_user_meta($user_id, 'vmo_quicklaunch_onboarding_username', $canonical);
        update_user_meta($user_id, 'vmo_quicklaunch_customer_username', $canonical);
        update_user_meta($user_id, 'vmo_quicklaunch_welcome_name', $canonical);
        update_user_meta($user_id, 'vmo_quicklaunch_username', $canonical);
        update_user_meta($user_id, 'first_name', $canonical);
        update_user_meta($user_id, 'nickname', $canonical);

        $user = get_userdata($user_id);
        if (!$user) {
            return;
        }
        $current_values = array($user->display_name, $user->nickname, $user->first_name);
        $needs_update = false;
        foreach ($current_values as $value) {
            $clean = vmo_normalize_personal_greeting_name($value);
            if ($clean === '' || $clean !== $canonical) {
                $needs_update = true;
                break;
            }
        }
        if ($needs_update) {
            wp_update_user(array(
                'ID' => $user_id,
                'display_name' => $canonical,
                'nickname' => $canonical,
                'first_name' => $canonical,
            ));
        }
    }
    add_action('init', 'vmo_quicklaunch_repair_current_user_greeting_identity', 25);
    add_action('admin_init', 'vmo_quicklaunch_repair_current_user_greeting_identity', 5);
}

if (!function_exists('vmo_quicklaunch_build_first_run_notification')) {
    function vmo_quicklaunch_build_first_run_notification($user_id, array $base = array()) {
        $welcome_name = vmo_quicklaunch_get_welcome_name((int) $user_id);
        $spoken_name = $welcome_name !== '' ? $welcome_name : 'there';
        $spoken_message = sprintf(
            'Welcome %s — and congratulations on choosing VMO. This is one of the best decisions you could have made for your business, because VMO is not just a website builder; it is your entire business builder and manager. I am Jarvis, the AI Coordinator for your VMO plugin. You can think of me as your operations manager. I work alongside Jerreme, your specialized business consultant, and more than 35 specialized VMO AI agents across strategy, business consultation, website building, content, design, SEO, media, documents, email, phone, CRM, payments, reporting, automation, and launch operations. If you need help, guidance, or clarification at any point, type to me or press Start Voice and talk to me. I will guide you through the plugin, answer questions, and help you perform the right action. Your most important first step is the Business Wizard consultation. Click the red Business Wizard button so Jerreme can learn your business dreams, goals, customers, offers, market, brand, and operating needs. That consultation becomes the foundation VMO uses to set up the right connected business system instead of a generic website. Premium Live Voice can respond almost instantly when available. Default Chat Voice is less expensive but usually a few seconds slower. You can choose your preferred voice and mode from the gear icon at the top of the Jarvis chat panel.',
            $spoken_name
        );

        return array_merge(array(
            'type' => 'quicklaunch_payment',
            'title' => 'Welcome to VMO, ' . $spoken_name,
            'message' => 'Your hosted VMO workspace is ready. Jarvis will introduce the VMO team, explain Default Chat Voice versus Premium Live Voice, and guide you to the red Business Wizard button so Jerreme can begin the consultation.',
            'welcome_name' => $welcome_name,
            'speak_on_open' => true,
            'spoken_message' => $spoken_message,
            'hosted_setup_included' => true,
        ), $base);
    }
}

if (!function_exists('vmo_quicklaunch_get_welcome_client_data')) {
    function vmo_quicklaunch_get_welcome_client_data($user_id) {
        $user_id = (int) $user_id;
        $pending = !empty(get_user_meta($user_id, 'vmo_quicklaunch_first_run_welcome_pending', true));
        $notification = vmo_quicklaunch_build_first_run_notification($user_id);
        $hosted_client = get_option('vmo_quicklaunch_hosted_client') === '1'
            || get_option('vmo_client_distribution_channel') === 'quicklaunch_hosted'
            || (defined('VMO_CLIENT_DISTRIBUTION_CHANNEL') && VMO_CLIENT_DISTRIBUTION_CHANNEL === 'quicklaunch_hosted');
        $service_ready = !$hosted_client || get_option('vmo_quicklaunch_service_ready') === '1';

        return array(
            'pending' => $pending,
            'name' => $notification['welcome_name'],
            'spoken_message' => $notification['spoken_message'],
            'hosted_client' => (bool) $hosted_client,
            'service_ready' => (bool) $service_ready,
            'service_ready_at' => (int) get_option('vmo_quicklaunch_service_ready_at', 0),
        );
    }
}

if (!function_exists('vmo_quicklaunch_get_request_origin')) {
    function vmo_quicklaunch_get_request_origin() {
        $host = isset($_SERVER['HTTP_HOST']) ? (string) $_SERVER['HTTP_HOST'] : '';
        $host = preg_replace('/[^A-Za-z0-9.\-:\[\]]/', '', $host);
        if ($host === '') {
            return '';
        }

        $https = is_ssl()
            || (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower((string) $_SERVER['HTTP_X_FORWARDED_PROTO']) === 'https')
            || (!empty($_SERVER['HTTP_CF_VISITOR']) && stripos((string) $_SERVER['HTTP_CF_VISITOR'], 'https') !== false);

        return ($https ? 'https' : 'http') . '://' . $host;
    }
}

if (!function_exists('vmo_quicklaunch_request_host_is_ip')) {
    function vmo_quicklaunch_request_host_is_ip() {
        $host = isset($_SERVER['HTTP_HOST']) ? (string) $_SERVER['HTTP_HOST'] : '';
        $host = preg_replace('/:\d+$/', '', trim($host, '[]'));
        return $host !== '' && filter_var($host, FILTER_VALIDATE_IP);
    }
}

if (!function_exists('vmo_quicklaunch_request_is_https')) {
    function vmo_quicklaunch_request_is_https() {
        return is_ssl()
            || (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower((string) $_SERVER['HTTP_X_FORWARDED_PROTO']) === 'https')
            || (!empty($_SERVER['HTTP_CF_VISITOR']) && stripos((string) $_SERVER['HTTP_CF_VISITOR'], 'https') !== false);
    }
}

if (!function_exists('vmo_quicklaunch_force_https_token_url')) {
    function vmo_quicklaunch_force_https_token_url() {
        if (vmo_quicklaunch_request_is_https() || vmo_quicklaunch_request_host_is_ip()) {
            return;
        }
        $host = isset($_SERVER['HTTP_HOST']) ? preg_replace('/[^A-Za-z0-9.\-:\[\]]/', '', (string) $_SERVER['HTTP_HOST']) : '';
        if ($host === '') {
            return;
        }
        $uri = isset($_SERVER['REQUEST_URI']) ? (string) $_SERVER['REQUEST_URI'] : '/';
        if ($uri === '' || $uri[0] !== '/') {
            $uri = '/';
        }
        wp_safe_redirect('https://' . $host . $uri, 302);
        exit;
    }
}

if (!function_exists('vmo_quicklaunch_should_use_current_origin')) {
    function vmo_quicklaunch_should_use_current_origin() {
        return vmo_quicklaunch_request_host_is_ip() || !empty($_GET['vmo_ql_login']) || !empty($_GET['vmo_ql_target']);
    }
}

add_filter('pre_option_home', function ($pre) {
    $origin = vmo_quicklaunch_get_request_origin();
    return ($origin !== '' && vmo_quicklaunch_should_use_current_origin()) ? $origin : $pre;
}, 0, 1);

add_filter('pre_option_siteurl', function ($pre) {
    $origin = vmo_quicklaunch_get_request_origin();
    return ($origin !== '' && vmo_quicklaunch_should_use_current_origin()) ? $origin : $pre;
}, 0, 1);

if (!function_exists('vmo_quicklaunch_get_current_host_admin_url')) {
    function vmo_quicklaunch_get_current_host_admin_url() {
        $origin = vmo_quicklaunch_get_request_origin();
        if ($origin !== '') {
            return $origin . '/wp-admin/admin.php?page=vmo-ai-builder-main&vmo_quicklaunch_welcome=1';
        }
        return admin_url('admin.php?page=vmo-ai-builder-main&vmo_quicklaunch_welcome=1');
    }
}

if (!function_exists('vmo_quicklaunch_get_current_host_login_url')) {
    function vmo_quicklaunch_get_current_host_login_url() {
        $origin = vmo_quicklaunch_get_request_origin();
        $redirect = rawurlencode(vmo_quicklaunch_get_current_host_admin_url());
        if ($origin !== '') {
            return $origin . '/vmo-login/?redirect_to=' . $redirect;
        }
        return home_url('/vmo-login/?redirect_to=' . $redirect);
    }
}

add_filter('redirect_canonical', function ($redirect_url) {
    if (!empty($_GET['vmo_ql_login']) || !empty($_GET['vmo_ql_target'])) {
        return false;
    }
    return $redirect_url;
}, 0, 1);

add_filter('allowed_redirect_hosts', function ($hosts) {
    $host = isset($_SERVER['HTTP_HOST']) ? strtolower((string) $_SERVER['HTTP_HOST']) : '';
    $host = preg_replace('/[^a-z0-9.\-:\[\]]/', '', $host);
    if ($host !== '') {
        $hosts[] = $host;
        $hosts[] = preg_replace('/:\d+$/', '', $host);
    }
    return array_values(array_unique(array_filter($hosts)));
}, 10, 1);

if (!function_exists('vmo_quicklaunch_handle_auto_login')) {
    function vmo_quicklaunch_handle_auto_login() {
        if (empty($_GET['vmo_ql_login'])) {
            return;
        }

        nocache_headers();
        vmo_quicklaunch_force_https_token_url();

        $raw_token = sanitize_text_field(wp_unslash((string) $_GET['vmo_ql_login']));
        $token = preg_replace('/[^A-Za-z0-9]/', '', $raw_token);
        $fallback_url = vmo_quicklaunch_get_current_host_login_url();

        if (strlen($token) < 32) {
            wp_safe_redirect($fallback_url);
            exit;
        }

        $token_hash = wp_hash($token);
        $matched_user_id = 0;
        $candidate_ids = get_users(array(
            'fields' => 'ID',
            'meta_key' => 'vmo_quicklaunch_auto_login_hash',
            'number' => 50,
        ));

        foreach ($candidate_ids as $candidate_id) {
            $candidate_id = (int) $candidate_id;
            $stored_hash = (string) get_user_meta($candidate_id, 'vmo_quicklaunch_auto_login_hash', true);
            $expires = (int) get_user_meta($candidate_id, 'vmo_quicklaunch_auto_login_expires', true);

            if ($stored_hash === '' || $expires < time()) {
                continue;
            }

            if (hash_equals($stored_hash, $token_hash)) {
                $matched_user_id = $candidate_id;
                break;
            }
        }

        if (!$matched_user_id) {
            wp_safe_redirect($fallback_url);
            exit;
        }

        $user = get_userdata($matched_user_id);
        if (!$user) {
            wp_safe_redirect($fallback_url);
            exit;
        }

        update_user_meta($matched_user_id, 'vmo_quicklaunch_auto_login_used_at', time());
        update_user_meta($matched_user_id, 'vmo_quicklaunch_auto_login_expires', min($expires, time() + 15 * MINUTE_IN_SECONDS));
        update_user_meta($matched_user_id, 'vmo_quicklaunch_first_run_welcome_pending', 1);

        $existing_notification = get_user_meta($matched_user_id, 'vmo_pending_notification', true);
        if (empty($existing_notification)) {
            update_user_meta($matched_user_id, 'vmo_pending_notification', wp_json_encode(vmo_quicklaunch_build_first_run_notification($matched_user_id)));
        }

        wp_clear_auth_cookie();
        wp_set_current_user($matched_user_id);
        wp_set_auth_cookie($matched_user_id, true, is_ssl());
        do_action('wp_login', $user->user_login, $user);

        wp_redirect(vmo_quicklaunch_get_current_host_admin_url(), 302);
        exit;
    }
}
add_action('init', 'vmo_quicklaunch_handle_auto_login', 2);

/**
 * QuickLaunch browser-safe readiness pixel.
 *
 * The master onboarding wizard loads this as an <img> from the customer's hosted
 * domain. A successful image load proves the browser can reach Cloudflare, the
 * origin server, WordPress admin-ajax, and this client plugin without exposing
 * the customer to a raw Cloudflare 521/522 page.
 */
if (!function_exists('vmo_quicklaunch_browser_ready_pixel')) {
    function vmo_quicklaunch_browser_ready_pixel() {
        $expected = (string) get_option('vmo_quicklaunch_probe_token', '');
        $passed = isset($_GET['token']) ? sanitize_text_field(wp_unslash((string) $_GET['token'])) : '';
        if ($expected === '') {
            status_header(404);
            nocache_headers();
            header('Content-Type: text/plain; charset=UTF-8');
            echo 'probe disabled';
            exit;
        }
        if ($passed === '' || !hash_equals($expected, $passed)) {
            status_header(403);
            nocache_headers();
            header('Content-Type: text/plain; charset=UTF-8');
            echo 'forbidden';
            exit;
        }
        status_header(200);
        nocache_headers();
        header('Content-Type: image/gif');
        header('X-VMO-QuickLaunch-Ready: 1');
        echo base64_decode('R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==');
        exit;
    }
}
add_action('wp_ajax_vmo_ql_browser_ready_pixel', 'vmo_quicklaunch_browser_ready_pixel');
add_action('wp_ajax_nopriv_vmo_ql_browser_ready_pixel', 'vmo_quicklaunch_browser_ready_pixel');


/**
 * Orchestrator-side health probe endpoint.
 *
 * Exposes /wp-json/vmo-quicklaunch-client/v1/health on every site where
 * this plugin is installed. The master/orchestrator polls this on each
 * customer's new WordPress to detect 9-marker readiness when the
 * outbound startup-script callback POST never made it back. Token-gated
 * — only callers presenting the matching vmo_quicklaunch_probe_token
 * option (written by the startup script) get a useful response.
 *
 * Returns ONLY readiness booleans. No PII, no credentials.
 */
if (!function_exists('vmo_quicklaunch_register_probe_health_route')) {
    function vmo_quicklaunch_register_probe_health_route() {
        register_rest_route('vmo-quicklaunch-client/v1', '/health', [
            'methods'             => 'GET',
            'permission_callback' => '__return_true',
            'callback'            => 'vmo_quicklaunch_handle_probe_health',
        ]);
    }
}
add_action('rest_api_init', 'vmo_quicklaunch_register_probe_health_route');

if (!function_exists('vmo_quicklaunch_get_probe_admin_user')) {
    function vmo_quicklaunch_get_probe_admin_user() {
        $user = null;
        $admin_email = (string) get_option('admin_email', '');
        if ($admin_email !== '') {
            $user = get_user_by('email', $admin_email);
        }
        if (!$user) {
            $admins = get_users(['role' => 'administrator', 'number' => 1, 'orderby' => 'ID', 'order' => 'ASC']);
            $user = is_array($admins) && !empty($admins) ? $admins[0] : null;
        }
        return $user;
    }
}

if (!function_exists('vmo_quicklaunch_audio_payload_is_playable')) {
    function vmo_quicklaunch_audio_payload_is_playable($body, $content_type = '') {
        $body = (string) $body;
        $content_type = strtolower((string) $content_type);
        if ($body === '' || strlen($body) < 128) {
            return false;
        }

        $trimmed = ltrim(substr($body, 0, 800));
        if ($trimmed !== '' && ($trimmed[0] === '{' || $trimmed[0] === '[' || strpos($content_type, 'json') !== false || strpos($content_type, 'text/') !== false)) {
            $decoded = json_decode($body, true);
            if (!is_array($decoded)) {
                return false;
            }
            $audio64 = $decoded['audio'] ?? ($decoded['data']['audio'] ?? ($decoded['audio_base64'] ?? ($decoded['data']['audio_base64'] ?? '')));
            if (!is_string($audio64) || $audio64 === '') {
                return false;
            }
            $audio64 = preg_replace('#^data:audio/[^;]+;base64,#i', '', $audio64);
            $raw = base64_decode(preg_replace('/\s+/', '', $audio64), true);
            if ($raw === false) {
                return false;
            }
            $body = $raw;
        } else {
            $base64_probe = preg_replace('/\s+/', '', $body);
            if (strlen($base64_probe) > 256 && preg_match('#^(?:data:audio/[a-z0-9.+-]+;base64,)?[A-Za-z0-9+/]+={0,2}$#i', $base64_probe)) {
                $base64_probe = preg_replace('#^data:audio/[^;]+;base64,#i', '', $base64_probe);
                $raw = base64_decode($base64_probe, true);
                if ($raw !== false && strlen($raw) > 128) {
                    $body = $raw;
                }
            }
        }

        $needles = ['ID3', 'OggS', 'fLaC', 'RIFF', "\xFF\xFB", "\xFF\xF3", "\xFF\xF2"];
        $limit = min(8192, strlen($body));
        foreach ($needles as $needle) {
            $pos = strpos(substr($body, 0, $limit), $needle);
            if ($pos !== false) {
                return true;
            }
        }
        return false;
    }
}

if (!function_exists('vmo_quicklaunch_probe_tts_runtime')) {
    function vmo_quicklaunch_probe_tts_runtime($enabled = true) {
        if (!$enabled) {
            return ['ready' => false, 'note' => 'tts_runtime_waiting_for_basic_markers', 'code' => 0];
        }

        $cached = get_transient('vmo_ql_tts_runtime_probe');
        if (is_array($cached) && array_key_exists('ready', $cached)) {
            $cached['cached'] = true;
            return $cached;
        }

        if (!function_exists('vmo_smart_remote_post')) {
            return ['ready' => false, 'note' => 'tts_runtime_missing_proxy_helper', 'code' => 0];
        }

        $previous_user_id = function_exists('get_current_user_id') ? (int) get_current_user_id() : 0;
        $probe_user = function_exists('vmo_quicklaunch_get_probe_admin_user') ? vmo_quicklaunch_get_probe_admin_user() : null;
        $probe_user_id = $probe_user instanceof WP_User ? (int) $probe_user->ID : 0;
        if ($previous_user_id <= 0 && $probe_user_id > 0 && function_exists('wp_set_current_user')) {
            wp_set_current_user($probe_user_id);
        }

        $response = vmo_smart_remote_post('https://api.openai.com/v1/audio/speech', [
            'timeout' => 45,
            'headers' => [
                'Authorization' => 'Bearer vmo-client-proxy',
                'Content-Type'  => 'application/json',
            ],
            'body' => wp_json_encode([
                'model' => 'tts-1',
                'input' => 'Jarvis voice readiness check.',
                'voice' => 'echo',
                'response_format' => 'mp3',
                'speed' => 1.0,
            ]),
            'vmo_operation' => 'tts',
            'vmo_request_id' => 'ql_tts_probe_' . md5((string) get_option('vmo_installation_id', '') . '|' . time()),
        ]);

        if (is_wp_error($response)) {
            $result = ['ready' => false, 'note' => 'tts_runtime_error:' . $response->get_error_code(), 'code' => 0];
            set_transient('vmo_ql_tts_runtime_probe', $result, 20);
            if ($previous_user_id <= 0 && $probe_user_id > 0 && function_exists('wp_set_current_user')) {
                wp_set_current_user(0);
            }
            return $result;
        }

        $code = (int) wp_remote_retrieve_response_code($response);
        $body = (string) wp_remote_retrieve_body($response);
        $headers = wp_remote_retrieve_headers($response);
        $content_type = '';
        if ($headers instanceof \WP_Http_Requests_Headers || $headers instanceof \Requests_Utility_CaseInsensitiveDictionary) {
            $content_type = (string) ($headers['content-type'] ?? '');
        } elseif (is_array($headers)) {
            $content_type = (string) ($headers['content-type'] ?? ($headers['Content-Type'] ?? ''));
        }

        $playable = ($code >= 200 && $code < 300 && vmo_quicklaunch_audio_payload_is_playable($body, $content_type));
        $result = [
            'ready' => $playable,
            'note' => $playable ? 'tts_runtime_ready' : ($code >= 200 && $code < 300 ? 'tts_runtime_non_audio' : 'tts_runtime_http_' . $code),
            'code' => $code,
            'content_type' => $content_type,
            'bytes' => strlen($body),
        ];
        set_transient('vmo_ql_tts_runtime_probe', $result, $playable ? 10 * MINUTE_IN_SECONDS : 20);
        if ($previous_user_id <= 0 && $probe_user_id > 0 && function_exists('wp_set_current_user')) {
            wp_set_current_user(0);
        }
        return $result;
    }
}

if (!function_exists('vmo_quicklaunch_collect_service_readiness')) {
    function vmo_quicklaunch_collect_service_readiness() {
        $user = vmo_quicklaunch_get_probe_admin_user();
        $user_id = $user instanceof WP_User ? (int) $user->ID : 0;

        $is_client_mode = (defined('VMO_CLIENT_MODE') && constant('VMO_CLIENT_MODE'))
            || get_option('vmo_quicklaunch_hosted_client') === '1'
            || get_option('vmo_client_distribution_channel') === 'quicklaunch_hosted';

        $master_proxy_url = defined('VMO_MASTER_PROXY_URL') ? trim((string) constant('VMO_MASTER_PROXY_URL')) : '';
        if ($master_proxy_url === '') {
            $master_proxy_url = trim((string) get_option('vmo_master_proxy_url', ''));
        }

        $user_license   = $user_id > 0 ? get_user_meta($user_id, 'vmo_license_key', true) : '';
        $option_license = get_option('vmo_license_key', '');
        $installation_id = trim((string) get_option('vmo_installation_id', ''));

        $routes = [];
        if (function_exists('rest_get_server')) {
            $server = rest_get_server();
            if ($server && method_exists($server, 'get_routes')) {
                $routes = $server->get_routes();
            }
        }

        $admin_ajax_probe_registered = (has_action('wp_ajax_vmo_ql_service_probe') !== false)
            && (has_action('wp_ajax_nopriv_vmo_ql_service_probe') !== false);
        $admin_ajax_request = function_exists('wp_doing_ajax')
            ? (bool) wp_doing_ajax()
            : (defined('DOING_AJAX') && DOING_AJAX);

        $payload = [
            'service_probe_ready' => true,
            'admin_ajax_ready' => $admin_ajax_probe_registered || $admin_ajax_request,
            'client_mode_ready' => (bool) $is_client_mode,
            'master_proxy_url_present' => ($master_proxy_url !== '' && strpos($master_proxy_url, '__' . 'VMO_MASTER_PROXY_URL' . '__') === false),
            'license_ready' => ($user_license !== '' || $option_license !== ''),
            'installation_id_present' => ($installation_id !== ''),
            'client_bootstrap_ready' => class_exists('VMO_Client_Bootstrap') || function_exists('vmo_smart_remote_post'),
            'tts_rest_speak_route_ready' => isset($routes['/vmo/v1/tts/speak']),
            'tts_rest_voices_route_ready' => isset($routes['/vmo/v1/tts/voices']),
            'tts_ajax_ready' => has_action('wp_ajax_vmo_tts_stream') !== false,
            'realtime_session_ajax_ready' => has_action('wp_ajax_vmo_realtime_session') !== false,
            'realtime_sdp_ajax_ready' => has_action('wp_ajax_vmo_realtime_sdp') !== false,
            'probed_at' => time(),
        ];

        $basic_runtime_markers_ready = !empty($payload['admin_ajax_ready'])
            && !empty($payload['client_mode_ready'])
            && !empty($payload['master_proxy_url_present'])
            && !empty($payload['license_ready'])
            && !empty($payload['installation_id_present'])
            && !empty($payload['client_bootstrap_ready'])
            && !empty($payload['tts_ajax_ready']);
        $tts_runtime = vmo_quicklaunch_probe_tts_runtime($basic_runtime_markers_ready);
        $payload['tts_runtime_ready'] = !empty($tts_runtime['ready']);
        $payload['tts_runtime_note'] = (string) ($tts_runtime['note'] ?? '');
        $payload['tts_runtime_code'] = (int) ($tts_runtime['code'] ?? 0);

        $required = [
            'service_probe_ready',
            'admin_ajax_ready',
            'client_mode_ready',
            'master_proxy_url_present',
            'license_ready',
            'installation_id_present',
            'client_bootstrap_ready',
            'tts_rest_speak_route_ready',
            'tts_rest_voices_route_ready',
            'tts_ajax_ready',
            'tts_runtime_ready',
            'realtime_session_ajax_ready',
            'realtime_sdp_ajax_ready',
        ];

        $missing = [];
        foreach ($required as $key) {
            if (empty($payload[$key])) {
                $missing[] = $key;
            }
        }

        $payload['service_ready'] = empty($missing);
        $payload['service_missing'] = $missing;
        return $payload;
    }
}

if (!function_exists('vmo_quicklaunch_mark_service_ready_if_complete')) {
    function vmo_quicklaunch_mark_service_ready_if_complete(array $payload) {
        if (!empty($payload['service_ready'])) {
            update_option('vmo_quicklaunch_service_ready', '1', false);
            update_option('vmo_quicklaunch_service_ready_at', time(), false);
        } elseif (get_option('vmo_quicklaunch_service_ready') !== '1') {
            update_option('vmo_quicklaunch_service_ready', '0', false);
        }
    }
}

if (!function_exists('vmo_quicklaunch_handle_service_probe_ajax')) {
    function vmo_quicklaunch_handle_service_probe_ajax() {
        $expected = (string) get_option('vmo_quicklaunch_probe_token', '');
        if ($expected === '') {
            wp_send_json_error(['ok' => false, 'reason' => 'probe_disabled'], 404);
        }

        $given = isset($_REQUEST['token']) ? sanitize_text_field(wp_unslash((string) $_REQUEST['token'])) : '';
        if ($given === '' || !hash_equals($expected, $given)) {
            wp_send_json_error(['ok' => false, 'reason' => 'forbidden'], 403);
        }

        $payload = vmo_quicklaunch_collect_service_readiness();
        vmo_quicklaunch_mark_service_ready_if_complete($payload);

        $status_code = !empty($payload['service_ready']) ? 200 : 503;
        wp_send_json_success($payload, $status_code);
    }
}
add_action('wp_ajax_vmo_ql_service_probe', 'vmo_quicklaunch_handle_service_probe_ajax');
add_action('wp_ajax_nopriv_vmo_ql_service_probe', 'vmo_quicklaunch_handle_service_probe_ajax');

if (!function_exists('vmo_quicklaunch_handle_user_service_status_ajax')) {
    function vmo_quicklaunch_handle_user_service_status_ajax() {
        if (!current_user_can('read')) {
            wp_send_json_error(['message' => 'Forbidden'], 403);
        }
        if (function_exists('vmo_verify_admin_security') && !vmo_verify_admin_security()) {
            wp_send_json_error(['message' => 'Security check failed'], 403);
        }

        $payload = vmo_quicklaunch_collect_service_readiness();
        vmo_quicklaunch_mark_service_ready_if_complete($payload);
        wp_send_json_success($payload, !empty($payload['service_ready']) ? 200 : 503);
    }
}
add_action('wp_ajax_vmo_ql_user_service_status', 'vmo_quicklaunch_handle_user_service_status_ajax');

if (!function_exists('vmo_quicklaunch_handle_probe_health')) {
    function vmo_quicklaunch_handle_probe_health($request) {
        $expected = (string) get_option('vmo_quicklaunch_probe_token', '');
        if ($expected === '') {
            return new WP_REST_Response(['ok' => false, 'reason' => 'probe_disabled'], 404);
        }
        $given = (string) $request->get_param('token');
        if (!hash_equals($expected, $given)) {
            return new WP_REST_Response(['ok' => false, 'reason' => 'forbidden'], 403);
        }

        // Identify the QuickLaunch customer admin. Prefer the user whose
        // email matches admin_email (which the startup script set to the
        // customer email), then fall back to any administrator.
        $user = vmo_quicklaunch_get_probe_admin_user();

        $is_client_mode = (defined('VMO_CLIENT_MODE') && constant('VMO_CLIENT_MODE')) || get_option('vmo_quicklaunch_hosted_client') === '1';

        $plan_meta      = $user ? get_user_meta($user->ID, 'vmo_subscription_status', true) : '';
        $token_balance  = $user ? get_user_meta($user->ID, 'vmo_token_balance', true) : '';
        $user_license   = $user ? get_user_meta($user->ID, 'vmo_license_key', true) : '';
        $option_license = get_option('vmo_license_key', '');

        $payload = [
            'ok'                     => true,
            'wordpress_ready'        => true,
            'client_plugin_installed'=> true,
            'client_plugin_active'   => true,
            'admin_user_ready'       => $user instanceof WP_User,
            'license_ready'          => ($user_license !== '' || $option_license !== ''),
            'client_mode_ready'      => (bool) $is_client_mode,
            'plan_ready'             => ($plan_meta !== ''),
            'token_balance_ready'    => ($token_balance !== '' && (int) $token_balance >= 0),
            'dashboard_access_ready' => $user instanceof WP_User,
            'probed_at'              => time(),
        ];
        $payload = array_merge($payload, vmo_quicklaunch_collect_service_readiness());
        return new WP_REST_Response($payload, 200);
    }
}

if (!function_exists('vmo_quicklaunch_get_login_url')) {
    function vmo_quicklaunch_get_login_url() {
        return home_url('/vmo-login/');
    }
}

if (!function_exists('vmo_quicklaunch_is_login_path')) {
    function vmo_quicklaunch_is_login_path() {
        $request_path = (string) wp_parse_url((string) ($_SERVER['REQUEST_URI'] ?? ''), PHP_URL_PATH);
        $home_path = (string) wp_parse_url(home_url('/'), PHP_URL_PATH);

        if ($home_path !== '' && $home_path !== '/' && strpos($request_path, rtrim($home_path, '/')) === 0) {
            $request_path = substr($request_path, strlen(rtrim($home_path, '/')));
        }

        $request_path = trim($request_path, '/');
        return $request_path === 'vmo-login';
    }
}

if (!function_exists('vmo_quicklaunch_output_shortcut_download')) {
    function vmo_quicklaunch_output_shortcut_download($type) {
        $type = sanitize_key((string) $type);
        $login_url = vmo_quicklaunch_get_login_url();
        $icon_url = VMO_AI_BUILDER_URL . 'assets/vmo-icon.svg';

        nocache_headers();

        if ($type === 'windows') {
            header('Content-Type: application/octet-stream; charset=UTF-8');
            header('Content-Disposition: attachment; filename="VMO Login.url"');
            echo "[InternetShortcut]\r\n";
            echo 'URL=' . esc_url_raw($login_url) . "\r\n";
            echo 'IconFile=' . esc_url_raw($icon_url) . "\r\n";
            echo "IconIndex=0\r\n";
            exit;
        }

        if ($type === 'mac') {
            header('Content-Type: application/xml; charset=UTF-8');
            header('Content-Disposition: attachment; filename="VMO Login.webloc"');
            $plist_url = htmlspecialchars($login_url, ENT_XML1 | ENT_COMPAT, 'UTF-8');
            echo '<?xml version="1.0" encoding="UTF-8"?>' . "\n";
            echo '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">' . "\n";
            echo '<plist version="1.0"><dict><key>URL</key><string>' . $plist_url . '</string></dict></plist>';
            exit;
        }

        if ($type === 'icon') {
            $icon_path = VMO_AI_BUILDER_DIR . 'assets/vmo-icon.svg';
            if (is_readable($icon_path)) {
                header('Content-Type: image/svg+xml; charset=UTF-8');
                header('Content-Disposition: attachment; filename="vmo-icon.svg"');
                readfile($icon_path);
                exit;
            }
        }

        wp_safe_redirect($login_url);
        exit;
    }
}

if (!function_exists('vmo_quicklaunch_output_manifest')) {
    function vmo_quicklaunch_output_manifest() {
        nocache_headers();
        header('Content-Type: application/manifest+json; charset=UTF-8');

        $icon_url = VMO_AI_BUILDER_URL . 'assets/vmo-icon.svg';
        echo wp_json_encode(array(
            'name' => 'VMO AI Website Builder',
            'short_name' => 'VMO',
            'id' => home_url('/vmo-login/'),
            'start_url' => vmo_quicklaunch_get_login_url(),
            'scope' => home_url('/'),
            'display' => 'standalone',
            'background_color' => '#071923',
            'theme_color' => '#0b7d94',
            'icons' => array(
                array(
                    'src' => $icon_url,
                    'sizes' => 'any',
                    'type' => 'image/svg+xml',
                    'purpose' => 'any maskable',
                ),
            ),
        ), JSON_UNESCAPED_SLASHES);
        exit;
    }
}

if (!function_exists('vmo_quicklaunch_render_vmo_login_page')) {
    function vmo_quicklaunch_render_vmo_login_page($error = '') {
        $login_url = vmo_quicklaunch_get_login_url();
        $builder_url = admin_url('admin.php?page=vmo-ai-builder-main');
        $redirect_to = isset($_REQUEST['redirect_to']) ? esc_url_raw(wp_unslash((string) $_REQUEST['redirect_to'])) : $builder_url;
        $redirect_to = wp_validate_redirect($redirect_to, $builder_url);
        $login_action_url = add_query_arg('redirect_to', $redirect_to, $login_url);
        $icon_url = VMO_AI_BUILDER_URL . 'assets/vmo-icon.svg';
        $manifest_url = add_query_arg('vmo_manifest', '1', $login_url);
        $mac_shortcut_url = add_query_arg('vmo_shortcut', 'mac', $login_url);
        $windows_shortcut_url = add_query_arg('vmo_shortcut', 'windows', $login_url);
        $icon_download_url = add_query_arg('vmo_shortcut', 'icon', $login_url);
        $current_user = is_user_logged_in() ? wp_get_current_user() : null;

        status_header(200);
        nocache_headers();
        header('Content-Type: text/html; charset=' . get_bloginfo('charset'));
        ?>
<!doctype html>
<html <?php language_attributes(); ?>>
<head>
    <meta charset="<?php echo esc_attr(get_bloginfo('charset')); ?>">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="robots" content="noindex,nofollow">
    <title>VMO Login</title>
    <link rel="icon" href="<?php echo esc_url($icon_url); ?>" type="image/svg+xml">
    <link rel="manifest" href="<?php echo esc_url($manifest_url); ?>">
    <style>
        :root { color-scheme: light; }
        * { box-sizing: border-box; }
        body { margin: 0; min-height: 100vh; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #071923; color: #f7fbfc; display: grid; place-items: center; padding: 28px 16px; }
        .vmo-login-shell { width: min(100%, 980px); display: grid; grid-template-columns: minmax(0, 1fr) 380px; gap: 28px; align-items: stretch; }
        .vmo-login-main, .vmo-login-side { background: #ffffff; color: #10222c; border: 1px solid rgba(255,255,255,.12); border-radius: 8px; box-shadow: 0 24px 70px rgba(0,0,0,.28); }
        .vmo-login-main { padding: 34px; }
        .vmo-login-side { padding: 24px; background: #f3fbfd; }
        .vmo-brand { display: flex; align-items: center; gap: 14px; margin-bottom: 28px; }
        .vmo-brand img { width: 58px; height: 58px; object-fit: contain; }
        .vmo-brand strong { display: block; font-size: 22px; line-height: 1.05; }
        .vmo-brand span { color: #5e7582; font-size: 14px; }
        h1 { margin: 0 0 10px; font-size: 34px; line-height: 1.08; letter-spacing: 0; }
        p { color: #5e7582; line-height: 1.6; margin: 0 0 22px; }
        label { display: block; font-size: 13px; font-weight: 800; margin: 18px 0 8px; color: #173042; }
        input { width: 100%; border: 1px solid #cfe1e8; border-radius: 8px; padding: 14px 14px; font-size: 16px; color: #10222c; background: #fff; }
        input:focus { outline: 3px solid rgba(19,199,223,.2); border-color: #0b7d94; }
        /* VMO_LOGIN_PASSWORD_TOGGLE_RESTORE_v1 */
        .vmo-login-password-wrap { position: relative; display: flex; align-items: center; }
        .vmo-login-password-wrap input { padding-right: 76px; }
        .vmo-login-password-toggle { position: absolute; right: 10px; top: 50%; transform: translateY(-50%); border: 1px solid #9addeb; background: #e3f5f8; color: #0b6174; border-radius: 999px; padding: 5px 11px; font-size: 12px; font-weight: 900; cursor: pointer; }
        .vmo-login-password-toggle:focus { outline: 3px solid rgba(19,199,223,.2); }

        .vmo-error { background: #fff1f1; color: #a12424; border: 1px solid #ffd0d0; border-radius: 8px; padding: 12px 14px; margin-bottom: 18px; font-weight: 700; }
        .vmo-actions { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 24px; }
        .vmo-btn { appearance: none; border: 0; border-radius: 8px; min-height: 46px; padding: 0 18px; display: inline-flex; align-items: center; justify-content: center; text-decoration: none; font-size: 14px; font-weight: 900; cursor: pointer; white-space: nowrap; }
        .vmo-btn-primary { background: #0b7d94; color: #fff; }
        .vmo-btn-secondary { background: #e3f5f8; color: #0b6174; border: 1px solid #9addeb; }
        .vmo-btn-quiet { background: #fff; color: #173042; border: 1px solid #d7e5eb; }
        .vmo-shortcuts { display: grid; gap: 10px; margin-top: 18px; }
        .vmo-shortcuts .vmo-btn { width: 100%; }
        .vmo-copy { width: 100%; display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; margin-top: 18px; }
        .vmo-copy input { font-size: 13px; padding: 12px; }
        .vmo-muted { font-size: 13px; color: #6e8490; margin-top: 14px; }
        @media (max-width: 820px) { .vmo-login-shell { grid-template-columns: 1fr; } .vmo-login-main { padding: 26px 20px; } h1 { font-size: 28px; } .vmo-actions, .vmo-copy { grid-template-columns: 1fr; } .vmo-btn { width: 100%; } }
    </style>
</head>
<body>
    <main class="vmo-login-shell">
        <section class="vmo-login-main" aria-label="VMO login">
            <div class="vmo-brand">
                <img src="<?php echo esc_url($icon_url); ?>" alt="VMO">
                <div><strong>VMO</strong><span>AI Website Builder</span></div>
            </div>

            <?php if ($current_user && $current_user->exists()) : ?>
                <h1>Welcome back, <?php echo esc_html(function_exists('vmo_get_greeting_display_name') ? vmo_get_greeting_display_name() : ($current_user->display_name ?: $current_user->user_login)); ?></h1>
                <p>Your VMO workspace is ready.</p>
                <div class="vmo-actions">
                    <a class="vmo-btn vmo-btn-primary" href="<?php echo esc_url($builder_url); ?>">Continue To VMO</a>
                    <a class="vmo-btn vmo-btn-secondary" href="<?php echo esc_url(wp_logout_url($login_url)); ?>">Use A Different Account</a>
                </div>
            <?php else : ?>
                <h1>Sign in to VMO</h1>
                <p>Use the email and password you created during QuickLaunch.</p>
                <?php if ($error !== '') : ?>
                    <div class="vmo-error" role="alert"><?php echo esc_html($error); ?></div>
                <?php endif; ?>
                <form method="post" action="<?php echo esc_url($login_action_url); ?>">
                    <input type="hidden" name="vmo_login_action" value="1">
                    <input type="hidden" name="vmo_login_nonce" value="<?php echo esc_attr(wp_create_nonce('vmo_login')); ?>">
                    <input type="hidden" name="redirect_to" value="<?php echo esc_attr($redirect_to); ?>">
                    <label for="vmo-login-email">Email</label>
                    <input id="vmo-login-email" name="vmo_login" type="email" autocomplete="username" required>
                    <label for="vmo-login-password">Password</label>
                    <div class="vmo-login-password-wrap">
                        <input id="vmo-login-password" name="vmo_password" type="password" autocomplete="current-password" required>
                        <button class="vmo-login-password-toggle" type="button" data-vmo-login-password-toggle aria-label="Show password">Show</button>
                    </div>
                    <div class="vmo-actions">
                        <button class="vmo-btn vmo-btn-primary" type="submit">Enter VMO</button>
                    </div>
                </form>
                <p class="vmo-forgot-link" style="margin-top:18px;font-size:14px;">
                    <a href="<?php echo esc_url(add_query_arg('vmo_reset', 'request', $login_url)); ?>" style="color:#0b7d94;font-weight:700;text-decoration:underline;">Forgot your password?</a>
                </p>
            <?php endif; ?>
        </section>

        <aside class="vmo-login-side" aria-label="VMO shortcut options">
            <h2 style="margin:0 0 10px;font-size:22px;line-height:1.15;">VMO desktop entry</h2>
            <p>Keep this VMO login on your desktop or app launcher.</p>
            <div class="vmo-shortcuts">
                <a class="vmo-btn vmo-btn-secondary" href="<?php echo esc_url($mac_shortcut_url); ?>">Download Mac Shortcut</a>
                <a class="vmo-btn vmo-btn-secondary" href="<?php echo esc_url($windows_shortcut_url); ?>">Download Windows Shortcut</a>
                <a class="vmo-btn vmo-btn-quiet" href="<?php echo esc_url($icon_download_url); ?>">Download VMO Icon</a>
            </div>
            <div class="vmo-copy">
                <input id="vmo-login-link" type="text" readonly value="<?php echo esc_attr($login_url); ?>" aria-label="VMO login link">
                <button class="vmo-btn vmo-btn-quiet" type="button" id="vmo-copy-login">Copy</button>
            </div>
            <p class="vmo-muted">This page signs you directly into your VMO plugin on your hosted site.</p>
        </aside>
    </main>
    <script>
        (function() {
            var copyButton = document.getElementById('vmo-copy-login');
            // VMO_LOGIN_PASSWORD_TOGGLE_RESTORE_v1
            var pwdToggle = document.querySelector('[data-vmo-login-password-toggle]');
            if (pwdToggle && !pwdToggle.getAttribute('data-vmo-login-password-bound')) {
                pwdToggle.setAttribute('data-vmo-login-password-bound', '1');
                pwdToggle.addEventListener('click', function() {
                    var pwd = document.getElementById('vmo-login-password');
                    if (!pwd) { return; }
                    var showing = pwd.type === 'text';
                    pwd.type = showing ? 'password' : 'text';
                    pwdToggle.textContent = showing ? 'Show' : 'Hide';
                    pwdToggle.setAttribute('aria-label', showing ? 'Show password' : 'Hide password');
                });
            }

            var copyInput = document.getElementById('vmo-login-link');
            if (copyButton && copyInput) {
                copyButton.addEventListener('click', function() {
                    copyInput.select();
                    copyInput.setSelectionRange(0, copyInput.value.length);
                    try {
                        navigator.clipboard ? navigator.clipboard.writeText(copyInput.value) : document.execCommand('copy');
                        copyButton.textContent = 'Copied';
                    } catch (error) {
                        copyButton.textContent = 'Select Link';
                    }
                });
            }
        })();
    </script>
</body>
</html>
        <?php
        exit;
    }
}

if (!function_exists('vmo_quicklaunch_handle_vmo_login_page')) {
    function vmo_quicklaunch_handle_vmo_login_page() {
        if (!vmo_quicklaunch_is_login_path()) {
            return;
        }

        if (isset($_GET['vmo_manifest'])) {
            vmo_quicklaunch_output_manifest();
        }

        if (isset($_GET['vmo_shortcut'])) {
            vmo_quicklaunch_output_shortcut_download(sanitize_key((string) wp_unslash($_GET['vmo_shortcut'])));
        }

        // ── Self-serve password reset flow ──
        // Mode 'request' = "Forgot password?" landing form (enter email).
        // Mode 'confirm' = secure link the user clicks from their email
        // (?vmo_reset=confirm&key=<rp_key>&login=<user_login>) to choose
        // a new password. Uses WordPress core's battle-tested
        // retrieve_password() + check_password_reset_key() +
        // reset_password() so we inherit rate-limiting, secure token
        // generation, and email delivery without rolling our own.
        $reset_mode = isset($_GET['vmo_reset']) ? sanitize_key((string) wp_unslash($_GET['vmo_reset'])) : '';
        if ($reset_mode === 'request' || (isset($_POST['vmo_reset_action']) && $_POST['vmo_reset_action'] === 'request')) {
            vmo_quicklaunch_handle_password_reset_request();
            return;
        }
        if ($reset_mode === 'confirm' || (isset($_POST['vmo_reset_action']) && $_POST['vmo_reset_action'] === 'confirm')) {
            vmo_quicklaunch_handle_password_reset_confirm();
            return;
        }

        $error = '';
        if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['vmo_login_action'])) {
            $nonce = isset($_POST['vmo_login_nonce']) ? sanitize_text_field(wp_unslash((string) $_POST['vmo_login_nonce'])) : '';
            if (!wp_verify_nonce($nonce, 'vmo_login')) {
                $error = 'The login request expired. Please try again.';
            } else {
                $login = isset($_POST['vmo_login']) ? sanitize_text_field(wp_unslash((string) $_POST['vmo_login'])) : '';
                $password = isset($_POST['vmo_password']) ? (string) wp_unslash($_POST['vmo_password']) : '';
                $login_for_auth = $login;
                if (is_email($login)) {
                    $user_by_email = get_user_by('email', $login);
                    if ($user_by_email) {
                        $login_for_auth = $user_by_email->user_login;
                    }
                }

                $user = wp_signon(array(
                    'user_login' => $login_for_auth,
                    'user_password' => $password,
                    'remember' => true,
                ), is_ssl());

                if (is_wp_error($user)) {
                    $error = 'Email or password was not accepted.';
                } else {
                    $redirect_to = isset($_POST['redirect_to']) ? esc_url_raw(wp_unslash((string) $_POST['redirect_to'])) : admin_url('admin.php?page=vmo-ai-builder-main');
                    wp_safe_redirect(wp_validate_redirect($redirect_to, admin_url('admin.php?page=vmo-ai-builder-main')));
                    exit;
                }
            }
        }

        vmo_quicklaunch_render_vmo_login_page($error);
    }
}
add_action('template_redirect', 'vmo_quicklaunch_handle_vmo_login_page', 0);

if (!function_exists('vmo_quicklaunch_is_hosted_client_context')) {
    function vmo_quicklaunch_is_hosted_client_context() {
        return get_option('vmo_quicklaunch_hosted_client') === '1'
            || get_option('vmo_client_distribution_channel') === 'quicklaunch_hosted'
            || (defined('VMO_CLIENT_DISTRIBUTION_CHANNEL') && constant('VMO_CLIENT_DISTRIBUTION_CHANNEL') === 'quicklaunch_hosted');
    }
}

if (!function_exists('vmo_quicklaunch_is_hosted_client_login_context')) {
    function vmo_quicklaunch_is_hosted_client_login_context() {
        return vmo_quicklaunch_is_hosted_client_context();
    }
}

if (!function_exists('vmo_quicklaunch_filter_core_login_url')) {
    function vmo_quicklaunch_filter_core_login_url($login_url, $redirect = '', $force_reauth = false) {
        if (!vmo_quicklaunch_is_hosted_client_login_context()) {
            return $login_url;
        }

        $url = home_url('/vmo-login/');
        $args = array();
        if ($redirect !== '') {
            $args['redirect_to'] = $redirect;
        }
        if ($force_reauth) {
            $args['reauth'] = '1';
        }
        return !empty($args) ? add_query_arg($args, $url) : $url;
    }
    add_filter('login_url', 'vmo_quicklaunch_filter_core_login_url', 10, 3);
}

if (!function_exists('vmo_quicklaunch_filter_core_lostpassword_url')) {
    function vmo_quicklaunch_filter_core_lostpassword_url($lostpassword_url, $redirect = '') {
        if (!vmo_quicklaunch_is_hosted_client_login_context()) {
            return $lostpassword_url;
        }

        $url = add_query_arg('vmo_reset', 'request', home_url('/vmo-login/'));
        if ($redirect !== '') {
            $url = add_query_arg('redirect_to', $redirect, $url);
        }
        return $url;
    }
    add_filter('lostpassword_url', 'vmo_quicklaunch_filter_core_lostpassword_url', 10, 2);
}

if (!function_exists('vmo_quicklaunch_redirect_core_login_page')) {
    function vmo_quicklaunch_redirect_core_login_page() {
        if (!vmo_quicklaunch_is_hosted_client_login_context()) {
            return;
        }

        $method = strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET'));
        if ($method !== 'GET') {
            return;
        }

        $action = isset($_REQUEST['action']) ? sanitize_key((string) wp_unslash($_REQUEST['action'])) : 'login';
        if (in_array($action, array('logout', 'postpass'), true)) {
            return;
        }

        $redirect_to = isset($_REQUEST['redirect_to'])
            ? esc_url_raw(wp_unslash((string) $_REQUEST['redirect_to']))
            : admin_url('admin.php?page=vmo-ai-builder-main');

        if ($action === 'lostpassword') {
            wp_safe_redirect(add_query_arg('vmo_reset', 'request', home_url('/vmo-login/')), 302);
            exit;
        }

        if (in_array($action, array('rp', 'resetpass'), true)) {
            $args = array('vmo_reset' => 'confirm');
            if (isset($_REQUEST['key'])) {
                $args['key'] = sanitize_text_field(wp_unslash((string) $_REQUEST['key']));
            }
            if (isset($_REQUEST['login'])) {
                $args['login'] = sanitize_text_field(wp_unslash((string) $_REQUEST['login']));
            }
            wp_safe_redirect(add_query_arg($args, home_url('/vmo-login/')), 302);
            exit;
        }

        wp_safe_redirect(add_query_arg('redirect_to', $redirect_to, home_url('/vmo-login/')), 302);
        exit;
    }
    add_action('login_init', 'vmo_quicklaunch_redirect_core_login_page', 0);
}

/**
 * Handle the "Forgot password?" request form.
 *
 * Renders the form on GET, processes the email on POST. Uses
 * retrieve_password() which:
 *   - Validates the email belongs to a real user.
 *   - Generates a single-use, hashed reset key stored in user_activation_key.
 *   - Emails the user a reset link.
 *
 * We deliberately show a non-specific success message either way so the
 * page cannot be used to enumerate which emails have accounts on this
 * site. Errors only surface for malformed input.
 */
if (!function_exists('vmo_quicklaunch_handle_password_reset_request')) {
    function vmo_quicklaunch_handle_password_reset_request() {
        $error = '';
        $notice = '';

        if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['vmo_reset_action']) && $_POST['vmo_reset_action'] === 'request') {
            $nonce = isset($_POST['vmo_reset_nonce']) ? sanitize_text_field(wp_unslash((string) $_POST['vmo_reset_nonce'])) : '';
            if (!wp_verify_nonce($nonce, 'vmo_reset_request')) {
                $error = 'The reset request expired. Please try again.';
            } else {
                $email = isset($_POST['vmo_reset_email']) ? sanitize_email(wp_unslash((string) $_POST['vmo_reset_email'])) : '';
                if (!is_email($email)) {
                    $error = 'Please enter a valid email address.';
                } else {
                    // Mark this request so the retrieve_password_message
                    // filter knows to rewrite the link to /vmo-login/
                    // instead of the default /wp-login.php URL.
                    if (!defined('VMO_QUICKLAUNCH_PASSWORD_RESET_IN_PROGRESS')) {
                        define('VMO_QUICKLAUNCH_PASSWORD_RESET_IN_PROGRESS', true);
                    }

                    // Need wp-login.php's retrieve_password() helper.
                    // Loading it directly here is the standard way to
                    // trigger a password reset from outside wp-login.
                    if (!function_exists('retrieve_password')) {
                        require_once ABSPATH . 'wp-login.php';
                    }

                    // WP 5.7+ accepts an email arg directly. Fall back to
                    // simulating the POST so older versions still work.
                    if (function_exists('retrieve_password')) {
                        $reflection = new ReflectionFunction('retrieve_password');
                        if ($reflection->getNumberOfParameters() >= 1) {
                            $result = retrieve_password($email);
                        } else {
                            $_POST['user_login'] = $email;
                            $result = retrieve_password();
                        }
                    } else {
                        $result = new WP_Error('vmo_no_retrieve_password', 'Password reset helper unavailable.');
                    }

                    if (is_wp_error($result)) {
                        // Map a few common WP error codes to friendly copy
                        // but do not leak whether the email exists.
                        $code = $result->get_error_code();
                        if (in_array($code, ['invalidcombo', 'invalid_email'], true)) {
                            // Identical wording to the success path —
                            // hides email enumeration.
                            $notice = 'If an account exists for that email, we just sent a password reset link. Check your inbox (and spam folder).';
                        } else {
                            $error = 'We could not send the reset email right now. Please try again in a minute.';
                        }
                    } else {
                        $notice = 'If an account exists for that email, we just sent a password reset link. Check your inbox (and spam folder).';
                    }
                }
            }
        }

        vmo_quicklaunch_render_password_reset_request_page($error, $notice);
        exit;
    }
}

/**
 * Handle the reset link the user clicks from their email.
 *
 * GET shows a "choose a new password" form. POST validates the key/login
 * via check_password_reset_key() and sets the new password via
 * reset_password() (which also signs them out of all other sessions).
 */
if (!function_exists('vmo_quicklaunch_handle_password_reset_confirm')) {
    function vmo_quicklaunch_handle_password_reset_confirm() {
        $key = isset($_REQUEST['key']) ? sanitize_text_field(wp_unslash((string) $_REQUEST['key'])) : '';
        $login = isset($_REQUEST['login']) ? sanitize_text_field(wp_unslash((string) $_REQUEST['login'])) : '';
        $error = '';
        $notice = '';
        $success = false;

        $user = check_password_reset_key($key, $login);
        if (is_wp_error($user)) {
            // Token is missing/expired/already used — show a friendly
            // dead-end with a link back to request a fresh one rather
            // than letting the user fight an invalid form.
            vmo_quicklaunch_render_password_reset_confirm_page(
                $key,
                $login,
                'This password reset link has expired or already been used. Please request a new one.',
                '',
                false
            );
            exit;
        }

        if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['vmo_reset_action']) && $_POST['vmo_reset_action'] === 'confirm') {
            $nonce = isset($_POST['vmo_reset_nonce']) ? sanitize_text_field(wp_unslash((string) $_POST['vmo_reset_nonce'])) : '';
            if (!wp_verify_nonce($nonce, 'vmo_reset_confirm')) {
                $error = 'The reset form expired. Please reload this page.';
            } else {
                $pass1 = isset($_POST['vmo_new_password']) ? (string) wp_unslash($_POST['vmo_new_password']) : '';
                $pass2 = isset($_POST['vmo_new_password_confirm']) ? (string) wp_unslash($_POST['vmo_new_password_confirm']) : '';
                if ($pass1 === '' || $pass2 === '') {
                    $error = 'Please fill in both password fields.';
                } elseif ($pass1 !== $pass2) {
                    $error = 'The two passwords do not match.';
                } elseif (strlen($pass1) < 8) {
                    $error = 'Choose a password that is at least 8 characters long.';
                } else {
                    reset_password($user, $pass1);
                    $success = true;
                    $notice = 'Your password has been updated. You can now sign in.';
                }
            }
        }

        vmo_quicklaunch_render_password_reset_confirm_page($key, $login, $error, $notice, $success);
        exit;
    }
}

/**
 * Rewrite the password-reset email link so it points to /vmo-login/
 * (our branded flow) instead of /wp-login.php (raw WordPress).
 *
 * Hooked only when VMO_QUICKLAUNCH_PASSWORD_RESET_IN_PROGRESS is defined
 * so we never alter password-reset emails that originate from elsewhere
 * (e.g. wp-admin Users screen). This keeps the change scoped and safe.
 */
if (!function_exists('vmo_quicklaunch_filter_reset_email')) {
    function vmo_quicklaunch_filter_reset_email($message, $key, $user_login, $user_data) {
        if (!defined('VMO_QUICKLAUNCH_PASSWORD_RESET_IN_PROGRESS')) {
            return $message;
        }
        $login_url = function_exists('vmo_quicklaunch_get_login_url') ? vmo_quicklaunch_get_login_url() : home_url('/vmo-login/');
        $reset_url = add_query_arg([
            'vmo_reset' => 'confirm',
            'key'       => $key,
            'login'     => rawurlencode($user_login),
        ], $login_url);

        // Replace any wp-login.php?action=rp link WP inserted.
        $message = preg_replace('#https?://\S+wp-login\.php\?action=rp[^\s>]*#', $reset_url, $message);

        // Also append our branded link at the end in case the default
        // template was customised and the regex missed it.
        $message .= "\n\n--- VMO ---\nTo set a new password, open this link:\n" . $reset_url . "\n";
        return $message;
    }
    add_filter('retrieve_password_message', 'vmo_quicklaunch_filter_reset_email', 10, 4);
}

/**
 * Render the "Forgot your password?" form.
 */
if (!function_exists('vmo_quicklaunch_render_password_reset_request_page')) {
    function vmo_quicklaunch_render_password_reset_request_page($error = '', $notice = '') {
        $login_url = vmo_quicklaunch_get_login_url();
        $icon_url = VMO_AI_BUILDER_URL . 'assets/vmo-icon.svg';
        status_header(200);
        nocache_headers();
        header('Content-Type: text/html; charset=' . get_bloginfo('charset'));
        ?>
<!doctype html>
<html <?php language_attributes(); ?>>
<head>
    <meta charset="<?php echo esc_attr(get_bloginfo('charset')); ?>">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="robots" content="noindex,nofollow">
    <title>Reset your VMO password</title>
    <link rel="icon" href="<?php echo esc_url($icon_url); ?>" type="image/svg+xml">
    <style>
        :root { color-scheme: light; }
        * { box-sizing: border-box; }
        body { margin: 0; min-height: 100vh; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #071923; color: #f7fbfc; display: grid; place-items: center; padding: 28px 16px; }
        .vmo-card { width: min(100%, 480px); background: #fff; color: #10222c; border-radius: 12px; padding: 34px; box-shadow: 0 24px 70px rgba(0,0,0,.28); }
        .vmo-brand { display: flex; align-items: center; gap: 14px; margin-bottom: 22px; }
        .vmo-brand img { width: 50px; height: 50px; }
        .vmo-brand strong { display: block; font-size: 20px; }
        .vmo-brand span { color: #5e7582; font-size: 13px; }
        h1 { margin: 0 0 8px; font-size: 26px; }
        p.lead { color: #5e7582; line-height: 1.55; margin: 0 0 22px; }
        label { display: block; font-size: 13px; font-weight: 800; margin: 14px 0 8px; color: #173042; }
        input[type=email], input[type=password] { width: 100%; border: 1px solid #cfe1e8; border-radius: 8px; padding: 14px; font-size: 16px; color: #10222c; }
        input:focus { outline: 3px solid rgba(19,199,223,.2); border-color: #0b7d94; }
        .vmo-notice { background: #ecfdf5; color: #065f46; border: 1px solid #a7f3d0; border-radius: 8px; padding: 12px 14px; margin-bottom: 16px; font-weight: 600; }
        .vmo-error { background: #fff1f1; color: #a12424; border: 1px solid #ffd0d0; border-radius: 8px; padding: 12px 14px; margin-bottom: 16px; font-weight: 700; }
        .vmo-btn { width: 100%; appearance: none; border: 0; border-radius: 8px; min-height: 48px; padding: 0 18px; font-size: 15px; font-weight: 900; cursor: pointer; background: #0b7d94; color: #fff; margin-top: 18px; }
        .vmo-back { display: block; text-align: center; margin-top: 18px; font-size: 14px; color: #0b7d94; font-weight: 700; }
    </style>
</head>
<body>
    <div class="vmo-card">
        <div class="vmo-brand">
            <img src="<?php echo esc_url($icon_url); ?>" alt="VMO">
            <div><strong>VMO</strong><span>AI Website Builder</span></div>
        </div>
        <h1>Reset your password</h1>
        <p class="lead">Enter the email address you used during QuickLaunch. We will email you a secure link to choose a new password.</p>
        <?php if ($notice !== '') : ?>
            <div class="vmo-notice" role="status"><?php echo esc_html($notice); ?></div>
        <?php endif; ?>
        <?php if ($error !== '') : ?>
            <div class="vmo-error" role="alert"><?php echo esc_html($error); ?></div>
        <?php endif; ?>
        <form method="post" action="<?php echo esc_url(add_query_arg('vmo_reset', 'request', $login_url)); ?>">
            <input type="hidden" name="vmo_reset_action" value="request">
            <input type="hidden" name="vmo_reset_nonce" value="<?php echo esc_attr(wp_create_nonce('vmo_reset_request')); ?>">
            <label for="vmo-reset-email">Email address</label>
            <input id="vmo-reset-email" name="vmo_reset_email" type="email" autocomplete="email" required>
            <button class="vmo-btn" type="submit">Send reset link</button>
        </form>
        <a class="vmo-back" href="<?php echo esc_url($login_url); ?>">&larr; Back to sign in</a>
    </div>
</body>
</html>
        <?php
    }
}

/**
 * Render the "Set a new password" form (linked from reset email).
 */
if (!function_exists('vmo_quicklaunch_render_password_reset_confirm_page')) {
    function vmo_quicklaunch_render_password_reset_confirm_page($key, $login, $error = '', $notice = '', $success = false) {
        $login_url = vmo_quicklaunch_get_login_url();
        $icon_url = VMO_AI_BUILDER_URL . 'assets/vmo-icon.svg';
        $request_url = add_query_arg('vmo_reset', 'request', $login_url);
        $form_action = add_query_arg([
            'vmo_reset' => 'confirm',
            'key'       => $key,
            'login'     => rawurlencode($login),
        ], $login_url);
        status_header(200);
        nocache_headers();
        header('Content-Type: text/html; charset=' . get_bloginfo('charset'));
        ?>
<!doctype html>
<html <?php language_attributes(); ?>>
<head>
    <meta charset="<?php echo esc_attr(get_bloginfo('charset')); ?>">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="robots" content="noindex,nofollow">
    <title>Choose a new VMO password</title>
    <link rel="icon" href="<?php echo esc_url($icon_url); ?>" type="image/svg+xml">
    <style>
        :root { color-scheme: light; }
        * { box-sizing: border-box; }
        body { margin: 0; min-height: 100vh; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #071923; color: #f7fbfc; display: grid; place-items: center; padding: 28px 16px; }
        .vmo-card { width: min(100%, 480px); background: #fff; color: #10222c; border-radius: 12px; padding: 34px; box-shadow: 0 24px 70px rgba(0,0,0,.28); }
        .vmo-brand { display: flex; align-items: center; gap: 14px; margin-bottom: 22px; }
        .vmo-brand img { width: 50px; height: 50px; }
        .vmo-brand strong { display: block; font-size: 20px; }
        .vmo-brand span { color: #5e7582; font-size: 13px; }
        h1 { margin: 0 0 8px; font-size: 26px; }
        p.lead { color: #5e7582; line-height: 1.55; margin: 0 0 22px; }
        label { display: block; font-size: 13px; font-weight: 800; margin: 14px 0 8px; color: #173042; }
        input[type=password] { width: 100%; border: 1px solid #cfe1e8; border-radius: 8px; padding: 14px; font-size: 16px; color: #10222c; }
        input:focus { outline: 3px solid rgba(19,199,223,.2); border-color: #0b7d94; }
        .vmo-notice { background: #ecfdf5; color: #065f46; border: 1px solid #a7f3d0; border-radius: 8px; padding: 12px 14px; margin-bottom: 16px; font-weight: 600; }
        .vmo-error { background: #fff1f1; color: #a12424; border: 1px solid #ffd0d0; border-radius: 8px; padding: 12px 14px; margin-bottom: 16px; font-weight: 700; }
        .vmo-btn { width: 100%; appearance: none; border: 0; border-radius: 8px; min-height: 48px; padding: 0 18px; font-size: 15px; font-weight: 900; cursor: pointer; background: #0b7d94; color: #fff; margin-top: 18px; text-decoration: none; display: inline-flex; align-items: center; justify-content: center; }
        .vmo-btn.secondary { background: #e3f5f8; color: #0b6174; }
        .vmo-back { display: block; text-align: center; margin-top: 18px; font-size: 14px; color: #0b7d94; font-weight: 700; }
        .vmo-hint { font-size: 12px; color: #6e8490; margin-top: 4px; }
    </style>
</head>
<body>
    <div class="vmo-card">
        <div class="vmo-brand">
            <img src="<?php echo esc_url($icon_url); ?>" alt="VMO">
            <div><strong>VMO</strong><span>AI Website Builder</span></div>
        </div>
        <?php if ($success) : ?>
            <h1>Password updated</h1>
            <p class="lead"><?php echo esc_html($notice); ?></p>
            <a class="vmo-btn" href="<?php echo esc_url($login_url); ?>">Sign in to VMO</a>
        <?php elseif ($error !== '' && stripos($error, 'expired') !== false) : ?>
            <h1>Link expired</h1>
            <p class="lead"><?php echo esc_html($error); ?></p>
            <a class="vmo-btn" href="<?php echo esc_url($request_url); ?>">Request a new reset link</a>
            <a class="vmo-back" href="<?php echo esc_url($login_url); ?>">&larr; Back to sign in</a>
        <?php else : ?>
            <h1>Choose a new password</h1>
            <p class="lead">Pick something you will remember. At least 8 characters. After saving you will be returned to the sign-in page.</p>
            <?php if ($error !== '') : ?>
                <div class="vmo-error" role="alert"><?php echo esc_html($error); ?></div>
            <?php endif; ?>
            <form method="post" action="<?php echo esc_url($form_action); ?>">
                <input type="hidden" name="vmo_reset_action" value="confirm">
                <input type="hidden" name="vmo_reset_nonce" value="<?php echo esc_attr(wp_create_nonce('vmo_reset_confirm')); ?>">
                <label for="vmo-new-password">New password</label>
                <input id="vmo-new-password" name="vmo_new_password" type="password" autocomplete="new-password" required minlength="8">
                <p class="vmo-hint">Use at least 8 characters. Letters, numbers, and symbols all work.</p>
                <label for="vmo-new-password-confirm">Confirm new password</label>
                <input id="vmo-new-password-confirm" name="vmo_new_password_confirm" type="password" autocomplete="new-password" required minlength="8">
                <button class="vmo-btn" type="submit">Save new password</button>
            </form>
            <a class="vmo-back" href="<?php echo esc_url($login_url); ?>">&larr; Back to sign in</a>
        <?php endif; ?>
    </div>
</body>
</html>
        <?php
    }
}

/**
 * Intercept the public marketing route /get-started/ and serve the document-root
 * get-started.php wrapper directly, bypassing any WordPress page that may have
 * claimed that slug (e.g. a stale Posts Page or auto-generated blog archive that
 * WP defaults to when the slug has no matching page).
 *
 * This runs at parse_request (priority 0) so it short-circuits WordPress BEFORE
 * the query is built — guaranteeing the wrapper renders regardless of whether
 * the custom root index.php router is in place on the host (some hosts route
 * directly to WordPress's index.php and bypass it).
 *
 * Skips: admin, REST, AJAX, cron, the iframe-embed request (?vmo_quicklaunch_embed=1)
 * which is what the get-started.php wrapper itself loads inside its <iframe>.
 */
add_action('parse_request', function ($wp) {
    if (is_admin() || (defined('DOING_AJAX') && DOING_AJAX) || (defined('DOING_CRON') && DOING_CRON) || (defined('REST_REQUEST') && REST_REQUEST)) {
        return;
    }
    if (!empty($_GET['vmo_quicklaunch_embed'])) {
        return; // iframe content request — let WP render the embed page
    }

    $request_uri = isset($_SERVER['REQUEST_URI']) ? (string) $_SERVER['REQUEST_URI'] : '';
    $path = parse_url($request_uri, PHP_URL_PATH);
    if (!is_string($path) || $path === '') {
        return;
    }
    $normalized = rtrim($path, '/');
    if ($normalized !== '/get-started') {
        return;
    }

    // Resolve the wrapper. ABSPATH is the WP root; the wrapper sits next to it
    // on Kinsta (document root). Fall back to a couple of common layouts.
    $candidates = [];
    if (defined('ABSPATH')) {
        $candidates[] = rtrim(ABSPATH, '/\\') . '/get-started.php';
        $candidates[] = dirname(rtrim(ABSPATH, '/\\')) . '/get-started.php';
    }
    $candidates[] = dirname(__DIR__, 3) . '/get-started.php';

    foreach ($candidates as $wrapper) {
        if ($wrapper && is_file($wrapper)) {
            // Suppress the WP admin bar from injecting itself for logged-in users
            // since the wrapper renders its own full HTML document.
            add_filter('show_admin_bar', '__return_false');
            // Prevent WP's main query / template loader from running afterward.
            status_header(200);
            nocache_headers();
            require $wrapper;
            exit;
        }
    }
}, 0);

if (!function_exists('vmo_quicklaunch_filter_login_url')) {
    function vmo_quicklaunch_filter_login_url($login_url, $redirect, $force_reauth) {
        if ((string) get_option('vmo_quicklaunch_hosted_client', '') !== '1') {
            return $login_url;
        }

        $target = $redirect ? $redirect : admin_url('admin.php?page=vmo-ai-builder-main');
        $target = wp_validate_redirect($target, admin_url('admin.php?page=vmo-ai-builder-main'));
        return add_query_arg('redirect_to', $target, vmo_quicklaunch_get_login_url());
    }
}
add_filter('login_url', 'vmo_quicklaunch_filter_login_url', 10, 3);

/**
 * Handle Stripe Connect return / refresh URLs.
 *
 * When Stripe onboarding redirects back to:
 *   - home_url('/?vmo_stripe_return=1&account_id=...')  (from provision flow)
 *   - home_url('/?vmo_stripe_refresh=1&account_id=...')  (onboarding expired / needs retry)
 *
 * This handler syncs the account status and sends the user to the WP admin dashboard
 * with a clear success or retry message.
 */
add_action('template_redirect', function () {
    $is_return  = isset($_GET['vmo_stripe_return'])  && $_GET['vmo_stripe_return'] == '1';
    $is_refresh = isset($_GET['vmo_stripe_refresh']) && $_GET['vmo_stripe_refresh'] == '1';

    if (!$is_return && !$is_refresh) {
        return;
    }

    $account_id = isset($_GET['account_id']) ? sanitize_text_field(wp_unslash($_GET['account_id'])) : '';
    $dashboard  = admin_url('admin.php?page=vmo-ai-dashboard');

    // ----- REFRESH: onboarding link expired, generate a new one -----
    if ($is_refresh && $account_id) {
        try {
            if (class_exists('VMO_Stripe_Connect')) {
                require_once VMO_AI_BUILDER_DIR . 'stripe-sdk/init.php';
                $sk = get_option('vmo_stripe_secret_key');
                if ($sk) {
                    \Stripe\Stripe::setApiKey($sk);
                    $link = \Stripe\AccountLink::create([
                        'account'     => $account_id,
                        'refresh_url' => home_url('/?vmo_stripe_refresh=1&account_id=' . $account_id),
                        'return_url'  => home_url('/?vmo_stripe_return=1&account_id=' . $account_id),
                        'type'        => 'account_onboarding',
                    ]);
                    wp_redirect($link->url);
                    exit;
                }
            }
        } catch (Throwable $e) {
            // Fall through to dashboard with error notice
        }
        wp_redirect(add_query_arg('stripe_onboard', 'refresh_error', $dashboard));
        exit;
    }

    // ----- RETURN: user finished Stripe onboarding, sync status -----
    if ($is_return) {
        // Sync account status to DB if possible
        if ($account_id) {
            try {
                require_once VMO_AI_BUILDER_DIR . 'stripe-sdk/init.php';
                $sk = get_option('vmo_stripe_secret_key');
                if ($sk) {
                    \Stripe\Stripe::setApiKey($sk);
                    $acct = \Stripe\Account::retrieve($account_id);

                    global $wpdb;
                    $table = $wpdb->prefix . 'vmo_connected_accounts';
                    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                    $wpdb->update($table, [
                        'details_submitted' => intval($acct->details_submitted),
                        'charges_enabled'   => intval($acct->charges_enabled),
                        'payouts_enabled'   => intval($acct->payouts_enabled),
                        'capabilities'      => wp_json_encode($acct->capabilities),
                    ], ['stripe_account_id' => $account_id]);
                }
            } catch (Throwable $e) {
                // Non-blocking: webhook will eventually sync anyway
            }
        }

        wp_redirect(add_query_arg('stripe_onboard', 'return', $dashboard));
        exit;
    }
});

// Ensure Twilio can reach our public REST webhooks even if another plugin
// globally blocks REST requests (common cause of Twilio "application error").
add_filter('rest_authentication_errors', function ($result) {
    // If nothing has blocked REST, do nothing.
    if (empty($result)) {
        return $result;
    }

    $uri = isset($_SERVER['REQUEST_URI']) ? (string) $_SERVER['REQUEST_URI'] : '';
    if ($uri === '') {
        return $result;
    }

    // Only bypass for specific public Twilio endpoints.
    $public_paths = array(
        '/wp-json/vmo/v1/twilio-voice-webhook',
        '/wp-json/vmo/v1/twilio-status-callback',
        '/wp-json/vmo/v1/twilio-sms-webhook',
        '/wp-json/vmo/v1/dial-status-callback',
        '/wp-json/vmo/v1/twilio-transcription-callback',
        '/wp-json/vmo/v1/recording-callback',
        '/wp-json/vmo/v1/power-dialer/twiml',
        '/wp-json/vmo/v1/power-dialer/outbound-callback',
    );

    foreach ($public_paths as $path) {
        if (strpos($uri, $path) !== false) {
            return null;
        }
    }

    return $result;
}, 99);

// MASTER VERSION FLAG
// This version has full access to all features without subscription checks
// Set to false in client/distribution version via create_client_zip.php
if (!defined('VMO_MASTER_VERSION')) {
    define('VMO_MASTER_VERSION', false);
}

// Define plugin file constant for activation hooks
if (!defined('VMO_PLUGIN_FILE')) {
    define('VMO_PLUGIN_FILE', __FILE__);
}

// Define plugin directory constants with fallback for early loading
if (!defined('VMO_AI_BUILDER_DIR')) {
    if (function_exists('plugin_dir_path')) {
        define('VMO_AI_BUILDER_DIR', plugin_dir_path(__FILE__));
    } else {
        define('VMO_AI_BUILDER_DIR', dirname(__FILE__) . '/');
    }
}

if (!defined('VMO_AI_BUILDER_URL')) {
    if (function_exists('plugin_dir_url')) {
        define('VMO_AI_BUILDER_URL', plugin_dir_url(__FILE__));
    } else {
        // Fallback URL - will be corrected when WordPress loads
        define('VMO_AI_BUILDER_URL', '');
    }
}
// Back-compat constants for auxiliary classes
if (!defined('VMO_PLUGIN_DIR')) {
    define('VMO_PLUGIN_DIR', VMO_AI_BUILDER_DIR);
}
if (!defined('VMO_PLUGIN_URL')) {
    define('VMO_PLUGIN_URL', VMO_AI_BUILDER_URL);
}

// The enhanced wizard owns consultation flow; retired handlers are not loaded.

// Register every text domain this plugin uses so WP's JIT loader never
// has to resolve them from a null path.
if (!function_exists('vmo_register_textdomains')) {
    function vmo_register_textdomains() {
        $domain_path = dirname(plugin_basename(__FILE__)) . '/languages';
        foreach (['vmo-ai-builder', 'vmo', 'vmo-consultant'] as $textdomain) {
            load_plugin_textdomain($textdomain, false, $domain_path);
        }
    }
}
add_action('init', 'vmo_register_textdomains', 1);

// === VMO Modular Architecture (Dec 2025) ===
// Load Core Services and Modules
$vmo_includes_dir = plugin_dir_path(__FILE__) . 'includes/';

// VMO Part 6 installable header/route fix: load the Master <-> Operations bridge early.
// This keeps the public REST health route available even before other Master-only modules finish booting.
if (file_exists($vmo_includes_dir . 'class-vmo-master-operations-bridge.php')) {
    require_once $vmo_includes_dir . 'class-vmo-master-operations-bridge.php';
    if (class_exists('VMO_Master_Operations_Bridge')) {
        VMO_Master_Operations_Bridge::instance();
    }
}

if (file_exists($vmo_includes_dir . 'vmo-platform-faq.php')) {
    require_once $vmo_includes_dir . 'vmo-platform-faq.php';
}

// === CLIENT MODE: Load Client Bootstrap (interceptor + auto-updater) ===
// This file no-ops on master. On client instances it (a) transparently routes
// every api.openai.com / api.stripe.com / api.twilio.com call through the
// master proxy at getvmo.com so client instances never need provider keys, and
// (b) registers an auto-update channel against master so new plugin versions
// are pulled automatically on the next admin page load — no manual reinstall.
if (file_exists($vmo_includes_dir . 'class-vmo-client-bootstrap.php')) {
    require_once $vmo_includes_dir . 'class-vmo-client-bootstrap.php';
    if (class_exists('VMO_Client_Bootstrap')) {
        VMO_Client_Bootstrap::boot(__FILE__);
    }
}

// === CLIENT MODE: automatic instance registry + entitlement/discount sync ===
// Bundled into every QuickLaunch/Direct/Staging client build so instances
// self-register with Master and never disappear from Operations after updates.
if (file_exists($vmo_includes_dir . 'class-vmo-client-sync-core.php')) {
    require_once $vmo_includes_dir . 'class-vmo-client-sync-core.php';
    if (class_exists('VMO_Client_Sync_Core')) {
        VMO_Client_Sync_Core::boot();
    }
}

// === MASTER MODE: Load API Proxy and Client Manager ===
// These classes handle incoming requests from client installations
if (defined('VMO_MASTER_VERSION') && VMO_MASTER_VERSION === true && !(defined('VMO_CLIENT_MODE') && VMO_CLIENT_MODE === true)) {
    // API Proxy Server - routes client API requests to providers
    if (file_exists($vmo_includes_dir . 'class-vmo-api-proxy.php')) {
        require_once $vmo_includes_dir . 'class-vmo-api-proxy.php';
    }
    
    // Master Client Manager - handles license validation, subscription sync
    if (file_exists($vmo_includes_dir . 'class-vmo-master-client-manager.php')) {
        require_once $vmo_includes_dir . 'class-vmo-master-client-manager.php';
    }

    // Master Update Manager - publishes client plugin releases and update manifests
    if (file_exists($vmo_includes_dir . 'class-vmo-master-update-manager.php')) {
        require_once $vmo_includes_dir . 'class-vmo-master-update-manager.php';
        if (class_exists('VMO_Master_Update_Manager')) {
            VMO_Master_Update_Manager::instance();
        }
    }

    // Master CRM Sync - Auto-sync VMO Users to CRM Contacts
    if (file_exists($vmo_includes_dir . 'class-vmo-master-sync.php')) {
        require_once $vmo_includes_dir . 'class-vmo-master-sync.php';
    }
    
    // Master ↔ Operations signed bridge authority.
    // The old VMO Platform admin UI is intentionally not loaded on Master after the split.
    if (file_exists($vmo_includes_dir . 'class-vmo-master-operations-bridge.php')) {
        require_once $vmo_includes_dir . 'class-vmo-master-operations-bridge.php';
        if (class_exists('VMO_Master_Operations_Bridge')) {
            VMO_Master_Operations_Bridge::instance();
        }
    }
    
    // Customer Service & Support System - Jarvis AI, Live Chat, Feedback
    // Removed per user request
}

if (file_exists($vmo_includes_dir . 'modules/core/class-vmo-service.php')) {
    require_once $vmo_includes_dir . 'modules/core/class-vmo-service.php';
}

if (file_exists($vmo_includes_dir . 'orchestration/class-vmo-plan-policy.php')) {
    require_once $vmo_includes_dir . 'orchestration/class-vmo-plan-policy.php';
}

if (file_exists($vmo_includes_dir . 'orchestration/class-vmo-booster-catalog.php')) {
    require_once $vmo_includes_dir . 'orchestration/class-vmo-booster-catalog.php';
}

if (file_exists($vmo_includes_dir . 'orchestration/class-vmo-model-router.php')) {
    require_once $vmo_includes_dir . 'orchestration/class-vmo-model-router.php';
}

if (file_exists($vmo_includes_dir . 'state/class-vmo-jurisdiction-engine.php')) {
    require_once $vmo_includes_dir . 'state/class-vmo-jurisdiction-engine.php';
}

if (file_exists($vmo_includes_dir . 'state/class-vmo-master-business-brief.php')) {
    require_once $vmo_includes_dir . 'state/class-vmo-master-business-brief.php';
}

$vmo_promotion_core_includes = array(
    'promotions/connectors/interface-vmo-promotion-provider-connector.php',
    'promotions/connectors/class-vmo-promotion-base-connector.php',
);

foreach ($vmo_promotion_core_includes as $promotion_include) {
    $promotion_path = $vmo_includes_dir . $promotion_include;
    if (file_exists($promotion_path)) {
        require_once $promotion_path;
    } else {
        error_log('VMO Promotions: missing required include ' . $promotion_include . '; promotion connectors disabled for this request.');
    }
}

$vmo_promotions_ready = interface_exists('VMO_Promotion_Provider_Connector', false)
    && class_exists('VMO_Promotion_Base_Connector', false);

if ($vmo_promotions_ready) {
    $vmo_require_promotion_include = function ($promotion_include) use ($vmo_includes_dir) {
        $promotion_path = $vmo_includes_dir . $promotion_include;
        if (file_exists($promotion_path)) {
            require_once $promotion_path;
            return true;
        }

        error_log('VMO Promotions: optional include missing ' . $promotion_include . '; continuing without it.');
        return false;
    };

    $vmo_require_promotion_include('promotions/connectors/class-vmo-promotion-configured-connector.php');
    $vmo_require_promotion_include('promotions/connectors/class-vmo-promotion-null-connector.php');

    if (class_exists('VMO_Promotion_Configured_Connector', false)) {
        $vmo_require_promotion_include('promotions/connectors/class-vmo-promotion-oauth2-connector.php');

        foreach (array(
            'promotions/connectors/class-vmo-promotion-telegram-connector.php',
            'promotions/connectors/class-vmo-promotion-discord-connector.php',
        ) as $promotion_include) {
            $vmo_require_promotion_include($promotion_include);
        }

        if (class_exists('VMO_Promotion_OAuth2_Connector', false)) {
            foreach (array(
                'promotions/connectors/class-vmo-promotion-meta-connector.php',
                'promotions/connectors/class-vmo-promotion-linkedin-connector.php',
                'promotions/connectors/class-vmo-promotion-google-business-profile-connector.php',
                'promotions/connectors/class-vmo-promotion-youtube-connector.php',
                'promotions/connectors/class-vmo-promotion-mastodon-connector.php',
                'promotions/connectors/class-vmo-promotion-tiktok-connector.php',
                'promotions/connectors/class-vmo-promotion-x-connector.php',
                'promotions/connectors/class-vmo-promotion-pinterest-connector.php',
                'promotions/connectors/class-vmo-promotion-reddit-connector.php',
                'promotions/connectors/class-vmo-promotion-tumblr-connector.php',
            ) as $promotion_include) {
                $vmo_require_promotion_include($promotion_include);
            }
        } else {
            error_log('VMO Promotions: OAuth2 connector base unavailable; OAuth promotion connectors disabled for this request.');
        }
    } else {
        error_log('VMO Promotions: configured connector unavailable; provider-specific promotion connectors disabled for this request.');
    }

    foreach (array(
        'promotions/class-vmo-promotion-admin-settings.php',
        'promotions/class-vmo-promotion-provider-registry.php',
        'promotions/class-vmo-promotion-data-store.php',
        'promotions/class-vmo-promotion-orchestrator.php',
        'promotions/class-vmo-promotion-execution-engine.php',
        'promotions/class-vmo-promotion-ad-execution-engine.php',
    ) as $promotion_include) {
        $vmo_require_promotion_include($promotion_include);
    }
} else {
    error_log('VMO Promotions: connector base dependencies are unavailable; continuing plugin boot without promotion connectors.');
}

if (file_exists($vmo_includes_dir . 'qa/class-vmo-website-qa.php')) {
    require_once $vmo_includes_dir . 'qa/class-vmo-website-qa.php';
}

if (file_exists($vmo_includes_dir . 'class-vmo-website-media-manifest.php')) {
    require_once $vmo_includes_dir . 'class-vmo-website-media-manifest.php';
}

if (file_exists($vmo_includes_dir . 'qa/class-vmo-website-final-qa-gate.php')) {
    require_once $vmo_includes_dir . 'qa/class-vmo-website-final-qa-gate.php';
}

if (file_exists($vmo_includes_dir . 'orchestration/class-vmo-approval-guard.php')) {
    require_once $vmo_includes_dir . 'orchestration/class-vmo-approval-guard.php';
}

if (file_exists($vmo_includes_dir . 'orchestration/class-vmo-agent-orchestrator.php')) {
    require_once $vmo_includes_dir . 'orchestration/class-vmo-agent-orchestrator.php';
}

if (file_exists($vmo_includes_dir . 'modules/services/class-vmo-openai-service.php')) {
    require_once $vmo_includes_dir . 'modules/services/class-vmo-openai-service.php';
}

if (file_exists($vmo_includes_dir . 'modules/services/class-vmo-twilio-service.php')) {
    require_once $vmo_includes_dir . 'modules/services/class-vmo-twilio-service.php';
}

// === LOAD TWILIO MANAGER (The class that handles phone provisioning) ===
$twilio_manager_file = $vmo_includes_dir . 'class-vmo-twilio-manager.php';
if (file_exists($twilio_manager_file)) {
    require_once $twilio_manager_file;
    // Instantiate it immediately so its hooks are registered
    if (class_exists('VMO_Twilio_Manager')) {
        VMO_Twilio_Manager::get_instance();
    }
} else {
    if (defined('WP_DEBUG') && WP_DEBUG) {
        error_log('VMO: Missing file - class-vmo-twilio-manager.php');
    }
}

if (file_exists($vmo_includes_dir . 'modules/subscription/class-vmo-subscription-manager.php')) {
    require_once $vmo_includes_dir . 'modules/subscription/class-vmo-subscription-manager.php';
}

if (file_exists($vmo_includes_dir . 'modules/generator/class-vmo-generator-engine.php')) {
    require_once $vmo_includes_dir . 'modules/generator/class-vmo-generator-engine.php';
}

// === STANDALONE MODULE: HTML TO PDF PUBLISHER ===
// For VMO Master/Bonded Distributions, load the converter natively.
$vmo_pdf_module = VMO_AI_BUILDER_DIR . 'vmo-html-to-pdf-converter/vmo-html-to-pdf.php';
if (file_exists($vmo_pdf_module)) {
    require_once $vmo_pdf_module;
}

// Initialize Modules
global $vmo_subscription_manager, $vmo_generator_engine, $vmo_affiliate_manager;

if (class_exists('VMO_Subscription_Manager')) {
    $vmo_subscription_manager = new VMO_Subscription_Manager();
}

if (class_exists('VMO_Generator_Engine')) {
    $vmo_generator_engine = new VMO_Generator_Engine();
}

// === Affiliate System (Dec 2025) ===
// ✅ Safe loading - prevent crashes if file doesn't exist on server
$affiliate_file = VMO_AI_BUILDER_DIR . 'includes/class-vmo-affiliate-manager.php';
global $vmo_affiliate_manager;
$vmo_affiliate_manager = null;

if (file_exists($affiliate_file)) {
    require_once $affiliate_file;
    if (class_exists('VMO_Affiliate_Manager')) {
        $vmo_affiliate_manager = new VMO_Affiliate_Manager();
    }
} else {
    if (defined('WP_DEBUG') && WP_DEBUG) {
        error_log('VMO: Missing file - class-vmo-affiliate-manager.php (expected at: ' . $affiliate_file . ')');
    }
}

// === Elite 8-Phase Consultation System (Jan 2025) ===
// ✅ Safe loading for phase/consultation files
$phase_output_file = VMO_AI_BUILDER_DIR . 'includes/class-vmo-phase-output-manager.php';
$elite_prompts_file = VMO_AI_BUILDER_DIR . 'includes/class-vmo-elite-consultation-prompts.php';

if (file_exists($phase_output_file)) {
    require_once $phase_output_file;
} else {
    if (defined('WP_DEBUG') && WP_DEBUG) {
        error_log('VMO: Missing file - class-vmo-phase-output-manager.php');
    }
}

if (file_exists($elite_prompts_file)) {
    require_once $elite_prompts_file;
} else {
    if (defined('WP_DEBUG') && WP_DEBUG) {
        error_log('VMO: Missing file - class-vmo-elite-consultation-prompts.php');
    }
}

// === Vision Roadmap Engine (AI-driven milestones, daily activities, daily reports, progress) ===
$roadmap_engine_file = VMO_AI_BUILDER_DIR . 'includes/class-vmo-vision-roadmap-engine.php';
if (file_exists($roadmap_engine_file)) {
    require_once $roadmap_engine_file;
} else {
    if (defined('WP_DEBUG') && WP_DEBUG) {
        error_log('VMO: Missing file - class-vmo-vision-roadmap-engine.php');
    }
}

// === Membership & Ticketing Systems (Dec 2025) ===
// ✅ Safe loading - prevent crashes if files don't exist on server
$membership_file = VMO_AI_BUILDER_DIR . 'includes/class-vmo-membership-system.php';
$ticketing_file = VMO_AI_BUILDER_DIR . 'includes/class-vmo-ticketing-system.php';

global $vmo_membership_system, $vmo_ticketing_system;
$vmo_membership_system = null;
$vmo_ticketing_system = null;

if (file_exists($membership_file)) {
    require_once $membership_file;
    $vmo_membership_system = new VMO_Membership_System();
} else {
    if (defined('WP_DEBUG') && WP_DEBUG) {
        error_log('VMO: Missing file - class-vmo-membership-system.php');
    }
}

if (file_exists($ticketing_file)) {
    require_once $ticketing_file;
    $vmo_ticketing_system = new VMO_Ticketing_System();
} else {
    if (defined('WP_DEBUG') && WP_DEBUG) {
        error_log('VMO: Missing file - class-vmo-ticketing-system.php');
    }
}

// ✅ CRITICAL FIX: Force cache busting for Nov 1 2025 fixes
// This ensures browsers and WordPress load the NEW fixed files
if (!defined('VMO_CACHE_BUST_VERSION')) {
    define('VMO_CACHE_BUST_VERSION', '9.0.5-TTS-PLAY-DEBUG-NOV01-2025-V2');
}

// Safe filemtime helper to prevent warnings on missing files
if (!function_exists('vmo_safe_filemtime')) {
    function vmo_safe_filemtime($file_path, $fallback = null) {
        if (file_exists($file_path)) {
            return filemtime($file_path);
        }
        return $fallback ?: (defined('VMO_CACHE_BUST_VERSION') ? VMO_CACHE_BUST_VERSION : time());
    }
}

// === Unified nonce verification helper (accepts both admin and builder nonces) ===
if (!function_exists('vmo_verify_admin_security')) {
    function vmo_verify_admin_security(): bool {
        $nonce_values = [];
        foreach (['security', 'nonce', '_ajax_nonce', 'vmo_admin_nonce', 'vmo_builder_nonce', 'vmo_nonce'] as $field_name) {
            if (isset($_REQUEST[$field_name])) {
                $nonce_values[] = sanitize_text_field(wp_unslash($_REQUEST[$field_name]));
            }
        }
        foreach (array_unique(array_filter($nonce_values)) as $nonce) {
            if (wp_verify_nonce($nonce, 'vmo_admin_nonce')) return true;
            if (wp_verify_nonce($nonce, 'vmo_builder_nonce')) return true;
            if (wp_verify_nonce($nonce, 'vmo_nonce')) return true;
        }
        return false;
    }
}

if (!function_exists('vmo_require_crm_ajax_security')) {
    function vmo_require_crm_ajax_security(): void {
        if (!current_user_can('read')) {
            wp_send_json_error(['message' => 'Unauthorized'], 403);
        }
        if (!vmo_verify_admin_security()) {
            wp_send_json_error(['message' => 'Invalid nonce', 'status' => 'invalid_nonce'], 403);
        }
    }
}

if (!function_exists('vmo_crm_contact_columns')) {
    function vmo_crm_contact_columns($table_name): array {
        static $cache = [];
        $table_name = (string) $table_name;
        if ($table_name === '') {
            return [];
        }
        if (isset($cache[$table_name])) {
            return $cache[$table_name];
        }
        global $wpdb;
        if (!vmo_table_exists($table_name)) {
            $cache[$table_name] = [];
            return [];
        }
        $suppress = is_object($wpdb) && method_exists($wpdb, 'suppress_errors') ? $wpdb->suppress_errors(true) : null;
        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is built from the WP prefix and an internal suffix.
        $columns = $wpdb->get_col("SHOW COLUMNS FROM {$table_name}", 0);
        if ($suppress !== null) {
            $wpdb->suppress_errors($suppress);
        }
        $cache[$table_name] = is_array($columns) ? array_map('strval', $columns) : [];
        return $cache[$table_name];
    }
}

if (!defined('VMO_SECRET_PLACEHOLDER')) {
    define('VMO_SECRET_PLACEHOLDER', '__VMO_SECRET_PLACEHOLDER__');
}

add_action('admin_head', function() {
    $page = isset($_GET['page']) ? sanitize_key(wp_unslash($_GET['page'])) : '';
    if ($page === '' || strpos($page, 'vmo') === false) {
        return;
    }
    echo '<link rel="icon" href="' . esc_url(VMO_AI_BUILDER_URL . 'assets/vmo-icon.svg') . '" type="image/svg+xml">' . "\n";
});

if (!function_exists('vmo_encrypt_secret_value')) {
    if (!function_exists('vmo_wp_salt_compat')) {
        function vmo_wp_salt_compat($scheme = 'auth') {
            if (!function_exists('wp_salt') && defined('ABSPATH') && defined('WPINC')) {
                $pluggable = rtrim(ABSPATH, '/\\') . '/' . trim(WPINC, '/\\') . '/pluggable.php';
                if (file_exists($pluggable)) {
                    require_once $pluggable;
                }
            }
            if (function_exists('wp_salt')) {
                return wp_salt($scheme);
            }
            // Fallback: stable material from wp-config constants (available before plugins load).
            $parts = array(
                defined('AUTH_KEY') ? AUTH_KEY : '',
                defined('SECURE_AUTH_KEY') ? SECURE_AUTH_KEY : '',
                defined('LOGGED_IN_KEY') ? LOGGED_IN_KEY : '',
                defined('NONCE_KEY') ? NONCE_KEY : '',
                defined('AUTH_SALT') ? AUTH_SALT : '',
                defined('SECURE_AUTH_SALT') ? SECURE_AUTH_SALT : '',
                defined('LOGGED_IN_SALT') ? LOGGED_IN_SALT : '',
                defined('NONCE_SALT') ? NONCE_SALT : '',
                (string) $scheme,
            );
            $material = implode('|', $parts);
            return $material !== '' ? $material : 'vmo-fallback-salt|' . (string) $scheme;
        }
    }

    function vmo_encrypt_secret_value($plaintext) {
        $plaintext = is_string($plaintext) ? trim($plaintext) : '';
        if ($plaintext === '' || strpos($plaintext, 'vmo_encrypted:') === 0) {
            return $plaintext;
        }
        if (!function_exists('openssl_encrypt')) {
            return $plaintext;
        }
        $method = 'AES-256-CBC';
        $key = hash('sha256', vmo_wp_salt_compat('auth'), true);
        $iv_length = openssl_cipher_iv_length($method);
        $iv = ($iv_length > 0) ? (function_exists('random_bytes') ? random_bytes($iv_length) : openssl_random_pseudo_bytes($iv_length)) : '';
        $cipher = openssl_encrypt($plaintext, $method, $key, OPENSSL_RAW_DATA, $iv);
        if ($cipher === false) {
            return $plaintext;
        }
        return 'vmo_encrypted:' . base64_encode($iv . $cipher); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- encryption/decryption
    }
}

if (!function_exists('vmo_decrypt_secret_value')) {
    function vmo_decrypt_secret_value($value) {
        $value = is_string($value) ? $value : '';
        if ($value === '' || strpos($value, 'vmo_encrypted:') !== 0 || !function_exists('openssl_decrypt')) {
            return $value;
        }
        $payload = base64_decode(substr($value, strlen('vmo_encrypted:'))); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- encryption/decryption
        if ($payload === false) {
            return '';
        }
        $method = 'AES-256-CBC';
        $iv_length = openssl_cipher_iv_length($method);
        $iv = substr($payload, 0, $iv_length);
        $cipher = substr($payload, $iv_length);
        if ($iv === false || $cipher === false) {
            return '';
        }
        $key = hash('sha256', vmo_wp_salt_compat('auth'), true);
        $plaintext = openssl_decrypt($cipher, $method, $key, OPENSSL_RAW_DATA, $iv);
        return $plaintext !== false ? $plaintext : '';
    }
}

if (!function_exists('vmo_register_secret_option_filters')) {
    function vmo_register_secret_option_filters() {
        // Encryption disabled — decrypt filters caused timing issues with OPcache
        // on production servers (encrypted value used as raw API credential).
        // Keep array empty; use wp-config.php constants for secrets if needed.
        $secret_options = array();
        foreach ($secret_options as $option) {
            add_filter("pre_update_option_{$option}", function ($value, $old_value) use ($option) {
                $value = is_string($value) ? trim($value) : '';
                if ($value === '') {
                    return '';
                }
                if ($value === VMO_SECRET_PLACEHOLDER) {
                    return $old_value;
                }
                return vmo_encrypt_secret_value($value);
            }, 10, 2);

            add_filter("option_{$option}", function ($value) use ($option) {
                return vmo_decrypt_secret_value($value);
            });
        }

        // ── One-time migration: decrypt any values that were previously encrypted ──
        $migrate_options = array(
            'vmo_twilio_account_sid',
            'vmo_twilio_auth_token',
            'vmo_twilio_api_key_sid',
            'vmo_twilio_api_key_secret',
            'vmo_openai_api_key',
        );
        foreach ($migrate_options as $opt) {
            $raw = get_option($opt, '');
            if (is_string($raw) && strpos($raw, 'vmo_encrypted:') === 0) {
                $plain = vmo_decrypt_secret_value($raw);
                // If decrypt succeeded (non-empty), save plaintext back.
                // If it failed (empty string), keep encrypted value and log.
                if ($plain !== '' && strpos($plain, 'vmo_encrypted:') !== 0) {
                    update_option($opt, $plain);
                    if (defined('WP_DEBUG') && WP_DEBUG) error_log("[VMO] Migrated {$opt} from encrypted back to plaintext");
                } else {
                    if (defined('WP_DEBUG') && WP_DEBUG) error_log("[VMO] WARNING: Could not decrypt {$opt} — user must re-enter on settings page");
                    // Wipe the broken encrypted value so the settings page shows empty
                    // and the user is clearly prompted to re-enter.
                    delete_option($opt);
                }
            }
        }

        // OpenAI key fallback: if vmo_openai_api_key is empty, try vmo_manager_openai_api_key
        add_filter('option_vmo_openai_api_key', function ($value) {
            if (empty($value)) {
                $manager_key = get_option('vmo_manager_openai_api_key', '');
                if (!empty($manager_key)) {
                    return $manager_key;
                }
            }
            return $value;
        }, 20);
    }
    vmo_register_secret_option_filters();
}

if (!function_exists('vmo_get_greeting_display_name')) {
    function vmo_get_greeting_display_name(): string {
        if (function_exists('wp_get_current_user')) {
            $user = wp_get_current_user();
            if ($user && $user->exists()) {
                $quicklaunch_name = function_exists('vmo_quicklaunch_get_welcome_name')
                    ? vmo_quicklaunch_get_welcome_name((int) $user->ID)
                    : '';
                if ($quicklaunch_name !== '') {
                    return $quicklaunch_name;
                }

                foreach (array($user->first_name, $user->nickname, $user->display_name, $user->user_login) as $candidate) {
                    $candidate = function_exists('vmo_normalize_personal_greeting_name')
                        ? vmo_normalize_personal_greeting_name($candidate)
                        : trim((string) $candidate);
                    if ($candidate !== '') {
                        return $candidate;
                    }
                }
            }
        }
        return __('there', 'vmo');
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// VMO CENTRALIZED API REQUEST HANDLER
// All external API calls (OpenAI, Twilio, Stripe) should use this function.
// In CLIENT MODE, requests are proxied through the master server.
// In MASTER MODE, requests go directly to the API providers.
// ═══════════════════════════════════════════════════════════════════════════════
if (!function_exists('vmo_infer_proxy_operation_key')) {
    function vmo_infer_proxy_operation_key($service, $endpoint, $args = []) {
        $service = sanitize_key((string) $service);
        $endpoint = strtolower(ltrim((string) $endpoint, '/'));

        if ($service === 'openai') {
            if (strpos($endpoint, 'chat/completions') !== false) {
                $body = [];
                if (!empty($args['body']) && is_string($args['body'])) {
                    $decoded = json_decode($args['body'], true);
                    if (is_array($decoded)) {
                        $body = $decoded;
                    }
                }
                $messages_text = '';
                if (!empty($body['messages']) && is_array($body['messages'])) {
                    $messages_text = strtolower(wp_json_encode($body['messages']));
                }
                if (strpos($messages_text, 'business consultant') !== false || strpos($messages_text, 'business wizard') !== false) {
                    return 'consultation_message';
                }
                return 'chat';
            }
            if (strpos($endpoint, 'audio/speech') !== false) {
                return 'tts';
            }
            if (strpos($endpoint, 'audio/transcriptions') !== false) {
                return 'transcription';
            }
            if (strpos($endpoint, 'images/generations') !== false) {
                return 'image';
            }
        }

        return '';
    }
}

if (!function_exists('vmo_remove_provider_authorization_header')) {
    function vmo_remove_provider_authorization_header(array &$headers) {
        foreach (array_keys($headers) as $header_key) {
            if (strtolower((string) $header_key) === 'authorization') {
                unset($headers[$header_key]);
            }
        }
    }
}

if (!function_exists('vmo_api_request')) {
    /**
     * Make an API request - routes through master proxy in client mode
     * 
     * @param string $service   'openai', 'twilio', or 'stripe'
     * @param string $endpoint  API endpoint path (e.g., 'v1/chat/completions')
     * @param array  $args      wp_remote_post arguments (headers, body, timeout, etc.)
     * @param string $method    HTTP method (POST, GET, etc.) - default POST
     * @return array|WP_Error   Response array or WP_Error
     */
    function vmo_api_request($service, $endpoint, $args = [], $method = 'POST') {
        $request_user_id = isset($args['vmo_user_id']) ? (int) $args['vmo_user_id'] : get_current_user_id();
        $operation_key = isset($args['vmo_operation']) ? sanitize_key((string) $args['vmo_operation']) : '';
        $operation_multiplier = isset($args['vmo_operation_multiplier']) ? (float) $args['vmo_operation_multiplier'] : 0;
        $request_id = isset($args['vmo_request_id']) ? sanitize_text_field((string) $args['vmo_request_id']) : '';
        unset($args['vmo_operation'], $args['vmo_operation_multiplier'], $args['vmo_request_id']);

        if ($request_user_id > 0 && function_exists('vmo_is_user_service_locked') && vmo_is_user_service_locked($request_user_id)) {
            $payload = function_exists('vmo_get_service_lock_payload') ? vmo_get_service_lock_payload($request_user_id, $service . '/' . $endpoint) : array();
            return new WP_Error(
                'vmo_service_locked',
                $payload['message'] ?? 'VMO services are paused until billing is resolved.',
                array(
                    'status' => 402,
                    'service_locked' => true,
                    'service_lock_reason' => $payload['service_lock_reason'] ?? '',
                    'recoverable_plan' => $payload['recoverable_plan'] ?? '',
                )
            );
        }

        // Check if user has an active subscription (required for client mode)
        if (defined('VMO_CLIENT_MODE') && VMO_CLIENT_MODE === true) {
            $proxy_user_id = isset($args['vmo_user_id']) ? (int) $args['vmo_user_id'] : 0;
            $proxy_plan = isset($args['vmo_plan']) ? sanitize_key((string) $args['vmo_plan']) : '';
            unset($args['vmo_user_id'], $args['vmo_plan']);

            // Verify subscription status before making any API calls
            $user_id = ($proxy_user_id > 0) ? $proxy_user_id : get_current_user_id();
            $plan = ($proxy_plan !== '') ? $proxy_plan : (string) get_user_meta($user_id, 'vmo_subscription_status', true);
            $valid_plans = ['tester', 'starter', 'pro', 'elite', 'enterprise', 'premium', 'business'];
            
            if (empty($plan) || !in_array(strtolower($plan), $valid_plans)) {
                return new WP_Error('vmo_subscription_required', 
                    'Active subscription required. Please select a plan to use VMO features.',
                    ['status' => 403]
                );
            }
            
            // Route through master proxy (configurable via constant or wp-admin setting)
            $proxy_url = defined('VMO_MASTER_PROXY_URL') 
                ? VMO_MASTER_PROXY_URL 
                : get_option('vmo_master_proxy_url', 'https://getvmo.com/wp-json/vmo-builder/v1/proxy');
            
            $target_url = trailingslashit($proxy_url) . $service . '/' . ltrim($endpoint, '/');
            
            // Add installation ID and user license info for tracking/validation
            $installation_id = get_option('vmo_installation_id', '');
            $license_key = get_option('vmo_license_key', '');
            if ($operation_key === '') {
                $operation_key = vmo_infer_proxy_operation_key($service, $endpoint, $args);
            }
            if ($request_id === '' && function_exists('wp_generate_uuid4')) {
                $request_id = wp_generate_uuid4();
            }
            
            // Merge custom headers
            $args['headers'] = array_merge(
                $args['headers'] ?? [],
                [
                    'X-VMO-Installation' => $installation_id,
                    'X-VMO-License' => $license_key,
                    'X-VMO-User' => $user_id,
                    'X-VMO-Plan' => $plan,
                ]
            );
            vmo_remove_provider_authorization_header($args['headers']);
            if ($operation_key !== '') {
                $args['headers']['X-VMO-Operation'] = $operation_key;
            }
            if ($operation_multiplier > 0) {
                $args['headers']['X-VMO-Operation-Multiplier'] = (string) $operation_multiplier;
            }
            if ($request_id !== '') {
                $args['headers']['X-VMO-Request-ID'] = $request_id;
            }
            
            $args['method'] = $method;
            
            if (defined('WP_DEBUG') && WP_DEBUG) error_log("VMO Client Mode: Routing $service/$endpoint through master proxy");
            
            return wp_remote_request($target_url, $args);
        }
        
        // MASTER MODE: Direct API calls
        $api_urls = [
            'openai' => 'https://api.openai.com/',
            'twilio' => 'https://api.twilio.com/',
            'stripe' => 'https://api.stripe.com/',
        ];
        
        if (!isset($api_urls[$service])) {
            return new WP_Error('vmo_invalid_service', 'Unknown service: ' . $service);
        }
        
        $target_url = $api_urls[$service] . ltrim($endpoint, '/');
        $args['method'] = $method;
        
        return wp_remote_request($target_url, $args);
    }
}

/**
 * Helper to make OpenAI chat completion requests
 */
if (!function_exists('vmo_openai_chat')) {
    function vmo_openai_chat($messages, $model = '', $options = []) {
        // Resolve empty/deprecated model to the current fast tier
        if (empty($model) || strpos($model, 'gpt-5.2') !== false || strpos($model, 'gpt-4') !== false) {
            $model = function_exists('vmo_get_model') ? vmo_get_model('fast') : (defined('VMO_OPENAI_FAST_MODEL') ? VMO_OPENAI_FAST_MODEL : 'gpt-5.5-nano');
        }

        $api_user_id = isset($options['vmo_user_id']) ? (int) $options['vmo_user_id'] : 0;
        $api_plan = isset($options['vmo_plan']) ? sanitize_key((string) $options['vmo_plan']) : '';
        $api_operation = isset($options['vmo_operation']) ? sanitize_key((string) $options['vmo_operation']) : 'chat';
        $lock_user_id = $api_user_id > 0 ? $api_user_id : get_current_user_id();
        if ($lock_user_id > 0 && function_exists('vmo_is_user_service_locked') && vmo_is_user_service_locked($lock_user_id)) {
            $payload = function_exists('vmo_get_service_lock_payload') ? vmo_get_service_lock_payload($lock_user_id, 'openai_chat') : array();
            return new WP_Error('vmo_service_locked', $payload['message'] ?? 'VMO AI is paused until billing is resolved.', array('status' => 402));
        }

        // Separate transport-level options from OpenAI API parameters.
        // These must NOT be sent in the API request body.
        $request_timeout = $options['timeout'] ?? 120;
        $request_max_tokens = $options['max_tokens'] ?? 4096;
        unset($options['vmo_user_id'], $options['vmo_plan'], $options['vmo_operation'], $options['timeout']);

        // In client mode, don't add API key (master will add it)
        $headers = ['Content-Type' => 'application/json'];
        
        if (!defined('VMO_CLIENT_MODE') || VMO_CLIENT_MODE !== true) {
            $api_key = get_option('vmo_openai_api_key');
            if (empty($api_key)) {
                return new WP_Error('vmo_no_api_key', 'AI API key not configured');
            }
            $headers['Authorization'] = 'Bearer ' . $api_key;
        }

        // Build API body with only valid OpenAI parameters.
        // Remove max_tokens from $options here — we'll add the correct
        // token param (max_tokens OR max_completion_tokens) below.
        $requested_temperature = $options['temperature'] ?? 0.7;
        unset($options['max_tokens']);
        unset($options['temperature']);

        $body = array_merge([
            'model' => $model,
            'messages' => $messages,
        ], $options);
        if (function_exists('vmo_apply_safe_temperature_to_payload')) {
            $body = vmo_apply_safe_temperature_to_payload($body, $model, $requested_temperature);
        } elseif (!preg_match('/^(gpt-realtime|gpt-5|o1|o3|o4)/i', (string) $model)) {
            $body['temperature'] = (float) $requested_temperature;
        }
        
        // Handle token parameters based on model.
        // GPT-5.x and o-series use max_completion_tokens; older models use max_tokens.
        // Only ONE of these may be present — sending both causes a 400 error.
        if (function_exists('vmo_get_token_param')) {
            $body = array_merge($body, vmo_get_token_param($model, $request_max_tokens));
        } else {
            $body['max_tokens'] = $request_max_tokens;
        }
        
        $args = [
            'headers' => $headers,
            'body' => json_encode($body),
            'timeout' => $request_timeout,
        ];

        if ($api_user_id > 0) {
            $args['vmo_user_id'] = $api_user_id;
        }
        if ($api_plan !== '') {
            $args['vmo_plan'] = $api_plan;
        }
        if ($api_operation !== '') {
            $args['vmo_operation'] = $api_operation;
        }
        
        return vmo_api_request('openai', 'v1/chat/completions', $args, 'POST');
    }
}

/**
 * Helper to make OpenAI image generation requests
 */
if (!function_exists('vmo_openai_image')) {
    function vmo_openai_image($prompt, $size = '1024x1024', $options = []) {
        $lock_user_id = isset($options['vmo_user_id']) ? (int) $options['vmo_user_id'] : get_current_user_id();
        if ($lock_user_id > 0 && function_exists('vmo_is_user_service_locked') && vmo_is_user_service_locked($lock_user_id)) {
            $payload = function_exists('vmo_get_service_lock_payload') ? vmo_get_service_lock_payload($lock_user_id, 'openai_image') : array();
            return new WP_Error('vmo_service_locked', $payload['message'] ?? 'VMO AI is paused until billing is resolved.', array('status' => 402));
        }

        $headers = ['Content-Type' => 'application/json'];
        
        if (!defined('VMO_CLIENT_MODE') || VMO_CLIENT_MODE !== true) {
            $api_key = get_option('vmo_openai_api_key');
            if (empty($api_key)) {
                return new WP_Error('vmo_no_api_key', 'AI API key not configured');
            }
            $headers['Authorization'] = 'Bearer ' . $api_key;
        }
        
        $image_model = $options['model'] ?? (function_exists('vmo_get_model') ? vmo_get_model('image') : 'gpt-image-1');
        $uses_base64_response = (strpos((string) $image_model, 'gpt-image') !== false || strpos((string) $image_model, 'openai-image-generation') !== false);
        $body = [
            'model' => $image_model,
            'prompt' => $prompt,
            'n' => $options['n'] ?? 1,
            'size' => $size,
        ];

        if ($uses_base64_response) {
            $body['output_format'] = $options['output_format'] ?? 'png';
        } else {
            $body['response_format'] = $options['response_format'] ?? 'url';
        }
        
        $args = [
            'headers' => $headers,
            'body' => json_encode($body),
            'timeout' => $options['timeout'] ?? 90,
            'vmo_operation' => $options['vmo_operation'] ?? 'image',
        ];
        
        return vmo_api_request('openai', 'v1/images/generations', $args, 'POST');
    }
}

/**
 * Helper to make OpenAI TTS (text-to-speech) requests
 */
if (!function_exists('vmo_openai_tts')) {
    function vmo_openai_tts($text, $voice = 'alloy', $options = []) {
        $lock_user_id = isset($options['vmo_user_id']) ? (int) $options['vmo_user_id'] : get_current_user_id();
        if ($lock_user_id > 0 && function_exists('vmo_is_user_service_locked') && vmo_is_user_service_locked($lock_user_id)) {
            $payload = function_exists('vmo_get_service_lock_payload') ? vmo_get_service_lock_payload($lock_user_id, 'openai_tts') : array();
            return new WP_Error('vmo_service_locked', $payload['message'] ?? 'VMO AI is paused until billing is resolved.', array('status' => 402));
        }

        $headers = ['Content-Type' => 'application/json'];
        
        if (!defined('VMO_CLIENT_MODE') || VMO_CLIENT_MODE !== true) {
            $api_key = get_option('vmo_openai_api_key');
            if (empty($api_key)) {
                return new WP_Error('vmo_no_api_key', 'AI API key not configured');
            }
            $headers['Authorization'] = 'Bearer ' . $api_key;
        }
        
        $body = [
            'model' => $options['model'] ?? 'tts-1-hd',
            'input' => $text,
            'voice' => $voice,
            'response_format' => $options['format'] ?? 'mp3',
        ];
        
        $args = [
            'headers' => $headers,
            'body' => json_encode($body),
            'timeout' => $options['timeout'] ?? 60,
            'vmo_operation' => $options['vmo_operation'] ?? 'tts',
        ];
        
        return vmo_api_request('openai', 'v1/audio/speech', $args, 'POST');
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// VMO SMART API WRAPPER
// Intercepts wp_remote_post calls to external APIs and routes through proxy in client mode
// This is a compatibility layer for legacy code that makes direct API calls
// ═══════════════════════════════════════════════════════════════════════════════
if (!function_exists('vmo_smart_remote_post')) {
    /**
     * Smart wrapper for wp_remote_post that handles client mode proxying
     * Use this instead of wp_remote_post for API calls to OpenAI, Twilio, Stripe
     * 
     * @param string $url  The target URL
     * @param array  $args wp_remote_post arguments
     * @return array|WP_Error
     */
    function vmo_smart_remote_post($url, $args = []) {
        $url = (string) ($url ?? '');
        // Check if this is a call to a supported external API
        $api_patterns = [
            'openai' => 'api.openai.com',
            'twilio' => 'api.twilio.com',
            'stripe' => 'api.stripe.com',
        ];
        
        $detected_service = null;
        $endpoint = '';
        
        foreach ($api_patterns as $service => $domain) {
            if (strpos($url, $domain) !== false) {
                $detected_service = $service;
                // Extract endpoint from URL
                $endpoint = preg_replace('#^https?://' . preg_quote($domain, '#') . '/?#', '', $url);
                break;
            }
        }
        
        // If not an API call we handle, use regular wp_remote_post
        if (!$detected_service) {
            return wp_remote_post($url, $args);
        }
        
        // In CLIENT MODE, route through proxy
        if (defined('VMO_CLIENT_MODE') && VMO_CLIENT_MODE === true) {
            // Check subscription before making API call
            $user_id = get_current_user_id();
            if ($user_id > 0 && function_exists('vmo_is_user_service_locked') && vmo_is_user_service_locked($user_id)) {
                $payload = function_exists('vmo_get_service_lock_payload') ? vmo_get_service_lock_payload($user_id, $detected_service . '/' . $endpoint) : array();
                return new WP_Error('vmo_service_locked', $payload['message'] ?? 'VMO services are paused until billing is resolved.', array('status' => 402));
            }

            $plan = get_user_meta($user_id, 'vmo_subscription_status', true);
            $valid_plans = ['tester', 'starter', 'pro', 'elite', 'enterprise', 'premium', 'business'];
            
            // Client/QuickLaunch installs must not be blocked by missing local plan
            // metadata. The Vultr client routes to the Kinsta master, and the
            // master/license ledger is the authoritative billing gate. New hosted
            // clients often have a valid license/installation before local plan
            // mirrors are hydrated, so blocking here causes TTS/Realtime to fail
            // and fall back to browser voice. If there is no local plan but there
            // is a VMO license, send the request to master and let master approve,
            // charge, or reject it.
            $installation_id_for_plan = get_option('vmo_installation_id', '');
            $license_key_for_plan = get_option('vmo_license_key', '');
            if (empty($plan) || !in_array(strtolower((string) $plan), $valid_plans, true)) {
                if (!empty($license_key_for_plan) || !empty($installation_id_for_plan)) {
                    $plan = 'client';
                } else {
                    return new WP_Error('vmo_subscription_required', 
                        'Active subscription or VMO client license required. Please select a plan to use VMO features.',
                        ['status' => 403]
                    );
                }
            }
            
            // Build proxy URL (configurable via constant or wp-admin setting)
            $proxy_url = defined('VMO_MASTER_PROXY_URL') 
                ? VMO_MASTER_PROXY_URL 
                : get_option('vmo_master_proxy_url', 'https://getvmo.com/wp-json/vmo-builder/v1/proxy');
            
            $target_url = trailingslashit($proxy_url) . $detected_service . '/' . ltrim($endpoint, '/');
            
            // Add VMO headers for authentication/tracking
            $installation_id = get_option('vmo_installation_id', '');
            $license_key = get_option('vmo_license_key', '');
            $operation_key = isset($args['vmo_operation']) ? sanitize_key((string) $args['vmo_operation']) : '';
            $operation_multiplier = isset($args['vmo_operation_multiplier']) ? (float) $args['vmo_operation_multiplier'] : 0;
            $request_id = isset($args['vmo_request_id']) ? sanitize_text_field((string) $args['vmo_request_id']) : '';
            unset($args['vmo_operation'], $args['vmo_operation_multiplier'], $args['vmo_request_id']);
            if ($operation_key === '') {
                $operation_key = vmo_infer_proxy_operation_key($detected_service, $endpoint, $args);
            }
            if ($request_id === '' && function_exists('wp_generate_uuid4')) {
                $request_id = wp_generate_uuid4();
            }
            
            $args['headers'] = array_merge(
                $args['headers'] ?? [],
                [
                    'X-VMO-Installation' => $installation_id,
                    'X-VMO-License' => $license_key,
                    'X-VMO-User' => $user_id,
                    'X-VMO-Plan' => $plan,
                ]
            );
            
            // Remove any existing Authorization header (master will add it)
            if (isset($args['headers']['Authorization'])) {
                unset($args['headers']['Authorization']);
            }
            vmo_remove_provider_authorization_header($args['headers']);
            if ($operation_key !== '') {
                $args['headers']['X-VMO-Operation'] = $operation_key;
            }
            if ($operation_multiplier > 0) {
                $args['headers']['X-VMO-Operation-Multiplier'] = (string) $operation_multiplier;
            }
            if ($request_id !== '') {
                $args['headers']['X-VMO-Request-ID'] = $request_id;
            }
            
            if (defined('WP_DEBUG') && WP_DEBUG) error_log("VMO Smart Proxy: Routing $detected_service/$endpoint through master proxy");
            
            return wp_remote_post($target_url, $args);
        }
        
        // MASTER MODE: Make direct call
        return wp_remote_post($url, $args);
    }
}

if (!function_exists('vmo_get_consultation_greeting_text')) {
    function vmo_get_consultation_greeting_text(): string {
        $default = <<<EOT
Welcome, {{user_name}}. I'm Jerreme, your VMO Business Wizard consultant. I'll lead your business consultation.

Jarvis and the rest of the VMO AI team are already standing by across design, content, SEO, media, documents, automation, and launch operations. My job is to turn your goals into a clear build plan we can execute inside your website builder without overwhelm.

We'll keep this focused and easy. First, tell me this: are we building a brand new business from scratch, or are we improving an existing business?

You can type, use speech-to-text, tap the mic, or upload documents if that is faster. I'll guide you one calm question at a time.
EOT;
        $display_name = vmo_get_greeting_display_name();
        $replacements = ['{{user_name}}', '{{ user_name }}', '[USER_NAME]', '[username]', '[UserName]'];
        $personalized = str_replace($replacements, $display_name, $default);
        return apply_filters('vmo_consultation_greeting_text', $personalized, $display_name);
    }
}

if (!function_exists('vmo_filter_greeting_audio_text')) {
    function vmo_filter_greeting_audio_text($audio_text, $voice) {
        $text = is_string($audio_text) ? $audio_text : '';
        if ($text === '') {
            return $audio_text;
        }
        $text = str_replace(
            ['{{user_name}}', '{{ user_name }}', '[USER_NAME]', '[username]', '[UserName]'],
            vmo_get_greeting_display_name(),
            $text
        );
        return $text;
    }
    add_filter('vmo_consultation_greeting_audio_text', 'vmo_filter_greeting_audio_text', 10, 2);
}

if (!function_exists('vmo_force_greeting_voice')) {
    function vmo_force_greeting_voice($voice) {
        return 'onyx';
    }
    add_filter('vmo_consultation_greeting_voice', 'vmo_force_greeting_voice', 1000, 1);
}

// Tiny echo endpoint for debugging admin-ajax connectivity and nonce issues
add_action('wp_ajax_vmo_debug_echo', function () {
    $payload = [
        'method' => $_SERVER['REQUEST_METHOD'] ?? 'UNKNOWN',
        'received' => [
            'action' => sanitize_key($_POST['action'] ?? ''),
            'security' => isset($_POST['security']) ? 'present' : 'missing',
            'nonce' => isset($_POST['nonce']) ? 'present' : 'missing',
        ],
        'nonce_valid' => vmo_verify_admin_security(),
        'user' => get_current_user_id(),
        'ts' => current_time('mysql'),
    ];
    if (!$payload['nonce_valid']) {
        $payload['status'] = 'invalid_nonce';
        wp_send_json_error($payload);
    }
    wp_send_json_success($payload);
});

/**
 * VMO: Resilient Cloudflare API token verifier.
 *
 * Cloudflare has two token types and `/user/tokens/verify` only works for
 * User API Tokens. Account API Tokens (which are required for multi-tenant
 * zone management at production scale) return non-active status there even
 * when the token is perfectly valid. This probe chain tries multiple
 * endpoints so verification works for ANY valid Cloudflare token, and
 * degrades gracefully if Cloudflare deprecates one of them.
 *
 * @param string $token Cloudflare API token (bearer).
 * @return array { valid: bool, message: string, token_type: string }
 */
if (!function_exists('vmo_cloudflare_verify_token')) {
function vmo_cloudflare_verify_token($token) {
    $token = trim((string) $token);
    if ($token === '') {
        return ['valid' => false, 'message' => 'No token configured', 'token_type' => 'none'];
    }
    $headers = [
        'Authorization' => 'Bearer ' . $token,
        'Content-Type'  => 'application/json',
    ];
    $last_err = '';
    $valid = false;
    $token_type = 'unknown';
    $valid_message = '';

    // Probe 1: /user/tokens/verify — definitive for User API Tokens.
    $resp = wp_remote_get('https://api.cloudflare.com/client/v4/user/tokens/verify', [
        'timeout' => 12, 'headers' => $headers,
    ]);
    if (!is_wp_error($resp)) {
        $code = (int) wp_remote_retrieve_response_code($resp);
        $body = json_decode(wp_remote_retrieve_body($resp), true);
        if ($code < 400 && (($body['result']['status'] ?? '') === 'active')) {
            $valid = true;
            $token_type = 'user';
            $valid_message = 'Token active (User API Token)';
        } else if (is_array($body['errors'] ?? null) && !empty($body['errors'][0]['message'])) {
            $last_err = $body['errors'][0]['message'];
        }
    } else {
        $last_err = $resp->get_error_message();
    }

    // Probe 2: GET /zones?per_page=1 — works for Account API Tokens with zone read.
    if (!$valid) {
        $resp = wp_remote_get('https://api.cloudflare.com/client/v4/zones?per_page=1', [
            'timeout' => 12, 'headers' => $headers,
        ]);
        if (!is_wp_error($resp)) {
            $code = (int) wp_remote_retrieve_response_code($resp);
            $body = json_decode(wp_remote_retrieve_body($resp), true);
            if ($code < 400 && !empty($body['success'])) {
                $valid = true;
                $token_type = 'account';
                $valid_message = 'Token active (Account API Token — can list zones)';
            } else if (is_array($body['errors'] ?? null) && !empty($body['errors'][0]['message'])) {
                $last_err = $body['errors'][0]['message'];
            }
        } else {
            $last_err = $resp->get_error_message();
        }
    }

    // Probe 3: GET /accounts?per_page=1 — last-resort account-scope probe.
    if (!$valid) {
        $resp = wp_remote_get('https://api.cloudflare.com/client/v4/accounts?per_page=1', [
            'timeout' => 12, 'headers' => $headers,
        ]);
        if (!is_wp_error($resp)) {
            $code = (int) wp_remote_retrieve_response_code($resp);
            $body = json_decode(wp_remote_retrieve_body($resp), true);
            if ($code < 400 && !empty($body['success'])) {
                $valid = true;
                $token_type = 'account';
                $valid_message = 'Token active (Account API Token — account-scoped)';
            } else if (is_array($body['errors'] ?? null) && !empty($body['errors'][0]['message'])) {
                $last_err = $body['errors'][0]['message'];
            }
        } else if (empty($last_err)) {
            $last_err = $resp->get_error_message();
        }
    }

    if (!$valid) {
        return [
            'valid' => false,
            'message' => 'Token invalid: ' . ($last_err !== '' ? $last_err : 'all verification probes failed'),
            'token_type' => 'unknown',
            'can_create_zones' => false,
        ];
    }

    // Probe 4: Account-discovery capability check.
    //
    // The token is valid, but POST /zones requires an `account.id` in
    // the body and the plugin auto-discovers it via GET /accounts. If
    // that endpoint returns zero accounts (because the token's account
    // resources are scoped to nothing, or to a sub-resource that hides
    // the account list), every customer onboarding will fail at the
    // configure_dns step with the misleading "Domain not in Cloudflare"
    // cascade. There is NO standalone "Zone:Create" permission in
    // Cloudflare's permission catalog — the real gate is whether the
    // token can resolve an account id at all.
    $can_create = null; // null = unknown
    $create_hint = '';
    $acct_probe = wp_remote_get('https://api.cloudflare.com/client/v4/accounts?per_page=1', [
        'timeout' => 12, 'headers' => $headers,
    ]);
    if (!is_wp_error($acct_probe)) {
        $acode = (int) wp_remote_retrieve_response_code($acct_probe);
        $abody = json_decode(wp_remote_retrieve_body($acct_probe), true);
        if ($acode < 400 && !empty($abody['result'][0]['id'])) {
            $can_create = true;
        } else {
            $can_create = false;
            $create_hint = 'Token is valid but cannot list any Cloudflare accounts (GET /accounts returned empty). Re-issue the token with Account Settings:Read at “All accounts” scope, or set vmo_cloudflare_account_id manually — otherwise new customer zones cannot be created.';
        }
    }

    $result = [
        'valid' => true,
        'message' => $valid_message,
        'token_type' => $token_type,
        'can_create_zones' => $can_create === true,
    ];
    if ($can_create === false) {
        // Token works but can't create zones — still report valid so the
        // settings UI shows green for connectivity, but include the
        // actionable warning so onboarding doesn't break silently later.
        $result['message'] .= ' — WARNING: ' . $create_hint;
        $result['warning'] = $create_hint;
    }
    return $result;
}
}

// ✅ QuickLaunch API Connection Test — admin-only AJAX endpoint
// Tests Vultr, Cloudflare, Mailcow, Stripe, and Namecheap API keys from the Settings page
add_action('wp_ajax_vmo_test_quicklaunch_apis', function() {
    if (!current_user_can('manage_options')) {
        wp_send_json_error(['message' => 'Unauthorized']);
    }
    check_ajax_referer('vmo_test_ql_apis', 'security');

    $results = [
        'vultr'      => ['valid' => false, 'message' => 'Not configured'],
        'cloudflare' => ['valid' => false, 'message' => 'Not configured'],
        'mailcow'    => ['valid' => false, 'message' => 'Not configured'],
        'stripe'     => ['valid' => false, 'message' => 'Not configured'],
        'namecheap'  => ['valid' => false, 'message' => 'Not configured'],
    ];

    $email_provider = strtolower(trim((string) get_option('vmo_quicklaunch_email_provider', 'mailcow')));
    $legacy_email_provider = str_replace(['-', '_'], '', $email_provider);
    if ($email_provider === '' || $legacy_email_provider === 'mx' . 'route') {
        $email_provider = 'mailcow';
    }

    // ── Vultr ──
    $vultr_key = get_option('vmo_vultr_api_key', '');
    if (!empty($vultr_key)) {
        $resp = wp_remote_get('https://api.vultr.com/v2/account', [
            'timeout' => 15,
            'headers' => ['Authorization' => 'Bearer ' . $vultr_key],
        ]);
        if (!is_wp_error($resp)) {
            $body = json_decode(wp_remote_retrieve_body($resp), true);
            $code = wp_remote_retrieve_response_code($resp);
            if ($code < 400 && isset($body['account'])) {
                // Vultr's sign convention for `account.balance` is the
                // OPPOSITE of what it intuitively looks like:
                //   negative balance = you have credit on file
                //   positive balance = you owe Vultr (past due)
                // The dashboard renders -$24.17 in green with a "You Have
                // Credit!" badge, and `remaining_credit = -balance -
                // pending_charges`. Compute the actual usable credit and
                // flag the badge red only when the account truly cannot
                // fund a new instance (no credit AND past due, or
                // remaining credit below the smallest plan's monthly
                // rate).
                $balance_raw     = isset($body['account']['balance']) ? (float) $body['account']['balance'] : 0.0;
                $pending         = isset($body['account']['pending_charges']) ? (float) $body['account']['pending_charges'] : 0.0;
                $remaining_credit = max(0.0, (-$balance_raw) - $pending);
                $past_due        = $balance_raw > 0;
                $balance_msg     = ($balance_raw < 0 ? '-$' : '$') . number_format(abs($balance_raw), 2);
                if ($past_due) {
                    $results['vultr'] = [
                        'valid'   => false,
                        'message' => 'API works but account is PAST DUE: balance ' . $balance_msg . ' owed (pending $' . number_format($pending, 2) . '). New servers will be rejected. Top up at https://my.vultr.com/billing/',
                    ];
                } else {
                    $results['vultr'] = [
                        'valid'   => true,
                        'message' => 'Connected. Credit on file: $' . number_format($remaining_credit, 2) . ' (balance ' . $balance_msg . ', pending $' . number_format($pending, 2) . ')',
                    ];
                }
            } else {
                if ($code >= 500) {
                    $results['vultr'] = ['valid' => false, 'message' => 'Vultr API temporarily unavailable: Server issue (HTTP ' . $code . '). Please retry shortly.'];
                } elseif ($code === 401 || $code === 403) {
                    $results['vultr'] = ['valid' => false, 'message' => 'Invalid or unauthorized API key (HTTP ' . $code . '). Ensure IP is whitelisted under Vultr API Access settings.'];
                } else {
                    $results['vultr'] = ['valid' => false, 'message' => 'Could not connect (HTTP ' . $code . ')'];
                }
            }
        } else {
            $results['vultr'] = ['valid' => false, 'message' => 'Connection failed: ' . $resp->get_error_message()];
        }
    }

    // ── Cloudflare ── uses resilient probe chain (works for User AND Account API Tokens)
    $cf_token = get_option('vmo_cloudflare_api_token', '');
    if (!empty($cf_token)) {
        $cf_check = vmo_cloudflare_verify_token($cf_token);
        $results['cloudflare'] = [
            'valid'   => (bool) $cf_check['valid'],
            'message' => (string) $cf_check['message'],
        ];
    }

    // ── Mailcow API ──
    $mailcow_base = untrailingslashit(trim((string) get_option('vmo_mailcow_api_base', '')));
    $mailcow_key = trim((string) get_option('vmo_mailcow_api_key', ''));
    if ($mailcow_base !== '' && $mailcow_key !== '') {
        $probe = wp_remote_request($mailcow_base . '/api/v1/get/domain/all', [
            'method' => 'GET',
            'timeout' => 15,
            'headers' => [
                'X-API-Key' => $mailcow_key,
                'Accept' => 'application/json',
            ],
        ]);

        if (!is_wp_error($probe) && wp_remote_retrieve_response_code($probe) < 400) {
            $body = json_decode(wp_remote_retrieve_body($probe), true);
            $domain_count = is_array($body) ? count($body) : 0;
            $results['mailcow'] = ['valid' => true, 'message' => 'Connected to ' . $mailcow_base . ' (' . $domain_count . ' domains)'];
        } else {
            $code = is_wp_error($probe) ? $probe->get_error_message() : 'HTTP ' . wp_remote_retrieve_response_code($probe);
            $body = !is_wp_error($probe) ? json_decode(wp_remote_retrieve_body($probe), true) : null;
            $detail = is_array($body) ? (string) ($body['msg'] ?? (($body[0]['msg'] ?? '') ?: $code)) : (string) $code;
            $results['mailcow'] = ['valid' => false, 'message' => 'Connection failed: ' . $detail];
        }
    }

    // ── Stripe ──
    $stripe_key = get_option('vmo_stripe_secret_key', '');
    if (!empty($stripe_key)) {
        $resp = wp_remote_get('https://api.stripe.com/v1/balance', [
            'timeout' => 15,
            'headers' => ['Authorization' => 'Bearer ' . $stripe_key],
        ]);
        if (!is_wp_error($resp)) {
            $body = json_decode(wp_remote_retrieve_body($resp), true);
            if (!empty($body['available'])) {
                $results['stripe'] = ['valid' => true, 'message' => 'Connected'];
            } else {
                $results['stripe'] = ['valid' => false, 'message' => 'Invalid key'];
            }
        } else {
            $results['stripe'] = ['valid' => false, 'message' => 'Connection failed: ' . $resp->get_error_message()];
        }
    }

    // ── Namecheap ──
    $namecheap_api_user = get_option('vmo_namecheap_api_user', '');
    $namecheap_username = get_option('vmo_namecheap_username', '');
    $namecheap_api_key = get_option('vmo_namecheap_api_key', '');
    $namecheap_client_ip = get_option('vmo_namecheap_client_ip', '');
    $contact_complete = !empty(get_option('vmo_namecheap_contact_first_name', ''))
        && !empty(get_option('vmo_namecheap_contact_last_name', ''))
        && !empty(get_option('vmo_namecheap_contact_address1', ''))
        && !empty(get_option('vmo_namecheap_contact_city', ''))
        && !empty(get_option('vmo_namecheap_contact_state', ''))
        && !empty(get_option('vmo_namecheap_contact_postal_code', ''))
        && !empty(get_option('vmo_namecheap_contact_country', ''))
        && !empty(get_option('vmo_namecheap_contact_phone', ''))
        && !empty(get_option('vmo_namecheap_contact_email', ''));

    if (!empty($namecheap_api_user) && !empty($namecheap_username) && !empty($namecheap_api_key) && !empty($namecheap_client_ip)) {
        $endpoint = get_option('vmo_namecheap_use_sandbox', '0') === '1'
            ? 'https://api.sandbox.namecheap.com/xml.response'
            : 'https://api.namecheap.com/xml.response';

        $resp = wp_remote_get(add_query_arg([
            'ApiUser' => $namecheap_api_user,
            'ApiKey' => $namecheap_api_key,
            'UserName' => $namecheap_username,
            'ClientIp' => $namecheap_client_ip,
            'Command' => 'namecheap.domains.getTldList',
        ], $endpoint), [
            'timeout' => 20,
        ]);

        if (!is_wp_error($resp)) {
            $body = wp_remote_retrieve_body($resp);
            $xml = @simplexml_load_string($body);
            $status = $xml ? strtoupper((string) ($xml['Status'] ?? '')) : '';
            $tlds = $xml ? $xml->xpath('//*[local-name()="Tld"]') : [];

            if ($status === 'OK') {
                $results['namecheap'] = [
                    'valid' => $contact_complete,
                    'message' => $contact_complete
                        ? 'Connected. ' . count($tlds) . ' TLDs available'
                        : 'API connected, but contact profile is incomplete',
                ];
            } else {
                $errors = [];
                if ($xml) {
                    foreach ($xml->xpath('//*[local-name()="Error"]') as $error) {
                        $errors[] = trim((string) $error);
                    }
                }
                $results['namecheap'] = ['valid' => false, 'message' => !empty($errors) ? implode(' | ', $errors) : 'Invalid Namecheap credentials'];
            }
        } else {
            $results['namecheap'] = ['valid' => false, 'message' => 'Connection failed: ' . $resp->get_error_message()];
        }
    }

    $email_valid = false;
    if ($email_provider === 'disabled') {
        $email_valid = true;
    } elseif ($email_provider === 'mailcow') {
        $email_valid = (bool) $results['mailcow']['valid'];
    } else {
        $results[$email_provider] = ['valid' => false, 'message' => 'Unsupported QuickLaunch email provider.'];
    }

    $all_valid = $results['vultr']['valid'] && $results['cloudflare']['valid'] && $email_valid && $results['stripe']['valid'] && $results['namecheap']['valid'];
    wp_send_json_success(['all_valid' => $all_valid, 'email_provider' => $email_provider, 'services' => $results]);
});

// ✅ GPT-5.5 Model Test Endpoint - Test OpenAI model availability
// Call from browser console: jQuery.post(vmoAjax.url, {action:'vmo_test_gpt52', security:vmoAjax.nonce}, console.log)
add_action('wp_ajax_vmo_test_gpt52', function() {
    if (!vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Invalid security token']);
    }
    
    $api_key = get_option('vmo_openai_api_key');
    if (empty($api_key)) {
        wp_send_json_error(['message' => 'AI API key not configured']);
    }
    
    $models_to_test = [vmo_get_model('fast'), vmo_get_model('nano'), vmo_get_model('mini')];
    $results = [];
    
    foreach ($models_to_test as $model) {
        // Use correct token parameter based on model
        $token_param = vmo_get_token_param($model, 50);
        
        $data = array_merge([
            'model' => $model,
            'messages' => [
                ['role' => 'user', 'content' => 'Say exactly: "Model ' . $model . ' is working!"']
            ],
            'temperature' => 0.3
        ], $token_param);
        
        $start = microtime(true);
        
        $response = wp_remote_post('https://api.openai.com/v1/chat/completions', [
            'headers' => [
                'Authorization' => 'Bearer ' . $api_key,
                'Content-Type' => 'application/json',
            ],
            'body' => json_encode($data),
            'timeout' => 60
        ]);
        
        $elapsed = round(microtime(true) - $start, 2);
        
        if (is_wp_error($response)) {
            $results[$model] = [
                'status' => 'error',
                'message' => $response->get_error_message(),
                'time' => $elapsed
            ];
            continue;
        }
        
        $http_code = wp_remote_retrieve_response_code($response);
        $body = json_decode(wp_remote_retrieve_body($response), true);
        
        if ($http_code === 200 && isset($body['choices'][0]['message']['content'])) {
            $results[$model] = [
                'status' => 'success',
                'http_code' => $http_code,
                'response' => trim($body['choices'][0]['message']['content']),
                'tokens' => $body['usage']['total_tokens'] ?? 'N/A',
                'time' => $elapsed
            ];
        } else {
            $results[$model] = [
                'status' => 'failed',
                'http_code' => $http_code,
                'error_type' => $body['error']['type'] ?? 'unknown',
                'error_message' => $body['error']['message'] ?? 'Unknown error',
                'time' => $elapsed
            ];
        }
    }
    
    wp_send_json_success([
        'message' => 'AI Model Test Complete',
        'vmo_openai_model' => defined('VMO_OPENAI_MODEL') ? VMO_OPENAI_MODEL : 'not defined',
        'results' => $results
    ]);
});

// ✅ SIMPLE GPT-5.5 Test (no nonce required - admin only) - for quick browser testing
// Just visit: /wp-admin/admin-ajax.php?action=vmo_simple_gpt_test
add_action('wp_ajax_vmo_simple_gpt_test', function() {
    if (!current_user_can('manage_options')) { wp_send_json_error(['message' => 'Admin only'], 403); }
    // Only allow for logged-in admins
    if (!current_user_can('manage_options')) {
        wp_send_json_error(['message' => 'Admin access required']);
    }
    
    $api_key = get_option('vmo_openai_api_key');
    if (empty($api_key)) {
        wp_send_json_error(['message' => 'AI API key not configured', 'key_exists' => false]);
    }
    
    $results = [];
    $models = [vmo_get_model('fast'), vmo_get_model('nano')];
    
    foreach ($models as $model) {
        $token_param = vmo_get_token_param($model, 30);
        
        $response = wp_remote_post('https://api.openai.com/v1/chat/completions', [
            'headers' => [
                'Authorization' => 'Bearer ' . $api_key,
                'Content-Type' => 'application/json',
            ],
            'body' => json_encode(array_merge([
                'model' => $model,
                'messages' => [['role' => 'user', 'content' => 'Say "OK"']],
                'temperature' => 0
            ], $token_param)),
            'timeout' => 30
        ]);
        
        if (is_wp_error($response)) {
            $results[$model] = '❌ WP Error: ' . $response->get_error_message();
        } else {
            $code = wp_remote_retrieve_response_code($response);
            $body = json_decode(wp_remote_retrieve_body($response), true);
            
            if ($code === 200) {
                $reply = $body['choices'][0]['message']['content'] ?? 'no content';
                $results[$model] = "✅ Works! Response: {$reply}";
            } else {
                $err = $body['error']['message'] ?? 'Unknown error';
                $results[$model] = "❌ HTTP {$code}: {$err}";
            }
        }
    }
    
    wp_send_json_success([
        'VMO_OPENAI_MODEL' => defined('VMO_OPENAI_MODEL') ? VMO_OPENAI_MODEL : 'undefined',
        'api_key_ending' => '...' . substr($api_key, -6),
        'test_results' => $results
    ]);
});

// ✅ DEEP RESEARCH DIAGNOSTIC - Test step-by-step what fails
// Visit: /wp-admin/admin-ajax.php?action=vmo_test_deep_research
add_action('wp_ajax_vmo_test_deep_research', function() {
    if (!current_user_can('manage_options')) { wp_send_json_error(['message' => 'Admin only'], 403); }
    // Only allow for logged-in admins
    if (!current_user_can('manage_options')) {
        wp_send_json_error(['message' => 'Admin access required']);
    }
    
    $diagnostics = ['started' => true];
    
    try {
        // Step 1: Check if class exists
        $diagnostics['step1_class_check'] = class_exists('VMO_Deep_Research') ? '✅ Class exists' : '❌ Class NOT found';
        
        if (!class_exists('VMO_Deep_Research')) {
            // Try loading it
            $file_path = VMO_AI_BUILDER_DIR . 'includes/class-vmo-deep-research.php';
            $diagnostics['class_file_exists'] = file_exists($file_path) ? '✅ File exists' : '❌ File missing';
            if (file_exists($file_path)) {
                require_once $file_path;
                $diagnostics['class_loaded_manually'] = class_exists('VMO_Deep_Research') ? '✅ Now exists' : '❌ Still missing';
            }
        }
        
        if (!class_exists('VMO_Deep_Research')) {
            wp_send_json_error($diagnostics);
        }
        
        // Step 2: Try instantiating
        $diagnostics['step2_instantiate'] = 'Attempting...';
        $research = new VMO_Deep_Research();
        $diagnostics['step2_instantiate'] = '✅ Instance created';
        
        // Step 3: Check API key
        $api_key = get_option('vmo_openai_api_key');
        $diagnostics['step3_api_key'] = !empty($api_key) ? '✅ API key exists (ends: ...' . substr($api_key, -6) . ')' : '❌ API key missing';
        
        // Step 4: Try a minimal test call
        $diagnostics['step4_test_call'] = 'Attempting minimal research call...';
        
        $test_data = [
            'business_name' => 'Test Business',
            'business_type' => 'service',
            'industry' => 'consulting',
            'location' => 'New York',
            'target_market' => 'small businesses'
        ];
        
        // Call chunk 1 only (4 API calls)
        $result = $research->conduct_deep_research($test_data, 1);
        
        $diagnostics['step4_test_call'] = '✅ Research chunk 1 completed';
        $diagnostics['step4_result_keys'] = is_array($result) ? array_keys($result) : 'not array';
        $diagnostics['step4_is_complete'] = $result['is_complete'] ?? 'unknown';
        $diagnostics['step4_current_chunk'] = $result['current_chunk'] ?? 'unknown';
        
        wp_send_json_success($diagnostics);
        
    } catch (Throwable $e) {
        $diagnostics['fatal_error'] = $e->getMessage();
        $diagnostics['error_file'] = $e->getFile() . ':' . $e->getLine();
        $diagnostics['error_trace'] = substr($e->getTraceAsString(), 0, 1000);
        wp_send_json_error($diagnostics);
    }
});

// Get user status for auto-greeting system
add_action('wp_ajax_vmo_get_user_status_for_greeting', function() {
    if (!vmo_verify_admin_security()) {
        wp_send_json_error(array('message' => 'Security check failed'));
        return;
    }
    
    $user_id = get_current_user_id();
    $user = get_userdata($user_id);
    $username = function_exists('vmo_get_greeting_display_name')
        ? vmo_get_greeting_display_name()
        : ($user ? $user->display_name : 'there');
    
    // Check if user has completed consultation
    global $wpdb;
    $consultation_table = $wpdb->prefix . 'vmo_consultations';
    
    $consultation = $wpdb->get_row(
        $wpdb->prepare(
            "SELECT * FROM {$consultation_table} WHERE user_id = %d AND status IN ('completed','implemented','generating_media') ORDER BY updated_at DESC LIMIT 1",
            $user_id
        )
    );
    
    // Check if user has a published website
    $has_published_site = false;
    $website_content = get_user_meta($user_id, 'vmo_published_website', true);
    if (!empty($website_content)) {
        $has_published_site = true;
    }
    
    // Check for new KPIs/notifications
    $has_new_data = false;
    $new_data_summary = '';
    
    // Check for new leads/contacts
    $contacts_table = $wpdb->prefix . 'vmo_contacts';
    $new_contacts = 0;
    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
    if ($wpdb->get_var("SHOW TABLES LIKE '{$contacts_table}'") == $contacts_table) {
        $new_contacts = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(*) FROM {$contacts_table} WHERE user_id = %d AND created_at > DATE_SUB(NOW(), INTERVAL 24 HOUR)",
                $user_id
            )
        );
    }
    
    // VOICE SYSTEM REMOVED: Check for new phone calls disabled
    // $phone_calls_table = $wpdb->prefix . 'vmo_phone_calls';
    $new_calls = 0;
    // if ($wpdb->get_var("SHOW TABLES LIKE '{$phone_calls_table}'") == $phone_calls_table) {
    //     $new_calls = $wpdb->get_var(
    //         $wpdb->prepare(
    //             "SELECT COUNT(*) FROM {$phone_calls_table} WHERE user_id = %d AND created_at > DATE_SUB(NOW(), INTERVAL 24 HOUR)",
    //             $user_id
    //         )
    //     );
    // }
    
    if ($new_contacts > 0 || $new_calls > 0) {
        $has_new_data = true;
        $parts = array();
        if ($new_contacts > 0) {
            $parts[] = "{$new_contacts} new contact" . ($new_contacts > 1 ? 's' : '');
        }
        if ($new_calls > 0) {
            $parts[] = "{$new_calls} new phone call" . ($new_calls > 1 ? 's' : '');
        }
        $new_data_summary = "You have " . implode(' and ', $parts) . " since yesterday.";
    }
    
    $requested_greeting = isset($_POST['requested_greeting']) ? sanitize_key(wp_unslash($_POST['requested_greeting'])) : '';
    $quicklaunch_first_entry = !empty($_POST['quicklaunch_first_entry']) && (string) wp_unslash($_POST['quicklaunch_first_entry']) === '1';
    $local_date = isset($_POST['local_date']) ? sanitize_text_field(wp_unslash($_POST['local_date'])) : '';
    $local_time = isset($_POST['local_time']) ? sanitize_text_field(wp_unslash($_POST['local_time'])) : '';

    // Determine scenario
    $scenario = 'first_time';
    $paused_for_credit = !empty(get_user_meta($user_id, 'vmo_consultation_paused_for_credit', true));
    $current_consultation_id = (string) get_user_meta($user_id, 'vmo_current_consultation', true);
    $consultation_blob = $current_consultation_id !== ''
        ? get_user_meta($user_id, 'vmo_consultation_data_' . $current_consultation_id, true)
        : array();
    $paused_section = is_array($consultation_blob) ? (string) ($consultation_blob['current_section'] ?? '') : '';

    if ($quicklaunch_first_entry && $requested_greeting !== 'daily') {
        $scenario = 'first_time';
    } elseif ($consultation && $has_published_site) {
        $scenario = 'active_business';
    } elseif ($paused_for_credit && $current_consultation_id !== '' && is_array($consultation_blob) && (string) ($consultation_blob['status'] ?? '') === 'paused_insufficient_credit') {
        // RESUME after credit top-up: detect explicit paused state so Jerreme welcomes
        // the user back and offers Continue vs Start Fresh from the exact checkpoint.
        $balance_now = function_exists('vmo_get_token_balance') ? (int) vmo_get_token_balance($user_id) : 0;
        $required_now = function_exists('vmo_get_token_cost') ? (int) vmo_get_token_cost('consultation_message') : 405;
        if ($balance_now >= $required_now) {
            $scenario = 'consultation_resume_after_topup';
        } else {
            $scenario = 'consultation_paused_for_credit';
        }
    } elseif ($consultation || get_user_meta($user_id, 'vmo_consultation_started', true)) {
        $scenario = 'incomplete_consultation';
    }
    
    // Check if this is truly first time. Daily greetings should not rewrite the
    // user's onboarding scenario or mark first-time state incorrectly.
    if ($requested_greeting !== 'daily') {
        if ($quicklaunch_first_entry) {
            update_user_meta($user_id, 'vmo_first_greeting_done', true);
        } elseif (get_user_meta($user_id, 'vmo_first_greeting_done', true)) {
            if ($scenario === 'first_time') {
                $scenario = 'incomplete_consultation';
            }
        } else {
            // Mark first greeting as done
            update_user_meta($user_id, 'vmo_first_greeting_done', true);
        }
    }
    
    // Generate personalized greeting based on scenario
    $greeting = '';
    $section_labels = array(
        'business_basics' => 'business basics',
        'target_audience' => 'target audience',
        'business_goals' => 'business goals',
        'reference_website' => 'the reference website step',
        'contact_preferences' => 'contact preferences',
    );
    $section_label = $paused_section !== '' && isset($section_labels[$paused_section]) ? $section_labels[$paused_section] : 'where we left off';

    if ($requested_greeting === 'daily') {
        $scenario = $scenario === 'active_business' ? 'daily_active_business' : 'daily_check_in';
        $greeting = "Welcome back, {$username}. Jarvis here. What would you like to do today? I can summarize your emails, check your website health, review visits, conversions, rankings, and other key signals, or guide you to the dashboard for the full detailed overview. If you want instant voice replies, open the gear icon and switch from Default Chat Voice to Premium Live Voice.";
    } elseif ($scenario === 'active_business') {
        // User has completed consultation and has active business
        $greeting = "Welcome back, {$username}. Jarvis here. Your VMO team is ready to keep building, refining, and managing your business. If you want strategy, content, design, or new execution work, open the red Business Wizard button and Jerreme will take it from there. For instant voice replies, use the gear icon to switch from Default Chat Voice to Premium Live Voice.";
    } elseif ($scenario === 'consultation_resume_after_topup') {
        $greeting = "Welcome back, {$username}. Jerreme here. Your top-up landed and your VMO Credits are healthy again. You paused in the middle of {$section_label}. Click the red Business Wizard button and I will pick up exactly where we left off — or just tell me you want to start the consultation fresh and we will reset cleanly.";
    } elseif ($scenario === 'consultation_paused_for_credit') {
        $greeting = "Welcome back, {$username}. Jarvis here. Your consultation with Jerreme is paused because your VMO Credits ran low — your exact place in the conversation is saved. Open My Plan to add a booster or upgrade your subscription, then return and Jerreme will continue from {$section_label}.";
    } elseif ($scenario === 'incomplete_consultation') {
        // User has started but not completed consultation
        $greeting = "Welcome back, {$username}. I'm Jarvis, your VMO launch guide. Your builder is ready, and behind me is the full VMO AI team covering design, content, SEO, documents, media, and automation. Click the red Business Wizard button to speak with Jerreme and continue your consultation. For instant voice replies, use the gear icon to switch from Default Chat Voice to Premium Live Voice.";
    } else {
        // First time user
        $greeting = "Welcome, {$username} — and congratulations on choosing VMO. This is one of the best decisions you could have made for your business, because VMO is not just a website builder; it is your entire business builder and manager. I am Jarvis, the AI Coordinator for your VMO plugin. You can think of me as your operations manager. I work alongside Jerreme, your specialized business consultant, and more than 35 specialized VMO AI agents across strategy, business consultation, website building, content, design, SEO, media, documents, email, phone, CRM, payments, reporting, automation, and launch operations. If you need help, guidance, or clarification at any point, type to me or press Start Voice and talk to me. I will guide you through the plugin, answer questions, and help you perform the right action. Your most important first step is the Business Wizard consultation. Click the red Business Wizard button so Jerreme can learn your business dreams, goals, customers, offers, market, brand, and operating needs. That consultation becomes the foundation VMO uses to set up the right connected business system instead of a generic website. Premium Live Voice can respond almost instantly when available. Default Chat Voice is less expensive but usually a few seconds slower. You can choose your preferred voice and mode from the gear icon at the top of the Jarvis chat panel.";
    }
    
    wp_send_json_success(array(
        'scenario' => $scenario,
        'username' => $username,
        'display_name' => $username,
        'greeting' => $greeting,
        'has_new_data' => $has_new_data,
        'new_data_summary' => $new_data_summary,
        'has_published_site' => $has_published_site,
        'has_consultation' => !empty($consultation),
        'status' => $scenario
    ));
});

// Save greeting to chat history so AI has full context
add_action('wp_ajax_vmo_save_greeting_to_history', function() {
    if (!vmo_verify_admin_security()) {
        wp_send_json_error(array('message' => 'Security check failed'));
        return;
    }
    
    $greeting = isset($_POST['greeting']) ? sanitize_textarea_field(wp_unslash($_POST['greeting'])) : '';
    
    if (empty($greeting)) {
        wp_send_json_error(array('message' => 'No greeting provided'));
        return;
    }
    
    $user_id = get_current_user_id();
    global $wpdb;
    $chat_history_table = $wpdb->prefix . 'vmo_chat_history';
    
    // Save greeting as assistant message to database
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    $result = $wpdb->insert(
        $chat_history_table,
        [
            'user_id' => $user_id,
            'message_role' => 'assistant',
            'message_content' => $greeting,
            'conversation_context' => 'auto_greeting',
            'chat_type' => isset($_POST['chat_type']) ? sanitize_text_field(wp_unslash($_POST['chat_type'])) : 'jarvis',
            'created_at' => current_time('mysql')
        ],
        ['%d', '%s', '%s', '%s', '%s', '%s']
    );
    
    if ($result === false) {
        if (defined('WP_DEBUG') && WP_DEBUG) error_log('❌ VMO: Failed to save greeting to database: ' . $wpdb->last_error);
        wp_send_json_error(array('message' => 'Failed to save greeting', 'error' => $wpdb->last_error));
        return;
    }
    
    if (defined('WP_DEBUG') && WP_DEBUG) error_log('✅ VMO: Greeting saved to chat history for AI context');
    wp_send_json_success(array('message' => 'Greeting saved successfully'));
});

// === Daily Progress Snapshot Cron ===
add_action('vmo_daily_progress_snapshot', function() {
    if (!function_exists('wp_get_current_user')) return;
    global $wpdb;
    $chat_history_table = $wpdb->prefix . 'vmo_chat_history';
    $summaries_table = $wpdb->prefix . 'vmo_progress_summaries';

    // Summarize per user based on last 24h activity
    $since = gmdate('Y-m-d H:i:s', time() - DAY_IN_SECONDS);
    $user_ids = $wpdb->get_col($wpdb->prepare(
        "SELECT DISTINCT user_id FROM $chat_history_table WHERE created_at >= %s LIMIT 500",
        $since
    ));
    if (empty($user_ids)) return;

    foreach ($user_ids as $uid) {
        $count_msgs = (int)$wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM $chat_history_table WHERE user_id = %d AND created_at >= %s",
            $uid, $since
        ));
        $count_ai = (int)$wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM $chat_history_table WHERE user_id = %d AND message_role='assistant' AND created_at >= %s",
            $uid, $since
        ));
        $count_user = $count_msgs - $count_ai;
        $date = gmdate('Y-m-d');

        // NOTE: $summary is built below after KPI snapshot + enriched metrics

        // Upsert summary for the day
        $existing_id = $wpdb->get_var($wpdb->prepare(
            "SELECT id FROM $summaries_table WHERE user_id = %d AND period = 'daily' AND period_date = %s",
            $uid, $date
        ));
        if ($existing_id) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $wpdb->update($summaries_table, [
                'summary' => $summary,
                'updated_at' => current_time('mysql')
            ], [ 'id' => $existing_id ], [ '%s', '%s' ], [ '%d' ]);
        } else {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $wpdb->insert($summaries_table, [
                'user_id' => $uid,
                'period' => 'daily',
                'period_date' => $date,
                'summary' => $summary,
                'created_at' => current_time('mysql')
            ], [ '%d', '%s', '%s', '%s', '%s' ]);
        }

        // Also compute a weekly rollup every Monday for the previous 7 days
        $dow = (int) gmdate('N'); // 1..7

        // ── KPI Snapshot + enriched daily metrics ──
        // Record KPI history for trend tracking
        if (class_exists('VMO_KPI_Dashboard')) {
            try {
                $kpi_dash = new VMO_KPI_Dashboard();
                if (method_exists($kpi_dash, 'snapshot_kpis')) {
                    $kpi_dash->snapshot_kpis((int) $uid);
                }
            } catch (\Exception $e) {} // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
        }

        // Enrich summary with task/action metrics
        $activities_table = $wpdb->prefix . 'vmo_daily_activities';
        $actions_table_cron = $wpdb->prefix . 'vmo_actions';
        $follow_ups_table = $wpdb->prefix . 'vmo_follow_ups';

        $tasks_completed = 0;
        $tasks_total = 0;
        $actions_completed = 0;
        $actions_pending = 0;
        $overdue_count = 0;

        // Count daily activity tasks for today
        $suppress = $wpdb->suppress_errors(true);
        $tasks_total = (int)$wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM $activities_table WHERE user_id = %d AND activity_date = %s",
            $uid, $date
        ));
        $tasks_completed = (int)$wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM $activities_table WHERE user_id = %d AND activity_date = %s AND status = 'completed'",
            $uid, $date
        ));

        // Count actions
        $actions_completed = (int)$wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM $actions_table_cron WHERE user_id = %d AND DATE(completed_at) = %s AND status = 'completed'",
            $uid, $date
        ));
        $actions_pending = (int)$wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM $actions_table_cron WHERE user_id = %d AND status = 'pending'",
            $uid
        ));

        // Overdue tasks from previous days
        $overdue_count = (int)$wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM $activities_table WHERE user_id = %d AND activity_date < %s AND status NOT IN ('completed','skipped')",
            $uid, $date
        ));

        // Overdue follow-ups
        $overdue_followups = (int)$wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM $follow_ups_table WHERE user_id = %d AND due_date < %s AND status NOT IN ('completed','cancelled')",
            $uid, $date
        ));
        $wpdb->suppress_errors($suppress);

        // Build enriched summary
        $summary = "Daily activity summary for $date:\n".
                   "• Total messages: $count_msgs (User: $count_user, AI: $count_ai)\n".
                   "• Tasks: $tasks_completed / $tasks_total completed\n".
                   "• Actions completed today: $actions_completed | Pending: $actions_pending\n".
                   "• Overdue tasks: $overdue_count | Overdue follow-ups: $overdue_followups\n".
                   "• Status: " . ($tasks_total > 0 && $tasks_completed >= $tasks_total ? "All tasks completed! " : "") .
                   ($overdue_count > 0 ? "⚠️ $overdue_count overdue items need attention. " : "") .
                   "Review milestones and next actions.";

        // Log to activity log
        $al_table = $wpdb->prefix . 'vmo_activity_log';
        $suppress2 = $wpdb->suppress_errors(true);
        $wpdb->insert($al_table, [
            'user_id' => (int) $uid,
            'action_type' => 'daily_snapshot',
            'description' => "Daily snapshot: $tasks_completed/$tasks_total tasks, $actions_completed actions done, $overdue_count overdue",
            'source_system' => 'system',
            'created_at' => current_time('mysql'),
        ], ['%d', '%s', '%s', '%s', '%s']);
        $wpdb->suppress_errors($suppress2);
        if ($dow === 1) {
            $week_start = gmdate('Y-m-d', strtotime('monday this week'));
            $week_end = gmdate('Y-m-d', strtotime('sunday this week'));

            $exists_week = $wpdb->get_var($wpdb->prepare(
                "SELECT id FROM $summaries_table WHERE user_id = %d AND period = 'weekly' AND period_date = %s",
                $uid, $week_start
            ));
            if (!$exists_week) {
                $since_week = gmdate('Y-m-d H:i:s', time() - 7 * DAY_IN_SECONDS);
                $count_msgs_w = (int)$wpdb->get_var($wpdb->prepare(
                    "SELECT COUNT(*) FROM $chat_history_table WHERE user_id = %d AND created_at >= %s",
                    $uid, $since_week
                ));
                $count_ai_w = (int)$wpdb->get_var($wpdb->prepare(
                    "SELECT COUNT(*) FROM $chat_history_table WHERE user_id = %d AND message_role='assistant' AND created_at >= %s",
                    $uid, $since_week
                ));
                $count_user_w = $count_msgs_w - $count_ai_w;

                $recent_dailies = $wpdb->get_results($wpdb->prepare(
                    "SELECT period_date, summary FROM $summaries_table WHERE user_id = %d AND period='daily' AND period_date >= %s ORDER BY period_date DESC LIMIT 3",
                    $uid, $since_week
                ), ARRAY_A);
                $daily_snippets = '';
                if (!empty($recent_dailies)) {
                    foreach ($recent_dailies as $row) {
                        $daily_snippets .= "- " . $row['period_date'] . ": " . wp_strip_all_tags((string)$row['summary']) . "\n";
                    }
                }

                $weekly_text = "Weekly activity summary (".$week_start." → ".$week_end."):\n".
                               "• Total messages: $count_msgs_w\n".
                               "• User messages: $count_user_w\n".
                               "• AI responses: $count_ai_w\n".
                               ($daily_snippets ? ("\nRecent daily notes:\n".$daily_snippets) : '') .
                               "\nStatus: Keep momentum. Review milestones and set next week's top 3 priorities.";

                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                $wpdb->insert($summaries_table, [
                    'user_id' => $uid,
                    'period' => 'weekly',
                    'period_date' => $week_start,
                    'summary' => $weekly_text,
                    'created_at' => current_time('mysql')
                ], [ '%d', '%s', '%s', '%s', '%s' ]);
            }
        }
    }
});

// Power Dialer Batch Processing Hook
// Handles scheduled batches of outbound calls for Power Dialer campaigns
add_action('vmo_power_dialer_batch', function($campaign_id) {
    // Get the Twilio Manager instance and process the batch
    $twilio_manager = VMO_Twilio_Manager::get_instance();
    if ($twilio_manager && method_exists($twilio_manager, 'schedule_power_dialer_batch')) {
        // Trigger through reflection since method is private
        $reflection = new ReflectionMethod($twilio_manager, 'schedule_power_dialer_batch');
        $reflection->setAccessible(true);
        $reflection->invoke($twilio_manager, $campaign_id);
    }
}, 10, 1);

// Quarterly Insights Auto-Generation Cron Hook
// Automatically generates quarterly business insights for all active users
add_action('vmo_quarterly_insights_cron', function() {
    global $wpdb;
    
    // Get current quarter info
    $current_month = (int) date('n');
    $current_year = (int) date('Y');
    $quarter = ceil($current_month / 3);
    
    // Check if this is the first day of a new quarter
    $is_new_quarter = in_array($current_month, [1, 4, 7, 10]) && (int) date('j') <= 7;
    
    if (!$is_new_quarter) {
        return; // Only run in first week of each quarter
    }
    
    // Get all users with active subscriptions or token balance
    $users = $wpdb->get_col(
        $wpdb->prepare(
            "SELECT DISTINCT user_id FROM {$wpdb->usermeta} 
            WHERE meta_key = %s AND CAST(meta_value AS UNSIGNED) > 0
            LIMIT 1000",
            'vmo_token_balance'
        )
    );
    
    if (!$users) {
        return;
    }
    
    foreach ($users as $user_id) {
        // Check if we already generated a report for this quarter
        $reports_table = $wpdb->prefix . 'vmo_quarterly_reports';
        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
        if ($wpdb->get_var("SHOW TABLES LIKE '{$reports_table}'") === $reports_table) {
            $existing = $wpdb->get_var($wpdb->prepare(
                "SELECT id FROM {$reports_table} WHERE user_id = %d AND quarter = %d AND year = %d",
                $user_id, $quarter, $current_year
            ));
            if ($existing) {
                continue; // Already generated
            }
        }
        
        // Generate quarterly insights for this user
        // We need to simulate the AJAX environment for the user
        wp_set_current_user($user_id);
        
        // Call the generation function directly (it will use current_user_id)
        if (function_exists('vmo_generate_quarterly_insights_for_user')) {
            vmo_generate_quarterly_insights_for_user($user_id);
        }
    }
});

// Register the quarterly cron schedule
add_action('init', function() {
    if (!wp_next_scheduled('vmo_quarterly_insights_cron')) {
        // Schedule to run daily at 9 AM - the function itself checks if it's the right time
        wp_schedule_event(strtotime('09:00:00'), 'daily', 'vmo_quarterly_insights_cron');
    }
});

// Feature flag: Payments (Stripe/Connect). ENABLED for VMO Payments functionality.
if (!defined('VMO_PAYMENTS_ENABLED')) {
    // Payments ENABLED - VMO white-labeled Stripe payment processing
    define('VMO_PAYMENTS_ENABLED', true);
}

// VMO AI Builder v8.0.0: Unified Business Development Platform with Multi-Agent AI System
// ═══════════════════════════════════════════════════════════════════════════════
// RINGTONE SANITIZER — runs once per admin load to scrub any stored ringtone URL
// that points at a video/image file (e.g. an iPhone .mov upload). Browsers will
// otherwise strip-and-loop the audio track of the video, producing the autoplay
// loop the user has been hearing. Cleans BOTH wp_options (legacy) and per-user
// meta (current storage) for every user. Idempotent and cheap.
// ═══════════════════════════════════════════════════════════════════════════════
add_action('init', function () {
    if (!is_admin()) { return; }
    if (get_transient('vmo_ringtone_sanitizer_v2_done')) { return; }

    $bad_re = '/\.(mov|mp4|m4v|webm|mkv|avi|3gp|wmv|flv|jpg|jpeg|png|gif|webp|bmp|svg|pdf|heic)(\?|#|$)/i';
    $audio_re = '/\.(mp3|wav|ogg|m4a|aac|flac|opus|weba)(\?|#|$)/i';

    // Per-user meta sweep
    global $wpdb;
    $rows = $wpdb->get_results("SELECT user_id, meta_value FROM {$wpdb->usermeta} WHERE meta_key = 'vmo_ring_url' AND meta_value <> ''");
    if (is_array($rows)) {
        foreach ($rows as $row) {
            $url = (string) $row->meta_value;
            if ($url === '') { continue; }
            $is_bad   = (bool) preg_match($bad_re, $url);
            $is_audio = (bool) preg_match($audio_re, $url);
            if ($is_bad || !$is_audio) {
                update_user_meta((int) $row->user_id, 'vmo_ring_url', '');
                update_user_meta((int) $row->user_id, 'vmo_ring_attachment_id', 0);
                error_log('[VMO Ring Sanitizer] Cleared non-audio ringtone for user ' . $row->user_id . ': ' . $url);
            }
        }
    }

    // BROAD sweep v2: any user_meta or option that stores a single URL pointing
    // at a video/image file. This catches stray fields like "background_video_url",
    // "vmo_call_tone", etc. that we never explicitly named.
    $broad = $wpdb->get_results($wpdb->prepare(
        "SELECT umeta_id, user_id, meta_key, meta_value FROM {$wpdb->usermeta}
         WHERE meta_value REGEXP %s
         AND CHAR_LENGTH(meta_value) < 1024
         AND meta_key REGEXP %s",
        '\\.(mov|mp4|m4v|webm|mkv|avi|3gp|wmv|flv|heic)(\\?|#|$)',
        '(ring|tone|sound|audio|alert|notification|background|video|media|asset)'
    ));
    if (is_array($broad)) {
        foreach ($broad as $row) {
            $val = (string) $row->meta_value;
            // Skip values that are obvious arrays/JSON/serialized blobs to avoid corrupting structures.
            if (preg_match('/^[{\[a:]/', trim($val))) { continue; }
            // Only act if the value looks like a single URL to a non-audio file.
            if (!preg_match('#^https?://[^\s"\'<>]+$#i', trim($val))) { continue; }
            if (preg_match($audio_re, $val)) { continue; }
            update_user_meta((int) $row->user_id, $row->meta_key, '');
            error_log('[VMO Ring Sanitizer v2] Cleared user_meta ' . $row->meta_key . ' for user ' . $row->user_id . ': ' . $val);
        }
    }

    // Legacy wp_options fallbacks (named)
    foreach (['vmo_ring_url', 'vmo_default_ring_url'] as $opt_key) {
        $url = (string) get_option($opt_key, '');
        if ($url === '') { continue; }
        $is_bad   = (bool) preg_match($bad_re, $url);
        $is_audio = (bool) preg_match($audio_re, $url);
        if ($is_bad || !$is_audio) {
            update_option($opt_key, '');
            error_log('[VMO Ring Sanitizer] Cleared non-audio ringtone option ' . $opt_key . ': ' . $url);
        }
    }

    // Broad option sweep
    $opt_rows = $wpdb->get_results($wpdb->prepare(
        "SELECT option_id, option_name, option_value FROM {$wpdb->options}
         WHERE option_value REGEXP %s
         AND CHAR_LENGTH(option_value) < 1024
         AND option_name REGEXP %s",
        '\\.(mov|mp4|m4v|webm|mkv|avi|3gp|wmv|flv|heic)(\\?|#|$)',
        '^vmo_.*(ring|tone|sound|audio|alert|notification|background|video|media|asset)'
    ));
    if (is_array($opt_rows)) {
        foreach ($opt_rows as $row) {
            $val = (string) $row->option_value;
            if (preg_match('/^[{\[a:]/', trim($val))) { continue; }
            if (!preg_match('#^https?://[^\s"\'<>]+$#i', trim($val))) { continue; }
            if (preg_match($audio_re, $val)) { continue; }
            update_option($row->option_name, '');
            error_log('[VMO Ring Sanitizer v2] Cleared option ' . $row->option_name . ': ' . $val);
        }
    }

    set_transient('vmo_ringtone_sanitizer_v2_done', 1, DAY_IN_SECONDS);
}, 5);

// VMO AI Builder v8.0.0: Unified Business Development Platform with Multi-Agent AI System
// ═══════════════════════════════════════════════════════════════════════════════
// CENTRALIZED MODEL REGISTRY v8.3.0 — 6-Tier Model System
// All model references flow through vmo_get_model($tier).
// Override any model in wp-config.php by defining its constant.
// ═══════════════════════════════════════════════════════════════════════════════

// One-time migration: clear any stale wp_options model overrides that pin model
// IDs that don't exist in the current OpenAI catalog. Per developers.openai.com
// (May 2026): flagship is gpt-5.5 / gpt-5.5-pro; smaller variants are gpt-5.4-mini
// and gpt-5.4-nano (no gpt-5.5-mini or gpt-5.5-nano); image is gpt-image-2;
// realtime is gpt-realtime-1.5. Without this migration, an old override would mask
// the corrected constants below and every API call would fail with "model not found".
if (!get_option('vmo_model_override_migration_v2_done')) {
    $__vmo_bad_patterns = [
        'gpt-5.5-mini', 'gpt-5.5-nano',          // never existed in catalog
        'gpt-5-nano', 'gpt-5-mini', 'gpt-5-pro', // pre-5.5 generation, superseded
        'gpt-realtime-1.0', 'openai-image-generation-2', 'gpt-image-1',
    ];
    foreach (['nano','mini','fast','strategic','pro','supreme','realtime','realtime_fallback','fallback','image','image_generation','sora','sora_fallback','video_standard','video_pro','media_reasoning'] as $__vmo_tier) {
        $__vmo_existing = (string) get_option('vmo_model_override_' . $__vmo_tier, '');
        if ($__vmo_existing === '') { continue; }
        $__vmo_should_clear = false;
        foreach ($__vmo_bad_patterns as $__vmo_bad) {
            if (strcasecmp($__vmo_existing, $__vmo_bad) === 0) { $__vmo_should_clear = true; break; }
        }
        // Also clear bare "gpt-5" (without .4/.5 suffix) which is the prior generation flagship
        if (!$__vmo_should_clear && strcasecmp($__vmo_existing, 'gpt-5') === 0) { $__vmo_should_clear = true; }
        // Also clear bare "gpt-realtime" (without -1.5) on the realtime tier
        if (!$__vmo_should_clear && in_array($__vmo_tier, ['realtime'], true) && strcasecmp($__vmo_existing, 'gpt-realtime') === 0) { $__vmo_should_clear = true; }
        if ($__vmo_should_clear) {
            delete_option('vmo_model_override_' . $__vmo_tier);
        }
    }
    update_option('vmo_model_override_migration_v2_done', 1, true);
    // Mark v1 done as well so the older block (if anyone re-runs it) doesn't fight us
    update_option('vmo_model_override_migration_v1_done', 1, true);
}

// One-time Realtime migration v3: remove preview-era realtime model overrides that
// no longer appear in the current OpenAI Realtime model catalog. This prevents a
// saved option from pinning Premium Live Voice to model_not_found forever.
if (!get_option('vmo_realtime_model_override_migration_v3_done')) {
    foreach (['realtime','realtime_fallback'] as $__vmo_tier) {
        $__vmo_existing = strtolower((string) get_option('vmo_model_override_' . $__vmo_tier, ''));
        if (in_array($__vmo_existing, ['gpt-4o-mini-realtime-preview', 'gpt-4o-realtime-preview', 'gpt-realtime'], true)) {
            delete_option('vmo_model_override_' . $__vmo_tier);
            error_log('[VMO Realtime Model Migration] Cleared stale realtime override for ' . $__vmo_tier . ': ' . $__vmo_existing);
        }
    }
    update_option('vmo_realtime_model_override_migration_v3_done', 1, true);
}

// Tier 1: Nano — cheapest, fastest. Chat, CRM queries, classification, data extraction.
// (GPT-5.5 has no nano variant in the OpenAI catalog; use the 5.4 nano.)
if (!defined('VMO_MODEL_NANO')) {
    define('VMO_MODEL_NANO', 'gpt-5.4-nano');
}
// Tier 2: Mini — balanced cost/quality. Page gen, docs, scanning, email campaigns.
// (GPT-5.5 has no mini variant; use the 5.4 mini.)
if (!defined('VMO_MODEL_MINI')) {
    define('VMO_MODEL_MINI', 'gpt-5.4-mini');
}
// Tier 3: Fast — live conversations, consultations, phone reasoning.
if (!defined('VMO_OPENAI_FAST_MODEL')) {
    define('VMO_OPENAI_FAST_MODEL', 'gpt-5.4-mini');
}
// Tier 4: Strategic — deep research, strategy, website architecture, complex reasoning.
if (!defined('VMO_OPENAI_STRATEGIC_MODEL')) {
    define('VMO_OPENAI_STRATEGIC_MODEL', 'gpt-5.5');
}
// Tier 4.5: Pro — highest quality. Core page generation, QA, website architecture.
if (!defined('VMO_MODEL_PRO')) {
    define('VMO_MODEL_PRO', 'gpt-5.5-pro');
}
// Tier 5: Realtime — native speech-to-speech for phone calls.
if (!defined('VMO_OPENAI_REALTIME_MODEL')) {
    define('VMO_OPENAI_REALTIME_MODEL', 'gpt-realtime-mini');
}
if (!defined('VMO_OPENAI_REALTIME_FALLBACK_MODEL')) {
    define('VMO_OPENAI_REALTIME_FALLBACK_MODEL', 'gpt-realtime-1.5');
}
// Tier 6: Fallback — last-resort when primary models are unavailable.
if (!defined('VMO_MODEL_FALLBACK')) {
    define('VMO_MODEL_FALLBACK', 'gpt-5.4-nano');
}
if (!defined('VMO_MODEL_SUPREME')) {
    define('VMO_MODEL_SUPREME', VMO_MODEL_PRO);
}
if (!defined('VMO_MODEL_STRATEGIC')) {
    define('VMO_MODEL_STRATEGIC', VMO_OPENAI_STRATEGIC_MODEL);
}
if (!defined('VMO_MODEL_FAST')) {
    define('VMO_MODEL_FAST', VMO_OPENAI_FAST_MODEL);
}
if (!defined('VMO_MODEL_MEDIA_REASONING')) {
    define('VMO_MODEL_MEDIA_REASONING', VMO_MODEL_STRATEGIC);
}
// Image & Video models
if (!defined('VMO_IMAGE_MODEL')) {
    define('VMO_IMAGE_MODEL', 'gpt-image-2');
}
if (!defined('VMO_OPENAI_IMAGE_MODEL')) {
    define('VMO_OPENAI_IMAGE_MODEL', VMO_IMAGE_MODEL);
}
if (!defined('VMO_VIDEO_MODEL_STANDARD')) {
    define('VMO_VIDEO_MODEL_STANDARD', 'sora-2');
}
if (!defined('VMO_VIDEO_MODEL_PRO')) {
    define('VMO_VIDEO_MODEL_PRO', 'sora-2-pro');
}
if (!defined('VMO_OPENAI_SORA_MODEL')) {
    define('VMO_OPENAI_SORA_MODEL', VMO_VIDEO_MODEL_PRO);
}
if (!defined('VMO_OPENAI_SORA_FALLBACK_MODEL')) {
    define('VMO_OPENAI_SORA_FALLBACK_MODEL', VMO_VIDEO_MODEL_STANDARD);
}
// Aliases — used by legacy code. Map to the tiered system.
if (!defined('VMO_PRIMARY_MODEL')) {
    define('VMO_PRIMARY_MODEL', VMO_OPENAI_FAST_MODEL);
}
if (!defined('VMO_OPENAI_MODEL')) {
    define('VMO_OPENAI_MODEL', VMO_OPENAI_STRATEGIC_MODEL);
}

/**
 * Centralized model resolver — the ONLY place to get the model ID for a given tier.
 *
 * Checks wp_options first (set by auto-update system or admin override),
 * then falls back to the compile-time constant.
 *
 * @param string $tier One of: nano, mini, fast, strategic, realtime, realtime_fallback, fallback, image, sora, sora_fallback
 * @return string The model identifier string.
 */
function vmo_get_model(string $tier = 'fast'): string {
    $is_realtime_tier = in_array($tier, ['realtime', 'realtime_fallback'], true);

    // Check for admin override stored in wp_options (set by auto-update or settings page)
    $override = get_option('vmo_model_override_' . $tier, '');
    if (!empty($override)) {
        if (!$is_realtime_tier || preg_match('/^gpt-realtime(?:-[A-Za-z0-9.]+)?$/i', (string) $override)) {
            return $override;
        }

        error_log('[VMO Model Resolver] Ignoring invalid realtime override for tier ' . $tier . ': ' . $override);
    }

    $map = [
        'nano'              => VMO_MODEL_NANO,
        'mini'              => VMO_MODEL_MINI,
        'fast'              => VMO_MODEL_FAST,
        'strategic'         => VMO_MODEL_STRATEGIC,
        'pro'               => VMO_MODEL_SUPREME,
        'supreme'           => VMO_MODEL_SUPREME,
        'media_reasoning'   => VMO_MODEL_MEDIA_REASONING,
        'realtime'          => VMO_OPENAI_REALTIME_MODEL,
        'realtime_fallback' => VMO_OPENAI_REALTIME_FALLBACK_MODEL,
        'fallback'          => VMO_MODEL_FALLBACK,
        'image'             => VMO_OPENAI_IMAGE_MODEL,
        'image_generation'  => VMO_IMAGE_MODEL,
        'sora'              => VMO_VIDEO_MODEL_PRO,
        'sora_fallback'     => VMO_VIDEO_MODEL_STANDARD,
        'video_standard'    => VMO_VIDEO_MODEL_STANDARD,
        'video_pro'         => VMO_VIDEO_MODEL_PRO,
    ];

    $resolved = $map[$tier] ?? $map['fast'];
    if ($is_realtime_tier && !preg_match('/^gpt-realtime/i', (string) $resolved)) {
        return ($tier === 'realtime_fallback') ? 'gpt-realtime-1.5' : 'gpt-realtime-mini';
    }

    return $resolved;
}

function vmo_get_website_agent_model(string $agent_key): string {
    $agent_key = strtolower($agent_key);
    $supreme_agents = array(
        'master_orchestrator',
        'website_strategy',
        'competitor_market_intelligence',
        'visual_color_psychology',
        'visual_experience_director',
        'hero_scene_director',
        'motion_experience_director',
        'media_director',
        'conversion_architecture',
        'seo_competitor_intelligence',
        'seo_content_intelligence',
        'functional_architecture',
        'navigation_menu_director',
        'fullscreen_minisite_experience',
        'ecommerce_storytelling',
        'food_restaurant_experience',
        'mobile_experience_director',
        'final_qa_critic',
    );
    if (in_array($agent_key, $supreme_agents, true)) {
        return vmo_get_model('supreme');
    }

    $strategic_agents = array(
        'jerreme_handoff_normalizer',
        'copy_messaging',
        'section_composition',
        'component_assembly',
        'accessibility',
        'wordpress_security_integrator',
        'seo_pack_generator',
        'logo_brand_identity',
        'analytics_kpi_wiring',
        'website_kpi_wiring',
        'dashboard_integration',
        'website_refinement_assistant',
    );
    if (in_array($agent_key, $strategic_agents, true)) {
        return vmo_get_model('strategic');
    }

    return vmo_get_model('fast');
}

// ═══════════════════════════════════════════════════════════════════════════════
// AUTO-UPDATE SYSTEM v8.3.0 — Weekly model health check via WP-Cron
// Validates each model against OpenAI /v1/models. On deprecation or failure,
// auto-migrates to the next-best model and emails the admin.
// ═══════════════════════════════════════════════════════════════════════════════

/**
 * Schedule the weekly model health check cron if not already scheduled.
 */
// MODEL HEALTH CHECK CRON: Disabled on client instances — master handles model updates.
// add_action('init', function() { /* cron scheduler removed in client build */ });

/**
 * Run model health check — called by WP-Cron weekly.
 * Validates each tier's model is still available via the OpenAI API.
 */
add_action('vmo_model_health_check_cron', 'vmo_run_model_health_check');

function vmo_run_model_health_check(): void {
    $api_key = get_option('vmo_openai_api_key', '');
    if (empty($api_key)) {
        return;
    }

    // Fetch the list of available models from OpenAI
    $response = wp_remote_get('https://api.openai.com/v1/models', [
        'headers' => ['Authorization' => 'Bearer ' . $api_key],
        'timeout' => 30,
    ]);

    if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
        error_log('[VMO Model Health] Failed to fetch model list from OpenAI.');
        return;
    }

    $body = json_decode(wp_remote_retrieve_body($response), true);
    $available = [];
    if (!empty($body['data']) && is_array($body['data'])) {
        foreach ($body['data'] as $m) {
            $available[$m['id']] = true;
        }
    }

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

    // Define the fallback chain for each tier
    $fallback_chains = [
        'nano'              => ['gpt-5.4-nano', 'gpt-5.4-mini'],
        'mini'              => ['gpt-5.4-mini', 'gpt-5.4-nano'],
        'fast'              => ['gpt-5.4-mini', 'gpt-5.4-nano'],
        'strategic'         => ['gpt-5.5', 'gpt-5.4', 'gpt-5.4-mini'],
        'pro'               => ['gpt-5.5-pro', 'gpt-5.5', 'gpt-5.4'],
        'supreme'           => ['gpt-5.5-pro', 'gpt-5.5', 'gpt-5.4'],
        'realtime'          => ['gpt-realtime-mini', 'gpt-realtime-1.5', 'gpt-realtime-2'],
        'realtime_fallback' => ['gpt-realtime-1.5', 'gpt-realtime-mini', 'gpt-realtime-2'],
    ];

    $issues = [];
    foreach ($fallback_chains as $tier => $chain) {
        $current = vmo_get_model($tier);

        // If the current model is in the available list, it's fine
        if (isset($available[$current])) {
            continue;
        }

        // Current model missing — find a replacement
        $replacement = '';
        foreach ($chain as $candidate) {
            if (isset($available[$candidate]) && $candidate !== $current) {
                $replacement = $candidate;
                break;
            }
        }

        if (!empty($replacement)) {
            update_option('vmo_model_override_' . $tier, $replacement);
            $issues[] = sprintf('%s tier: %s → %s (auto-migrated)', $tier, $current, $replacement);
            error_log(sprintf('[VMO Model Health] Auto-migrated %s tier from %s to %s', $tier, $current, $replacement));
        } else {
            $issues[] = sprintf('%s tier: %s unavailable, NO replacement found!', $tier, $current);
            error_log(sprintf('[VMO Model Health] CRITICAL: %s tier model %s unavailable and no fallback found!', $tier, $current));
        }
    }

    // Email admin if any issues were found
    if (!empty($issues)) {
        $admin_email = get_option('admin_email', '');
        if (!empty($admin_email)) {
            $subject = '[VMO AI] Model Health Check — ' . count($issues) . ' model(s) auto-migrated';
            $message = "The weekly VMO model health check detected the following changes:\n\n";
            $message .= implode("\n", $issues);
            $message .= "\n\nYou can review and override model assignments in the VMO AI Dashboard → Settings → Models.";
            $message .= "\n\nThis is an automated message from your VMO AI Website Builder plugin.";
            wp_mail($admin_email, $subject, $message);
        }
    }

    update_option('vmo_last_model_health_check', current_time('mysql'));
}

/**
 * On-demand model validation — called when an API call fails with "model not found."
 * Triggers an immediate health check and returns the replacement model for the tier.
 *
 * @param string $failed_model The model that returned a "model not found" error.
 * @param string $tier         The tier this model belonged to.
 * @return string The replacement model ID.
 */
function vmo_handle_model_failure(string $failed_model, string $tier = 'fast'): string {
    error_log(sprintf('[VMO Model Fallback] Model %s failed for tier %s — triggering health check', $failed_model, $tier));

    // Schedule an async health check (non-blocking)
    if (!wp_next_scheduled('vmo_model_health_check_cron')) {
        wp_schedule_single_event(time() + 5, 'vmo_model_health_check_cron');
    }

    // Return immediate fallback for this tier
    $immediate_fallbacks = [
        'nano'      => VMO_MODEL_NANO !== $failed_model ? VMO_MODEL_NANO : VMO_MODEL_FALLBACK,
        'mini'      => VMO_MODEL_MINI !== $failed_model ? VMO_MODEL_MINI : VMO_MODEL_NANO,
        'fast'      => VMO_OPENAI_FAST_MODEL !== $failed_model ? VMO_OPENAI_FAST_MODEL : VMO_MODEL_MINI,
        'strategic' => VMO_OPENAI_STRATEGIC_MODEL !== $failed_model ? VMO_OPENAI_STRATEGIC_MODEL : VMO_OPENAI_FAST_MODEL,
        'realtime'  => VMO_OPENAI_REALTIME_MODEL !== $failed_model ? VMO_OPENAI_REALTIME_MODEL : VMO_OPENAI_REALTIME_FALLBACK_MODEL,
    ];

    return $immediate_fallbacks[$tier] ?? VMO_MODEL_FALLBACK;
}

if (!defined('VMO_TOKEN_RATE')) {
    define('VMO_TOKEN_RATE', 0.000667); // $0.000667 per VMO token
}
if (!defined('VMO_DEFAULT_AFFILIATE_COMMISSION')) {
    define('VMO_DEFAULT_AFFILIATE_COMMISSION', 5.00);
}
if (!defined('VMO_OPERATING_OVERHEAD_RATE')) {
    define('VMO_OPERATING_OVERHEAD_RATE', 0.40);
}
if (!defined('VMO_MIN_NET_PROFIT_RATE')) {
    define('VMO_MIN_NET_PROFIT_RATE', 0.25);
}
if (!defined('VMO_PROVIDER_COST_BUDGET_RATE')) {
    define('VMO_PROVIDER_COST_BUDGET_RATE', 0.30);
}

if (!function_exists('vmo_is_internal_unmetered_user')) {
    function vmo_is_internal_unmetered_user($user_id = 0) {
        $user_id = (int) ($user_id ?: get_current_user_id());
        if ($user_id <= 0) {
            return false;
        }

        if (defined('VMO_CLIENT_MODE') && VMO_CLIENT_MODE === true) {
            return false;
        }

        if (!defined('VMO_ENABLE_INTERNAL_UNMETERED') || VMO_ENABLE_INTERNAL_UNMETERED !== true) {
            return false;
        }

        if (!user_can($user_id, 'manage_options')) {
            return false;
        }

        if (defined('VMO_INTERNAL_UNMETERED_USER_IDS')) {
            $allowed_ids = VMO_INTERNAL_UNMETERED_USER_IDS;
            if (!is_array($allowed_ids)) {
                $allowed_ids = preg_split('/[\s,]+/', (string) $allowed_ids, -1, PREG_SPLIT_NO_EMPTY);
            }
            $allowed_ids = array_map('intval', $allowed_ids);
            if (!empty($allowed_ids)) {
                return in_array($user_id, $allowed_ids, true);
            }
        }

        return true;
    }
}

// Updated token costs for v8.0.0 - includes business wizard features
if (!defined('VMO_TOKENS_FULL_PAGE')) {
    define('VMO_TOKENS_FULL_PAGE', 2500); // Complete webpage
}
if (!defined('VMO_TOKENS_SECTION')) {
    define('VMO_TOKENS_SECTION', 500); // Single section (400-600 range)
}
if (!defined('VMO_TOKENS_REFINEMENT')) {
    define('VMO_TOKENS_REFINEMENT', 225); // AI refinement (150-300 range)
}
if (!defined('VMO_TOKENS_IMAGE_GENERATION')) {
    define('VMO_TOKENS_IMAGE_GENERATION', 1080); // Image generation (9x markup on ~$0.08 avg GPT Image 1 cost)
}
if (!defined('VMO_TOKENS_ADVANCED_EFFECTS')) {
    define('VMO_TOKENS_ADVANCED_EFFECTS', 175); // Animations/effects (100-250 range)
}
// VOICE SYSTEM REMOVED: Voice transcription tokens disabled
// if (!defined('VMO_TOKENS_VOICE_TRANSCRIPTION')) {
//     define('VMO_TOKENS_VOICE_TRANSCRIPTION', 10); // Voice transcription per minute
// }
if (!defined('VMO_TOKENS_MEDIA_INTEGRATION')) {
    // Media Integration: ~8K input + 4K output tokens on GPT-5.5
    // Actual cost: $0.02 (input) + $0.04 (output) = $0.06
    // 9x markup: $0.54 / $0.000667 = 810 tokens (rounded to 900 for safety margin)
    define('VMO_TOKENS_MEDIA_INTEGRATION', 900); // AI-powered creative media integration (9x markup)
}

// Business Wizard specific token costs
if (!defined('VMO_TOKENS_BUSINESS_WIZARD')) {
    define('VMO_TOKENS_BUSINESS_WIZARD', 2500); // Business wizard conversation
}

if (!defined('VMO_TOKENS_EMAIL')) {
    define('VMO_TOKENS_EMAIL', 50); // Cost per email send for rationing and abuse control
}

if (!defined('VMO_TOKENS_GENERATE_WEBSITE')) {
    define('VMO_TOKENS_GENERATE_WEBSITE', 9450); // Complete business website generation (9x markup on ~$0.70 API cost)
}
if (!defined('VMO_TOKENS_AI_CONVERSATION')) {
    define('VMO_TOKENS_AI_CONVERSATION', 50); // AI conversation per message
}
// VOICE SYSTEM REMOVED: Voice processing tokens disabled
// if (!defined('VMO_TOKENS_VOICE_PROCESSING')) {
//     define('VMO_TOKENS_VOICE_PROCESSING', 100); // Voice processing
// }
if (!defined('VMO_TOKENS_PREMIUM_FEATURES')) {
    define('VMO_TOKENS_PREMIUM_FEATURES', 1000); // Premium features
}

// ✅ NEW v8.1.0: Token costs for previously unbilled operations (fixing revenue leaks)
// All costs maintain 7-9x markup on actual API costs for sustainable 23% margin

// TTS (Text-to-Speech) - OpenAI tts-1-hd costs ~$0.030/1K chars
// At $0.000667/token, 1K chars = $0.030 / $0.000667 = ~45 tokens (actual cost)
// 9x markup: 45 * 9 = 405 tokens per 1K chars → ~200 char response = 9 actual tokens * 9 = 81
if (!defined('VMO_TOKENS_TTS_RESPONSE')) {
    define('VMO_TOKENS_TTS_RESPONSE', 81); // TTS voice response (~200 chars avg, 9x markup)
}
if (!defined('VMO_TOKENS_TTS_PER_1K_CHARS')) {
    define('VMO_TOKENS_TTS_PER_1K_CHARS', 405); // TTS per 1,000 characters (9x markup on $0.030)
}

// Consultation Greeting Audio - TTS-1-HD for consultation greeting (~600-800 chars)
// At $0.030/1K chars: ~700 chars = $0.021 actual → 31.5 tokens actual → 9x = 285
if (!defined('VMO_TOKENS_GREETING_AUDIO')) {
    define('VMO_TOKENS_GREETING_AUDIO', 285); // Consultation greeting TTS generation (9x markup)
}

// Whisper Transcription - OpenAI costs ~$0.006/min
// At $0.000667/token, 1 min = $0.006 / $0.000667 = ~9 tokens (actual cost)
// 9x markup: 9 * 9 = 81 tokens per minute
if (!defined('VMO_TOKENS_TRANSCRIPTION_PER_MIN')) {
    define('VMO_TOKENS_TRANSCRIPTION_PER_MIN', 81); // Whisper transcription per minute (9x markup)
}

// Vision Analysis (GPT-5.5 Vision) - ~$0.01-0.03 per image analysis
// At $0.000667/token, avg $0.02 = ~30 tokens (actual cost)
// 9x markup: 30 * 9 = 270 tokens
if (!defined('VMO_TOKENS_VISION_ANALYSIS')) {
    define('VMO_TOKENS_VISION_ANALYSIS', 270); // Image/vision analysis (9x markup)
}

// Deep Research - GPT-5.5 extensive research (25+ API calls, ~$0.50-2.00 per research)
// Average $1.00 actual cost = ~1500 tokens; 9x markup = 13,500 tokens
if (!defined('VMO_TOKENS_DEEP_RESEARCH')) {
    define('VMO_TOKENS_DEEP_RESEARCH', 13500); // Comprehensive deep research (9x markup)
}

// Consultation Message - Each AI response in business wizard (~$0.02-0.05)
// Average $0.03 actual cost = ~45 tokens; 9x markup = 405 tokens
if (!defined('VMO_TOKENS_CONSULTATION_MESSAGE')) {
    define('VMO_TOKENS_CONSULTATION_MESSAGE', 405); // Per consultation AI response (9x markup)
}

// Document Analysis - GPT-5.5 analyzing uploaded documents (~$0.02-0.05)
// Average $0.03 = ~45 tokens; 9x markup = 405 tokens
if (!defined('VMO_TOKENS_DOCUMENT_ANALYSIS')) {
    define('VMO_TOKENS_DOCUMENT_ANALYSIS', 405); // Document/blueprint analysis (9x markup)
}

// Email Campaign Generation - GPT generating campaign content (~$0.05-0.10)
// Average $0.07 = ~105 tokens; 9x markup = 945 tokens
if (!defined('VMO_TOKENS_EMAIL_CAMPAIGN_GENERATION')) {
    define('VMO_TOKENS_EMAIL_CAMPAIGN_GENERATION', 945); // AI email campaign content generation (9x markup)
}

// Contact Insights - AI analyzing contact data (~$0.02-0.04)
// Average $0.03 = ~45 tokens; 9x markup = 405 tokens
if (!defined('VMO_TOKENS_CONTACT_INSIGHTS')) {
    define('VMO_TOKENS_CONTACT_INSIGHTS', 405); // AI contact insights generation (9x markup)
}

// Business Roadmap Generation - Comprehensive AI planning (~$0.10-0.20)
// Average $0.15 = ~225 tokens; 9x markup = 2025 tokens
if (!defined('VMO_TOKENS_ROADMAP_GENERATION')) {
    define('VMO_TOKENS_ROADMAP_GENERATION', 2025); // Business roadmap/daily activities generation (9x markup)
}

// GPT Image 1 Generation - $0.04-0.12 per image (1024x1024 standard)
// At $0.08 average = ~120 tokens actual; 9x markup = 1080 tokens
if (!defined('VMO_TOKENS_DALLE_IMAGE')) {
    define('VMO_TOKENS_DALLE_IMAGE', 1080); // GPT Image 1 generation (9x markup on $0.08 avg)
}

// Sora 2 Pro Video Generation - Premium AI video creation
// Pricing: ~$0.60 per second of video (sora-2-pro)
// 4 sec = ~$2.40 actual → 7x markup = $16.80 → 25,200 tokens
// 8 sec = ~$4.80 actual → 7x markup = $33.60 → 50,400 tokens
// 12 sec = ~$7.20 actual → 7x markup = $50.40 → 75,600 tokens
if (!defined('VMO_TOKENS_SORA_VIDEO_4S')) {
    define('VMO_TOKENS_SORA_VIDEO_4S', 25200); // Sora 2 Pro 4-second video (7x markup)
}
if (!defined('VMO_TOKENS_SORA_VIDEO_8S')) {
    define('VMO_TOKENS_SORA_VIDEO_8S', 50400); // Sora 2 Pro 8-second video (7x markup)
}
if (!defined('VMO_TOKENS_SORA_VIDEO_12S')) {
    define('VMO_TOKENS_SORA_VIDEO_12S', 75600); // Sora 2 Pro 12-second video (7x markup)
}
// Sora 2 Standard (lower quality, ~$0.30/sec)
// 4 sec = ~$1.20 actual → 7x = $8.40 → 12,600 tokens
// 8 sec = ~$2.40 actual → 7x = $16.80 → 25,200 tokens
// 12 sec = ~$3.60 actual → 7x = $25.20 → 37,800 tokens
if (!defined('VMO_TOKENS_SORA_STD_4S')) {
    define('VMO_TOKENS_SORA_STD_4S', 12600); // Sora 2 Standard 4-second video (7x markup)
}
if (!defined('VMO_TOKENS_SORA_STD_8S')) {
    define('VMO_TOKENS_SORA_STD_8S', 25200); // Sora 2 Standard 8-second video (7x markup)
}
if (!defined('VMO_TOKENS_SORA_STD_12S')) {
    define('VMO_TOKENS_SORA_STD_12S', 37800); // Sora 2 Standard 12-second video (7x markup)
}

// ═══════════════════════════════════════════════════════════════════════════════
// ✅ RECURRING OPERATION TOKEN COSTS v8.1.0
// These are for scheduled/automated AI operations that run weekly/monthly/daily
// These justify the ongoing subscription and provide continuous value
// ═══════════════════════════════════════════════════════════════════════════════

// Weekly Deep Research - Comprehensive business analysis (~GPT-5.5 extended reasoning)
// Actual API cost ~$0.05 = 75 tokens; 20x markup = 1,500 tokens
// This scans competitors, market trends, KPIs, and generates strategic recommendations
if (!defined('VMO_TOKENS_WEEKLY_DEEP_RESEARCH')) {
    define('VMO_TOKENS_WEEKLY_DEEP_RESEARCH', 1500); // Weekly deep business research scan (20x markup)
}

// Weekly Email Research & Automation - Full marketing intelligence
// Actual API cost ~$0.10 = 150 tokens; 20x markup = 3,000 tokens
// Conducts competitive email analysis, generates leads, creates campaigns
if (!defined('VMO_TOKENS_WEEKLY_EMAIL_RESEARCH')) {
    define('VMO_TOKENS_WEEKLY_EMAIL_RESEARCH', 3000); // Weekly email marketing automation (20x markup)
}

// Weekly SEO Audit - Comprehensive SEO health check
// Actual API cost ~$0.08 = 120 tokens; 20x markup = 2,400 tokens
// Analyzes competition, generates strategy updates, identifies content gaps
if (!defined('VMO_TOKENS_WEEKLY_SEO_AUDIT')) {
    define('VMO_TOKENS_WEEKLY_SEO_AUDIT', 2400); // Weekly SEO audit and strategy (20x markup)
}

// Monthly Blog Generation - Full SEO-optimized blog post
// Actual API cost ~$0.05 = 75 tokens; 20x markup = 1,500 tokens
// Creates 1500-2000 word article with topic research
if (!defined('VMO_TOKENS_MONTHLY_BLOG')) {
    define('VMO_TOKENS_MONTHLY_BLOG', 1500); // Monthly automated blog post (20x markup)
}

// Competitor Monitoring - Bi-weekly competitive analysis
// Actual API cost ~$0.05 = 75 tokens; 20x markup = 1,500 tokens
// Deep dive into competitor strategies and market positioning
if (!defined('VMO_TOKENS_COMPETITOR_MONITORING')) {
    define('VMO_TOKENS_COMPETITOR_MONITORING', 1500); // Competitor monitoring scan (20x markup)
}

// Email Strategy Review - Monthly email performance analysis
// Actual API cost ~$0.04 = 60 tokens; 20x markup = 1,200 tokens
// Reviews campaigns, generates optimization recommendations
if (!defined('VMO_TOKENS_EMAIL_STRATEGY_REVIEW')) {
    define('VMO_TOKENS_EMAIL_STRATEGY_REVIEW', 1200); // Monthly email strategy review (20x markup)
}

// Content Embeddings - Daily knowledge base update
// Actual API cost ~$0.01 = 15 tokens; 20x markup = 300 tokens
// Creates embeddings from website content for AI phone assistant
if (!defined('VMO_TOKENS_CONTENT_EMBEDDINGS')) {
    define('VMO_TOKENS_CONTENT_EMBEDDINGS', 300); // Daily content embedding generation (20x markup)
}

// Call Summary - Post-call transcription and analysis
// Whisper transcription + GPT summary generation
// Real cost: ~$0.03 (Whisper + GPT), 10x markup = $0.30 = 450 tokens
if (!defined('VMO_TOKENS_CALL_SUMMARY')) {
    define('VMO_TOKENS_CALL_SUMMARY', 450); // Call transcription + summary (10x markup)
}

// ✅ PHONE SYSTEM TOKEN COSTS
// DEPRECATED: Flat per-minute billing replaced by granular usage-based billing
// in VMO_Twilio_Token_Economics (outbound 53/min, inbound 33/min, TTS 60/1K, GPT 12/turn).
// This constant is retained for backward compatibility only.
if (!defined('VMO_TOKENS_PHONE_PER_MINUTE')) {
    define('VMO_TOKENS_PHONE_PER_MINUTE', 300); // Legacy — see class-vmo-twilio-token-economics.php
}

// Granular phone token costs (override in wp-config.php if needed)
if (!defined('VMO_TOKENS_PHONE_TTS_1K')) {
    define('VMO_TOKENS_PHONE_TTS_1K', 60); // AI TTS per 1K chars — OpenAI $0.015 × 2.67 markup
}
if (!defined('VMO_TOKENS_PHONE_GPT_TURN')) {
    define('VMO_TOKENS_PHONE_GPT_TURN', 12); // AI GPT per conversation turn — OpenAI ~$0.003 × 2.67 markup
}

// Publishing costs based on user type
// Note: No free users can publish — they must purchase a plan first.
if (!defined('VMO_TOKENS_PUBLISH_FREE')) {
    define('VMO_TOKENS_PUBLISH_FREE', 625); // Legacy constant — free users cannot publish (0 tokens)
}
if (!defined('VMO_TOKENS_PUBLISH_SUBSCRIBER')) {
    define('VMO_TOKENS_PUBLISH_SUBSCRIBER', 500); // Subscribers
}
if (!defined('VMO_TOKENS_PUBLISH_TOPUP')) {
    define('VMO_TOKENS_PUBLISH_TOPUP', 550); // Top-up users
}

// Updated plan allocations

/**
 * ✅ HELPER: Build OpenAI API payload with correct token parameter
 * GPT-5.x and o-series models require 'max_completion_tokens', older models use 'max_tokens'
 * 
 * @param string $model The model name
 * @param int $tokens The maximum tokens for completion
 * @return array The token parameter array to merge with payload
 */
function vmo_get_token_param($model, $tokens) {
    // GPT-5.x and o-series models (o1, o3) require max_completion_tokens
    if (preg_match('/^(gpt-5|o1|o3)/i', $model)) {
        return array('max_completion_tokens' => $tokens);
    }
    // Older models use max_tokens; GPT-5.x and o-series use max_completion_tokens
    return array('max_tokens' => $tokens);
}

// --- Load Documents handler (registers AJAX endpoints for the Documents tab) with file validation
$documents_handler_path = VMO_AI_BUILDER_DIR . 'includes/documents-handler.php';
if (file_exists($documents_handler_path) && is_readable($documents_handler_path)) {
    try {
        require_once $documents_handler_path;
    } catch (Exception $e) {
        if (defined('WP_DEBUG') && WP_DEBUG) {
            error_log('VMO: Failed to load documents-handler.php: ' . $e->getMessage());
        }
    }
} else {
    if (defined('WP_DEBUG') && WP_DEBUG) {
        error_log('VMO: documents-handler.php missing or unreadable: ' . $documents_handler_path);
    }
}
// ✅ NO FREE TOKENS — All users must purchase a plan before using any features.
// This constant is kept at 0 for backward compatibility with code that references it.
if (!defined('VMO_TOKENS_FREE_TRIAL')) {
    define('VMO_TOKENS_FREE_TRIAL', 0); // No free tokens — users must purchase a plan
}

// ✅ FINAL PRICING: Sustainable Business Model (23% Net Margin)
// 25% Direct Costs (API/Tokens) + 40% Operating Overhead + 26.75% Tax on Profit = 23% Net Profit
// Includes: Servers, Salaries, Marketing, R&D (Custom AI), Legal, Insurance
// See: BUSINESS-FINANCIAL-MODEL.md for complete cash flow breakdown
if (!defined('VMO_TOKENS_TESTER_PACKAGE')) {
    define('VMO_TOKENS_TESTER_PACKAGE', 36034); // $49.99 - Covers full onboarding + testing (108 min phone OR 127 pages)
}
if (!defined('VMO_TOKENS_STARTER_PLAN')) {
    define('VMO_TOKENS_STARTER_PLAN', 72326); // $99.99/month - 217 min phone max OR 255 pages max
}
if (!defined('VMO_TOKENS_PRO_PLAN')) {
    define('VMO_TOKENS_PRO_PLAN', 144909); // $199.99/month - 435 min phone max OR 512 pages max
}
if (!defined('VMO_TOKENS_ELITE_PLAN')) {
    define('VMO_TOKENS_ELITE_PLAN', 362659); // $499.99/month - 1,089 min phone max OR 1,281 pages max
}
// Legacy compatibility
if (!defined('VMO_TOKENS_BUSINESS_PLAN')) {
    define('VMO_TOKENS_BUSINESS_PLAN', 362659); // Same as Elite
}
if (!defined('VMO_TOKENS_ENTERPRISE_PLAN')) {
    define('VMO_TOKENS_ENTERPRISE_PLAN', 500000); // Custom enterprise tier
}

// Phone activation is billed per Token Economics (3,750 tokens = $2.50).
// Set to true in wp-config.php ONLY for promotional periods.
if (!defined('VMO_PHONE_ACTIVATION_FREE')) {
    define('VMO_PHONE_ACTIVATION_FREE', false);
}

// === Table Name Constants – Set after plugins_loaded (to ensure $wpdb is ready) ===
add_action('plugins_loaded', function () {
    global $wpdb;
    if (!defined('VMO_PROJECTS_TABLE')) {
        define('VMO_PROJECTS_TABLE', $wpdb->prefix . 'vmo_projects');
    }
    if (!defined('VMO_HISTORY_TABLE')) {
        define('VMO_HISTORY_TABLE', $wpdb->prefix . 'vmo_project_history');
    }
});

/**
 * Determine if the current user has a published live page generated by VMO.
 * We consider a site "published" when at least one vmo_project has a non-empty
 * vmo_live_page_id meta that points to a WordPress page with status 'publish'.
 */
function vmo_user_has_published_site($user_id) {
    $user_id = (int) $user_id;
    if ($user_id <= 0) return false;

    $projects = get_posts([
        'post_type'      => 'vmo_project',
        'author'         => $user_id,
        'posts_per_page' => 5,
        'fields'         => 'ids',
        'meta_query'     => [
            [
                'key'     => 'vmo_live_page_id',
                'compare' => 'EXISTS',
            ],
        ],
    ]);

    if (empty($projects)) return false;

    foreach ($projects as $project_id) {
        $live_id = get_post_meta($project_id, 'vmo_live_page_id', true);
        if ($live_id) {
            $status = get_post_status((int) $live_id);
            if ($status === 'publish') return true;
        }
    }
    return false;
}

/**
 * ✅ Get or create an activation record for the user
 * This ensures every user has an install_id for email/token operations
 */
function vmo_get_or_create_user_activation($user_id = null) {
    if (!$user_id) {
        $user_id = get_current_user_id();
    }
    if (!$user_id) {
        return null;
    }
    
    global $wpdb;
    $table_name = $wpdb->prefix . 'vmo_manager_activations';
    
    // Check if table exists, create it if not
    $table_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table_name));
    if (!$table_exists) {
        // Table doesn't exist - trigger table creation
        vmo_create_plugin_tables();
        $table_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table_name));
        if (!$table_exists) {
            if (defined('WP_DEBUG') && WP_DEBUG) error_log('VMO: Failed to create manager_activations table');
            return null;
        }
    }
    
    // Check if user has an activation
    $activation = $wpdb->get_row($wpdb->prepare(
        "SELECT * FROM {$table_name} WHERE user_id = %d LIMIT 1",
        $user_id
    ));
    
    if ($activation) {
        return $activation;
    }
    
    // Create a new activation for the user
    $install_id = 'vmo_' . wp_generate_uuid4();
    $site_url = get_site_url();
    
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    $inserted = $wpdb->insert($table_name, [
        'install_id' => $install_id,
        'user_id' => $user_id,
        'site_url' => $site_url,
        'plan_type' => 'free',
        'token_balance' => 0, // No free tokens — user must purchase a plan
        'tokens_used' => 0,
        'status' => 'active',
        'activated_at' => current_time('mysql'),
        'created_at' => current_time('mysql'),
        'updated_at' => current_time('mysql')
    ]);
    
    if (!$inserted) {
        if (defined('WP_DEBUG') && WP_DEBUG) error_log('VMO: Failed to create activation for user ' . $user_id);
        return null;
    }
    
    if (defined('WP_DEBUG') && WP_DEBUG) error_log('VMO: Created new activation for user ' . $user_id . ' with install_id ' . $install_id);
    
    return $wpdb->get_row($wpdb->prepare(
        "SELECT * FROM {$table_name} WHERE user_id = %d LIMIT 1",
        $user_id
    ));
}

/**
 * ✅ Get install_id for current user (creates activation if needed)
 */
function vmo_get_user_install_id($user_id = null) {
    $activation = vmo_get_or_create_user_activation($user_id);
    return $activation ? $activation->install_id : null;
}

add_filter('cron_schedules', function($schedules) {
    if (!isset($schedules['vmo_every_five_minutes'])) {
        $schedules['vmo_every_five_minutes'] = [
            'interval' => 5 * MINUTE_IN_SECONDS,
            'display'  => __('Every Five Minutes (VMO)', 'vmo-ai-website-builder'),
        ];
    }
    return $schedules;
});

add_action('init', function() {
    if (!wp_next_scheduled('vmo_process_scheduled_emails')) {
        wp_schedule_event(time() + 300, 'vmo_every_five_minutes', 'vmo_process_scheduled_emails');
    }
});

// === Activation Hook to Create Tables ===
register_activation_hook(__FILE__, 'vmo_activate_plugin');
register_deactivation_hook(__FILE__, 'vmo_deactivate_plugin');

// ✅ Ensure critical tables exist on every admin page load (not just activation)
add_action('admin_init', 'vmo_ensure_critical_tables_exist');

// ✅ CLIENT MODE: Auto-register with the master server on first admin load
// When the plugin runs in VMO_CLIENT_MODE and has no installation_id yet,
// it calls the master's /register endpoint to get an installation_id and license_key.
add_action('admin_init', 'vmo_client_auto_register_with_master');

function vmo_client_auto_register_with_master() {
    // Only run in client mode
    if (!defined('VMO_CLIENT_MODE') || VMO_CLIENT_MODE !== true) {
        return;
    }

    // Already registered? Skip.
    $installation_id = get_option('vmo_installation_id', '');
    if (!empty($installation_id)) {
        return;
    }

    // Rate limit: don't try more than once per hour
    if (get_transient('vmo_client_register_attempted')) {
        return;
    }
    set_transient('vmo_client_register_attempted', true, HOUR_IN_SECONDS);

    // Determine master URL
    $master_url = defined('VMO_MASTER_PROXY_URL')
        ? VMO_MASTER_PROXY_URL
        : get_option('vmo_master_proxy_url', 'https://getvmo.com/wp-json/vmo-builder/v1/proxy');

    // Build the register URL (vmo-manager/v1/register on the master)
    $register_url = str_replace('/vmo-builder/v1/proxy', '/vmo-manager/v1/register', $master_url);

    $site_url    = home_url();
    $site_domain = wp_parse_url($site_url, PHP_URL_HOST);
    $admin_email = get_option('admin_email', '');

    $response = wp_remote_post($register_url, [
        'timeout' => 15,
        'headers' => ['Content-Type' => 'application/json'],
        'body'    => wp_json_encode([
            'site_url'    => $site_url,
            'site_email'  => $admin_email,
            'site_domain' => $site_domain,
            'user_email'  => $admin_email,
        ]),
    ]);

    if (is_wp_error($response)) {
        if (defined('WP_DEBUG') && WP_DEBUG) {
            error_log('VMO Client: Auto-registration failed — ' . $response->get_error_message());
        }
        return;
    }

    $body = json_decode(wp_remote_retrieve_body($response), true);
    $code = wp_remote_retrieve_response_code($response);

    if ($code === 200 && !empty($body['success']) && !empty($body['installation_id'])) {
        update_option('vmo_installation_id', sanitize_text_field($body['installation_id']));

        if (!empty($body['license_key'])) {
            update_option('vmo_license_key', sanitize_text_field($body['license_key']));
        }

        // Clear the rate-limit transient so it doesn't block future valid checks
        delete_transient('vmo_client_register_attempted');

        if (defined('WP_DEBUG') && WP_DEBUG) {
            error_log('✅ VMO Client: Registered with master — install_id=' . $body['installation_id']);
        }
    } else {
        if (defined('WP_DEBUG') && WP_DEBUG) {
            error_log('VMO Client: Registration response code=' . $code . ' body=' . wp_json_encode($body));
        }
    }
}

// ✅ CLIENT MODE: Periodic subscription sync from master server
// Runs once every 6 hours to keep local plan/tokens in sync with the master.
// Without this, plan upgrades/downgrades/renewals wouldn't reflect locally.
if (!function_exists('vmo_quicklaunch_normalize_hosted_plan_key')) {
    function vmo_quicklaunch_normalize_hosted_plan_key($plan) {
        $plan = strtolower(trim((string) $plan));
        if ($plan === '') {
            return '';
        }

        if (function_exists('vmo_normalize_service_plan_key')) {
            $plan = strtolower(trim((string) vmo_normalize_service_plan_key($plan)));
        } elseif (function_exists('vmo_normalize_plan_key')) {
            $plan = strtolower(trim((string) vmo_normalize_plan_key($plan)));
        }

        $aliases = array(
            'tester_pack' => 'tester',
            'starter_pack' => 'starter',
            'pro_pack' => 'pro',
            'professional' => 'pro',
            'premium' => 'pro',
            'business' => 'elite',
            'business_pack' => 'elite',
            'enterprise_pack' => 'enterprise',
        );
        $plan = $aliases[$plan] ?? $plan;
        return in_array($plan, array('tester', 'starter', 'pro', 'elite', 'enterprise'), true) ? $plan : '';
    }
}

if (!function_exists('vmo_quicklaunch_hosted_entitlement_repair_is_allowed')) {
    function vmo_quicklaunch_hosted_entitlement_repair_is_allowed($user_id) {
        $user_id = (int) $user_id;
        if ($user_id <= 0) {
            return false;
        }

        $blocking = function_exists('vmo_service_lock_blocking_statuses')
            ? vmo_service_lock_blocking_statuses()
            : array('payment_failed', 'past_due', 'unpaid', 'suspended', 'canceled', 'cancelled', 'paused', 'incomplete', 'incomplete_expired', 'requires_payment_method');
        $payment_status = sanitize_key((string) get_user_meta($user_id, 'vmo_subscription_payment_status', true));
        $subscription_status = sanitize_key((string) get_user_meta($user_id, 'vmo_subscription_status', true));
        $lock_status = strtolower((string) get_user_meta($user_id, 'vmo_service_lock_status', true));
        $lock_reason = sanitize_key((string) get_user_meta($user_id, 'vmo_service_lock_reason', true));
        $recoverable_reasons = array('', 'billing_hold', 'subscription_required', 'no_plan', 'missing_plan', 'master_subscription_sync');

        if (in_array($payment_status, $blocking, true) || in_array($subscription_status, $blocking, true)) {
            return false;
        }

        return $lock_status !== 'locked' || in_array($lock_reason, $recoverable_reasons, true);
    }
}

if (!function_exists('vmo_quicklaunch_hosted_plan_for_user')) {
    function vmo_quicklaunch_hosted_plan_for_user($user_id, $fallback = '') {
        $user_id = (int) $user_id;
        $candidates = array(
            $fallback,
            $user_id > 0 ? get_user_meta($user_id, 'vmo_quicklaunch_selected_plan', true) : '',
            get_option('vmo_quicklaunch_selected_plan', ''),
            $user_id > 0 ? get_user_meta($user_id, 'vmo_last_active_subscription_plan', true) : '',
            $user_id > 0 ? get_user_meta($user_id, 'vmo_service_lock_plan', true) : '',
            $user_id > 0 ? get_user_meta($user_id, 'vmo_subscription_status', true) : '',
        );

        foreach ($candidates as $candidate) {
            $plan = vmo_quicklaunch_normalize_hosted_plan_key($candidate);
            if ($plan !== '') {
                return $plan;
            }
        }

        return '';
    }
}

if (!function_exists('vmo_quicklaunch_hosted_token_floor_for_user')) {
    function vmo_quicklaunch_hosted_token_floor_for_user($user_id, $plan = '') {
        $user_id = (int) $user_id;
        if ($user_id <= 0) {
            return 0;
        }

        $candidates = array(
            get_user_meta($user_id, 'vmo_service_pre_lock_token_balance', true),
            get_user_meta($user_id, 'vmo_quicklaunch_available_tokens', true),
            get_user_meta($user_id, 'vmo_month_one_token_wallet', true),
            get_user_meta($user_id, 'vmo_plan_recurring_tokens', true),
            get_user_meta($user_id, 'vmo_quicklaunch_launch_credit_tokens', true),
        );

        if (function_exists('vmo_get_user_plan_credit_allocation')) {
            $candidates[] = vmo_get_user_plan_credit_allocation($user_id);
        } elseif (class_exists('VMO_Plan_Policy') && $plan !== '') {
            $plan_policy = VMO_Plan_Policy::get_plan($plan);
            $candidates[] = (int) ($plan_policy['recurring_tokens'] ?? 0);
        }

        $floor = 0;
        foreach ($candidates as $candidate) {
            $floor = max($floor, (int) $candidate);
        }
        return $floor;
    }
}

if (!function_exists('vmo_quicklaunch_repair_hosted_client_entitlement')) {
    function vmo_quicklaunch_repair_hosted_client_entitlement($user_id = 0, $context = array()) {
        if (!function_exists('vmo_quicklaunch_is_hosted_client_context') || !vmo_quicklaunch_is_hosted_client_context()) {
            return false;
        }
        $context = is_array($context) ? $context : array();

        $user_id = (int) $user_id;
        if ($user_id <= 0) {
            $user_id = get_current_user_id();
        }
        if ($user_id <= 0 && function_exists('get_users')) {
            $admins = get_users(array('role' => 'administrator', 'number' => 1, 'fields' => 'ID'));
            $user_id = !empty($admins) ? (int) $admins[0] : 0;
        }
        if ($user_id <= 0 || !vmo_quicklaunch_hosted_entitlement_repair_is_allowed($user_id)) {
            return false;
        }

        $plan = vmo_quicklaunch_hosted_plan_for_user($user_id, $context['plan'] ?? '');
        if ($plan === '') {
            return false;
        }

        $license_key = (string) get_user_meta($user_id, 'vmo_license_key', true);
        if ($license_key === '') {
            $license_key = (string) get_option('vmo_license_key', '');
            if ($license_key !== '') {
                update_user_meta($user_id, 'vmo_license_key', sanitize_text_field($license_key));
            }
        }

        update_user_meta($user_id, 'vmo_subscription_status', $plan);
        update_user_meta($user_id, 'vmo_last_active_subscription_plan', $plan);
        update_user_meta($user_id, 'vmo_quicklaunch_selected_plan', $plan);

        $payment_status = sanitize_key((string) get_user_meta($user_id, 'vmo_subscription_payment_status', true));
        if ($payment_status === '' || in_array($payment_status, array('subscription_required', 'billing_hold', 'no_plan'), true)) {
            update_user_meta($user_id, 'vmo_subscription_payment_status', 'paid');
        }

        $token_balance = (int) get_user_meta($user_id, 'vmo_token_balance', true);
        $token_floor = vmo_quicklaunch_hosted_token_floor_for_user($user_id, $plan);
        $lock_reason = sanitize_key((string) get_user_meta($user_id, 'vmo_service_lock_reason', true));
        if ($token_balance <= 0 && $token_floor > 0 && in_array($lock_reason, array('', 'subscription_required', 'billing_hold', 'no_plan', 'master_subscription_sync'), true)) {
            update_user_meta($user_id, 'vmo_token_balance', $token_floor);
            update_user_meta($user_id, 'vmo_quicklaunch_available_tokens', $token_floor);
            $token_balance = $token_floor;
        }

        update_user_meta($user_id, 'vmo_master_entitlement_active', '1');
        update_user_meta($user_id, 'vmo_master_has_selected_plan', '1');
        update_user_meta($user_id, 'vmo_master_can_use_cost_bearing_services', $token_balance > 0 ? '1' : '0');
        update_user_meta($user_id, 'vmo_master_no_credit', $token_balance <= 0 ? '1' : '0');
        update_user_meta($user_id, 'vmo_quicklaunch_entitlement_repaired_at', current_time('mysql'));

        if (function_exists('vmo_unlock_user_services')) {
            vmo_unlock_user_services($user_id, array(
                'reason' => $context['reason'] ?? 'quicklaunch_hosted_client_repair',
                'payment_status' => 'paid',
                'plan' => $plan,
                'token_balance' => $token_balance,
            ));
        } else {
            update_user_meta($user_id, 'vmo_service_lock_status', 'unlocked');
            delete_user_meta($user_id, 'vmo_service_lock_reason');
            delete_user_meta($user_id, 'vmo_service_lock_context');
            update_user_meta($user_id, 'vmo_services_suspended', 0);
            update_user_meta($user_id, 'vmo_service_access_enabled', 1);
        }

        return true;
    }
}

add_action('admin_init', function() {
    vmo_quicklaunch_repair_hosted_client_entitlement(0, array('reason' => 'hosted_client_admin_init'));
}, 1);

add_action('admin_init', 'vmo_client_sync_subscription_from_master');

function vmo_client_sync_subscription_from_master() {
    if (!defined('VMO_CLIENT_MODE') || VMO_CLIENT_MODE !== true) {
        return;
    }

    $license_key     = get_option('vmo_license_key', '');
    $installation_id = get_option('vmo_installation_id', '');

    if (empty($license_key) || empty($installation_id)) {
        return;
    }

    if (function_exists('vmo_quicklaunch_repair_hosted_client_entitlement')) {
        vmo_quicklaunch_repair_hosted_client_entitlement(0, array('reason' => 'pre_master_subscription_sync'));
    }

    // Only sync every 6 hours
    $last_sync = get_transient('vmo_client_last_subscription_sync');
    if ($last_sync) {
        return;
    }

    $master_url = defined('VMO_MASTER_PROXY_URL')
        ? VMO_MASTER_PROXY_URL
        : get_option('vmo_master_proxy_url', 'https://getvmo.com/wp-json/vmo-builder/v1/proxy');

    // Build the subscription status URL on the master
    $status_url = str_replace('/vmo-builder/v1/proxy', '/vmo-builder/v1/subscription/status', $master_url);

    $response = wp_remote_get($status_url, [
        'timeout' => 10,
        'headers' => [
            'X-VMO-License'      => $license_key,
            'X-VMO-Installation' => $installation_id,
        ],
    ]);

    if (is_wp_error($response)) {
        delete_transient('vmo_client_last_subscription_sync');
        return;
    }

    $body = json_decode(wp_remote_retrieve_body($response), true);
    $code = wp_remote_retrieve_response_code($response);

    if ($code === 200 && !empty($body['success']) && !empty($body['subscription'])) {
        $user_id = get_current_user_id();
        if ($user_id) {
            $sub = $body['subscription'];
            $is_hosted_client = function_exists('vmo_quicklaunch_is_hosted_client_context') && vmo_quicklaunch_is_hosted_client_context();
            $master_plan_raw = !empty($sub['plan']) ? sanitize_key((string) $sub['plan']) : '';
            $master_plan = $is_hosted_client ? vmo_quicklaunch_normalize_hosted_plan_key($master_plan_raw) : $master_plan_raw;
            $local_hosted_plan = ($is_hosted_client && function_exists('vmo_quicklaunch_hosted_plan_for_user')) ? vmo_quicklaunch_hosted_plan_for_user($user_id) : '';
            $master_reason = sanitize_key((string) ($sub['service_lock_reason'] ?? ''));
            $master_subscription_required = !empty($sub['service_locked']) && in_array($master_reason, array('', 'subscription_required', 'billing_hold'), true);
            if ($master_plan !== '') {
                update_user_meta($user_id, 'vmo_subscription_status', $master_plan);
            } elseif ($local_hosted_plan !== '' && $master_subscription_required && function_exists('vmo_quicklaunch_repair_hosted_client_entitlement')) {
                vmo_quicklaunch_repair_hosted_client_entitlement($user_id, array('reason' => 'master_subscription_sync_missing_plan', 'plan' => $local_hosted_plan));
            }
            if (isset($sub['tokens'])) {
                update_user_meta($user_id, 'vmo_token_balance', (int) $sub['tokens']);
            }
            foreach (array('entitlement_active', 'has_selected_plan', 'free_access_active', 'free_access_expired', 'critical_credit', 'low_credit', 'no_credit', 'can_use_cost_bearing_services') as $bool_key) {
                if (array_key_exists($bool_key, $sub)) {
                    update_user_meta($user_id, 'vmo_master_' . $bool_key, !empty($sub[$bool_key]) ? '1' : '0');
                }
            }
            if (array_key_exists('credit_warning_level', $sub)) {
                update_user_meta($user_id, 'vmo_master_credit_warning_level', sanitize_key((string) $sub['credit_warning_level']));
            }
            if (array_key_exists('service_locked', $sub)) {
                if (!empty($sub['service_locked']) && !($local_hosted_plan !== '' && $master_subscription_required)) {
                    if (function_exists('vmo_lock_user_services')) {
                        vmo_lock_user_services($user_id, sanitize_key((string) ($sub['service_lock_reason'] ?? 'billing_hold')), array(
                            'source' => 'master_subscription_sync',
                            'payment_status' => sanitize_key((string) ($sub['payment_status'] ?? '')),
                            'plan' => sanitize_key((string) ($sub['recoverable_plan'] ?? '')),
                        ));
                    } else {
                        update_user_meta($user_id, 'vmo_service_lock_status', 'locked');
                    }
                } elseif (function_exists('vmo_unlock_user_services')) {
                    vmo_unlock_user_services($user_id, array(
                        'reason' => 'master_subscription_sync',
                        'payment_status' => sanitize_key((string) ($sub['payment_status'] ?? 'paid')),
                        'plan' => sanitize_key((string) ($sub['plan'] ?? '')),
                    ));
                }
            }
            if (!empty($sub['pricing_mode'])) {
                $pricing_mode = sanitize_key($sub['pricing_mode']);
                update_option('vmo_client_distribution_channel', $pricing_mode, false);
                update_option('vmo_quicklaunch_hosted_client', $pricing_mode === 'quicklaunch_hosted' ? '1' : '0', false);
            }
        }
        set_transient('vmo_client_last_subscription_sync', time(), 6 * HOUR_IN_SECONDS);
    }
}

// ✅ One-time migration: auto-enable call recording for all existing users
// Recording previously defaulted to OFF, which meant no calls were ever recorded.
// This flips it ON for every user who has it at 0 or unset.
add_action('admin_init', function() {
    if (get_option('vmo_recording_default_on_v1')) return;
    $users = get_users(array('fields' => 'ID'));
    foreach ($users as $uid) {
        update_user_meta($uid, 'call_recording_enabled', 1);
    }
    update_option('vmo_recording_default_on_v1', time(), true);
}, 4);

// ✅ One-time migration: ensure vmo_contacts has full_name, first_name, last_name columns
add_action('admin_init', function() {
    if (get_option('vmo_contacts_name_cols_migrated')) return;
    global $wpdb;
    $contacts_table = $wpdb->prefix . 'vmo_contacts';
    $table_exists = vmo_table_exists($contacts_table);
    if (!$table_exists) return; // table doesn't exist yet, activation hook will create it properly
    $suppress = $wpdb->suppress_errors(true);
    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
    $cols = $wpdb->get_col("SHOW COLUMNS FROM {$contacts_table}", 0);
    if (is_array($cols)) {
        if (!in_array('full_name', $cols, true)) {
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
            $wpdb->query("ALTER TABLE {$contacts_table} ADD COLUMN full_name VARCHAR(200) DEFAULT NULL AFTER last_name");
        }
        if (!in_array('first_name', $cols, true)) {
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
            $wpdb->query("ALTER TABLE {$contacts_table} ADD COLUMN first_name VARCHAR(100) DEFAULT NULL AFTER lifecycle_stage");
        }
        if (!in_array('last_name', $cols, true)) {
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
            $wpdb->query("ALTER TABLE {$contacts_table} ADD COLUMN last_name VARCHAR(100) DEFAULT NULL AFTER first_name");
        }
    }
    $wpdb->suppress_errors($suppress);
    update_option('vmo_contacts_name_cols_migrated', 1, true);
}, 5); // priority 5 = runs before most other admin_init hooks

// ===========================================
// VMO SIGN UP BONUS HANDLER
// Checks on login for deferred bonuses from webhook.
// These are post-purchase bonuses ONLY — given after a user
// buys a plan, to help cover the heavy Month 1 setup costs
// (consultation, generations, background ops, calls, emails).
// ===========================================
add_action('admin_init', 'vmo_process_pending_signup_bonus');
function vmo_process_pending_signup_bonus() {
    $user_id = get_current_user_id();
    if (!$user_id) return;
    
    // Check for deferred bonus set by webhook
    $bonus_tokens = (int) get_user_meta($user_id, 'vmo_unclaimed_signup_bonus', true);
    
    if ($bonus_tokens > 0) {
        $bonus_value = get_user_meta($user_id, 'vmo_unclaimed_signup_bonus_value', true) ?: '$0';
        $plan_name = get_user_meta($user_id, 'vmo_unclaimed_signup_bonus_plan', true) ?: 'Premium';
        
        // 1. Award Tokens
        if (function_exists('vmo_update_token_balance')) {
            vmo_update_token_balance($user_id, $bonus_tokens, 'add');
        } else {
            $current = (int) get_user_meta($user_id, 'vmo_token_balance', true);
            update_user_meta($user_id, 'vmo_token_balance', $current + $bonus_tokens);
        }
        
        // 2. Queue the Notification
        update_user_meta($user_id, 'vmo_pending_notification', wp_json_encode([
            'type' => 'welcome_bonus',
            'title' => '\ud83c\udf89 Sign Up Bonus!',
            'message' => "Thank you for choosing the {$plan_name} Plan! We've added {$bonus_value} in bonus credits to your account to help you get through your initial setup — consultation, website builds, phone calls, and more. Let's start building!",
            'amount' => $bonus_value,
            'created' => current_time('mysql')
        ]));
        
        // 3. Clean up
        delete_user_meta($user_id, 'vmo_unclaimed_signup_bonus');
        delete_user_meta($user_id, 'vmo_unclaimed_signup_bonus_value');
        delete_user_meta($user_id, 'vmo_unclaimed_signup_bonus_plan');
        
        // 4. Log
        if (defined('WP_DEBUG') && WP_DEBUG) error_log("VMO Sign Up Bonus Claimed: User {$user_id} logged in and received {$bonus_tokens} tokens.");
    }
}
function vmo_ensure_critical_tables_exist() {
    // Only run once per day to avoid performance impact
    $last_check = get_option('vmo_tables_last_check', 0);
    if (time() - $last_check < DAY_IN_SECONDS) {
        // But always check if the critical table is missing
        global $wpdb;
        $activations_table = $wpdb->prefix . 'vmo_manager_activations';
        $table_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $activations_table));
        if ($table_exists) {
            return; // Table exists, no need to run
        }
    }
    
    // Run table creation (buffer output to prevent HTML leaking into AJAX responses)
    ob_start();
    try {
        vmo_create_plugin_tables();
        if (function_exists('vmo_ensure_email_queue_table_schema')) {
            vmo_ensure_email_queue_table_schema();
        }
    } catch (Exception $e) {
        if (defined('WP_DEBUG') && WP_DEBUG) error_log('VMO: Table creation error: ' . $e->getMessage());
    }
    ob_end_clean();
    update_option('vmo_tables_last_check', time());
    if (defined('WP_DEBUG') && WP_DEBUG) error_log('VMO: Critical tables check completed');
}

function vmo_activate_plugin() {
    // Buffer ALL output during activation to prevent "unexpected output" errors.
    // dbDelta() and ALTER TABLE can emit notices/warnings that WordPress treats as errors.
    ob_start();

    try {
        vmo_create_plugin_tables();
    } catch (Exception $e) {
        if (defined('WP_DEBUG') && WP_DEBUG) error_log('VMO Activation Error: ' . $e->getMessage());
    }

    // Discard any output that leaked (dbDelta notices, SQL warnings, etc.)
    ob_end_clean();
    
    // Create abandoned cart table
    if (class_exists('VMO_Abandoned_Cart')) {
        VMO_Abandoned_Cart::create_table();
    }

    // Schedule monthly reports if not already scheduled
    if (!wp_next_scheduled('vmo_send_monthly_reports')) {
        wp_schedule_event(time(), 'monthly', 'vmo_send_monthly_reports');
    }

    // Schedule daily progress snapshots
    if (!wp_next_scheduled('vmo_daily_progress_snapshot')) {
        // Schedule for next 02:30 AM site time
        $timestamp = strtotime('tomorrow 02:30');
        if ($timestamp === false) { $timestamp = time() + DAY_IN_SECONDS; }
        wp_schedule_event($timestamp, 'daily', 'vmo_daily_progress_snapshot');
    }

    // Schedule session cleanup (removes stale consultation sessions daily)
    if (!wp_next_scheduled('vmo_cleanup_expired_sessions')) {
        wp_schedule_event(time() + HOUR_IN_SECONDS, 'daily', 'vmo_cleanup_expired_sessions');
    }

    // Schedule email queue processing (hourly)
    if (!wp_next_scheduled('vmo_process_email_queue')) {
        wp_schedule_event(time() + 300, 'hourly', 'vmo_process_email_queue');
    }

    // Schedule inbound mailbox sync so Mailcow/Zoho/user IMAP inboxes enter VMO Email.
    if (!wp_next_scheduled('vmo_sync_inbound_mailboxes')) {
        wp_schedule_event(time() + 300, 'hourly', 'vmo_sync_inbound_mailboxes');
    }

    // Schedule background auto-reply processing for mail imported outside dashboard visits.
    if (!wp_next_scheduled('vmo_process_auto_replies_cron')) {
        wp_schedule_event(time() + 360, 'vmo_every_five_minutes', 'vmo_process_auto_replies_cron');
    }

    // Schedule operational email sends more frequently for user-selected send times.
    if (!wp_next_scheduled('vmo_process_scheduled_emails')) {
        wp_schedule_event(time() + 300, 'vmo_every_five_minutes', 'vmo_process_scheduled_emails');
    }

    // Schedule Stripe subscription sync (hourly fallback for missed webhooks)
    if (!wp_next_scheduled('vmo_sync_stripe_subscriptions')) {
        wp_schedule_event(time() + 600, 'hourly', 'vmo_sync_stripe_subscriptions');
    }

    // Schedule hourly A2P status polling so SMS users still get updates if a webhook is delayed.
    if (!wp_next_scheduled('vmo_a2p_status_poll')) {
        wp_schedule_event(time() + 900, 'hourly', 'vmo_a2p_status_poll');
    }

    // Schedule weekly analytics report
    if (!wp_next_scheduled('vmo_weekly_analytics_report')) {
        wp_schedule_event(time() + DAY_IN_SECONDS, 'weekly', 'vmo_weekly_analytics_report');
    }

    // Schedule weekly SEO report
    if (!wp_next_scheduled('vmo_weekly_seo_report')) {
        wp_schedule_event(time() + DAY_IN_SECONDS, 'weekly', 'vmo_weekly_seo_report');
    }

    // Schedule daily SEO check
    if (!wp_next_scheduled('vmo_daily_seo_check')) {
        wp_schedule_event(strtotime('tomorrow 04:00') ?: time() + DAY_IN_SECONDS, 'daily', 'vmo_daily_seo_check');
    }

    // Schedule daily competitor check
    if (!wp_next_scheduled('vmo_daily_competitor_check')) {
        wp_schedule_event(strtotime('tomorrow 05:00') ?: time() + DAY_IN_SECONDS, 'daily', 'vmo_daily_competitor_check');
    }

    // ── Guardrail G3: Daily hosting bandwidth usage monitor ──
    if (!wp_next_scheduled('vmo_daily_hosting_usage_check')) {
        wp_schedule_event(strtotime('tomorrow 06:00') ?: time() + DAY_IN_SECONDS, 'daily', 'vmo_daily_hosting_usage_check');
    }

    // ──────────────────────────────────────────────────────────────
    // Auto-create the Stripe promo coupon for testers/reviewers
    // (runs only once; requires Stripe secret key to be configured)
    // NOTE: The /get-started/ page with [vmo_quicklaunch] lives on
    // the VMO marketing website (getvmo.com), NOT auto-created here.
    // ──────────────────────────────────────────────────────────────
    vmo_ensure_stripe_promo_coupon();
}

/**
 * Ensure the VMO reviewer-access Stripe promo coupon exists.
 * Creates a first-invoice access coupon + human-readable promotion code.
 * Idempotent — skips if already created or Stripe key not configured.
 */
function vmo_ensure_stripe_promo_coupon() {
    // Already created?
    if (get_option('vmo_stripe_promo_coupon_created')) {
        return;
    }

    $stripe_secret = get_option('vmo_stripe_secret_key', '');
    if (empty($stripe_secret)) {
        return; // No Stripe key yet — will retry on admin_init
    }

    $coupon_id   = 'vmo-elite-review-access';
    $promo_code  = 'VMO-ELITE-REVIEW';
    $headers     = [
        'Authorization' => 'Bearer ' . $stripe_secret,
        'Content-Type'  => 'application/x-www-form-urlencoded',
    ];

    // Step 1: Create the reviewer-access coupon
    $coupon_response = wp_remote_post('https://api.stripe.com/v1/coupons', [
        'timeout' => 15,
        'headers' => $headers,
        'body'    => [
            'id'          => $coupon_id,
            'percent_off' => 100,
            'duration'    => 'once',
            'name'        => 'VMO Elite Reviewer Access',
            'max_redemptions' => 100,      // Limit total uses
        ],
    ]);

    $coupon_body = json_decode(wp_remote_retrieve_body($coupon_response), true);

    // If coupon already exists in Stripe, that's fine — proceed to promo code
    if (isset($coupon_body['error']) && ($coupon_body['error']['code'] ?? '') !== 'resource_already_exists') {
        if (defined('WP_DEBUG') && WP_DEBUG) {
            error_log('VMO: Stripe coupon creation failed — ' . ($coupon_body['error']['message'] ?? 'unknown'));
        }
        // Don't mark as created so it retries
        return;
    }

    // Step 2: Create the human-readable promotion code
    $promo_response = wp_remote_post('https://api.stripe.com/v1/promotion_codes', [
        'timeout' => 15,
        'headers' => $headers,
        'body'    => [
            'coupon' => $coupon_id,
            'code'   => $promo_code,
            'active' => 'true',
            'metadata[purpose]'   => 'Tester/reviewer promotional access',
            'metadata[created_by]' => 'vmo-plugin-auto',
        ],
    ]);

    $promo_body = json_decode(wp_remote_retrieve_body($promo_response), true);

    // Mark as created (even if promo code already exists)
    if (!empty($promo_body['id']) || (($promo_body['error']['code'] ?? '') === 'resource_already_exists')) {
        update_option('vmo_stripe_promo_coupon_created', true);
        update_option('vmo_stripe_promo_code', $promo_code);
        if (defined('WP_DEBUG') && WP_DEBUG) {
            error_log('VMO: Stripe promo coupon ' . $promo_code . ' ready');
        }
    } else {
        if (defined('WP_DEBUG') && WP_DEBUG) {
            error_log('VMO: Stripe promo code creation failed — ' . ($promo_body['error']['message'] ?? 'unknown'));
        }
    }
}

function vmo_deactivate_plugin() {
    // Clear all scheduled events
    wp_clear_scheduled_hook('vmo_send_monthly_reports');
    wp_clear_scheduled_hook('vmo_daily_progress_snapshot');
    wp_clear_scheduled_hook('vmo_cleanup_expired_sessions');
    wp_clear_scheduled_hook('vmo_process_email_queue');
    wp_clear_scheduled_hook('vmo_process_scheduled_emails');
    wp_clear_scheduled_hook('vmo_sync_inbound_mailboxes');
    wp_clear_scheduled_hook('vmo_process_auto_replies_cron');
    wp_clear_scheduled_hook('vmo_sync_stripe_subscriptions');
    wp_clear_scheduled_hook('vmo_a2p_status_poll');
    wp_clear_scheduled_hook('vmo_weekly_analytics_report');
    wp_clear_scheduled_hook('vmo_weekly_seo_report');
    wp_clear_scheduled_hook('vmo_daily_seo_check');
    wp_clear_scheduled_hook('vmo_daily_competitor_check');
    wp_clear_scheduled_hook('vmo_quarterly_insights_cron');
    if (class_exists('VMO_Abandoned_Cart')) {
        VMO_Abandoned_Cart::deactivate();
    }
}

// ══════════════════════════════════════════════════════════════════
// admin_init fallback: Ensure Stripe promo coupon exists
// For existing installations that update without re-activating
// ══════════════════════════════════════════════════════════════════
add_action('admin_init', function() {
    if (!current_user_can('manage_options')) return;

    // Only run once per day to avoid performance overhead
    $last_check = get_option('vmo_quicklaunch_setup_checked', 0);
    if (time() - $last_check < DAY_IN_SECONDS) return;

    vmo_ensure_stripe_promo_coupon();

    update_option('vmo_quicklaunch_setup_checked', time());
});

// === Admin Notices for Stripe Configuration ===
add_action('admin_notices', function() {
    // Only show on VMO plugin pages
    $screen = get_current_screen();
    if (!$screen || strpos($screen->id, 'vmo') === false) {
        return;
    }

    $is_hosted_client = (function_exists('vmo_quicklaunch_is_hosted_client_context') && vmo_quicklaunch_is_hosted_client_context())
        || (defined('VMO_CLIENT_MODE') && VMO_CLIENT_MODE === true)
        || get_option('vmo_quicklaunch_hosted_client') === '1'
        || get_option('vmo_client_distribution_channel') === 'quicklaunch_hosted'
        || (defined('VMO_CLIENT_DISTRIBUTION_CHANNEL') && VMO_CLIENT_DISTRIBUTION_CHANNEL === 'quicklaunch_hosted')
        || (get_option('vmo_license_key', '') !== '' && get_option('vmo_quicklaunch_domain', '') !== '');
    if ($is_hosted_client) {
        return;
    }
    
    // Check if user has dismissed this notice
    if (get_user_meta(get_current_user_id(), 'vmo_stripe_notice_dismissed', true)) {
        return;
    }
    
    // Only show to admins
    if (!current_user_can('manage_options')) {
        return;
    }
    
    // Check Stripe configuration status
    $stripe_secret = get_option('vmo_stripe_secret_key', '');
    $stripe_publishable = get_option('vmo_stripe_publishable_key', '');
    $payments_enabled = defined('VMO_PAYMENTS_ENABLED') && VMO_PAYMENTS_ENABLED;
    
    // Show warning if payments enabled but keys not configured
    if ($payments_enabled && (empty($stripe_secret) || empty($stripe_publishable))) {
        ?>
        <div class="notice notice-warning is-dismissible" data-vmo-notice="stripe-config">
            <p>
                <strong>⚠️ VMO Payments Setup Required</strong><br>
                Stripe payments are enabled, but API keys are not configured. 
                <a href="<?php echo admin_url('admin.php?page=vmo-ai-builder'); ?>">Configure Stripe Keys →</a>
            </p>
        </div>
        <?php
    }
    
    // Show success if everything is configured
    if ($payments_enabled && !empty($stripe_secret) && !empty($stripe_publishable)) {
        // Only check API connection if SDK is loaded
        if (class_exists('Stripe\\Stripe')) {
            try {
                \Stripe\Stripe::setApiKey($stripe_secret);
                $account = \Stripe\Account::retrieve();
                
                // Show success notice with account details
                ?>
                <div class="notice notice-success is-dismissible" data-vmo-notice="stripe-success">
                    <p>
                        <strong>✅ Stripe Connected Successfully</strong><br>
                        Account: <?php echo esc_html($account->email); ?> (<?php echo esc_html($account->country); ?>)<br>
                        Mode: <?php echo (strpos($stripe_secret, 'sk_test_') === 0) ? 'Test Mode' : 'Live Mode'; ?>
                    </p>
                </div>
                <?php
                
            } catch (Exception $e) {
                // Show error if API key is invalid
                ?>
                <div class="notice notice-error is-dismissible" data-vmo-notice="stripe-error">
                    <p>
                        <strong>❌ Stripe Connection Error</strong><br>
                        <?php echo esc_html($e->getMessage()); ?><br>
                        <a href="<?php echo admin_url('admin.php?page=vmo-ai-builder'); ?>">Check API Keys →</a>
                    </p>
                </div>
                <?php
            }
        }
    }
});

// Handle notice dismissal
add_action('wp_ajax_vmo_dismiss_notice', function() {
    check_ajax_referer('vmo-admin', 'nonce');
    update_user_meta(get_current_user_id(), 'vmo_stripe_notice_dismissed', true);
    wp_send_json_success();
});

// ========================================
// DISMISS BACKGROUND OPS UPGRADE PROMPT
// ========================================
add_action('wp_ajax_vmo_dismiss_upgrade_prompt', function() {
    check_ajax_referer('vmo_builder_nonce', 'security');
    $user_id = get_current_user_id();
    if (!$user_id) { wp_send_json_error(['message' => 'Not logged in']); return; }

    $index = isset($_POST['index']) ? intval($_POST['index']) : -1;
    $dismiss_all = !empty($_POST['dismiss_all']);

    if ($dismiss_all) {
        delete_user_meta($user_id, 'vmo_upgrade_prompts');
        wp_send_json_success(['cleared' => true]);
        return;
    }

    $prompts = get_user_meta($user_id, 'vmo_upgrade_prompts', true) ?: [];
    if ($index >= 0 && $index < count($prompts)) {
        array_splice($prompts, $index, 1);
        if (empty($prompts)) {
            delete_user_meta($user_id, 'vmo_upgrade_prompts');
        } else {
            update_user_meta($user_id, 'vmo_upgrade_prompts', $prompts);
        }
    }
    wp_send_json_success(['remaining' => count($prompts)]);
});

if (!function_exists('vmo_get_current_generated_website_html')) {
    function vmo_get_current_generated_website_html($user_id) {
        $website_html = get_user_meta($user_id, 'vmo_website_code', true);
        if (empty($website_html)) {
            $website_html = get_user_meta($user_id, 'vmo_generated_website', true);
        }
        $website_html = is_string($website_html) ? $website_html : '';
        if (function_exists('vmo_strip_editor_only_media_overlays')) {
            $website_html = vmo_strip_editor_only_media_overlays($website_html);
        }
        return $website_html;
    }
}

if (!function_exists('vmo_export_readiness_blockers')) {
    function vmo_export_readiness_blockers($user_id) {
        $manifest = get_user_meta((int) $user_id, 'vmo_website_operations_manifest', true);
        if (!is_array($manifest)) {
            return array();
        }

        $indicators = is_array($manifest['dashboard']['indicators'] ?? null) ? $manifest['dashboard']['indicators'] : array();
        if (empty($indicators)) {
            return array();
        }

        $blockers = array();
        if (empty($indicators['media_complete'])) {
            $blockers[] = 'required media is incomplete';
        }
        if (empty($indicators['preview_ready'])) {
            $blockers[] = 'final preview gate is not ready';
        }
        if (empty($indicators['standalone_portable'])) {
            $blockers[] = 'standalone portability is not confirmed';
        }
        if (empty($indicators['component_state_ready'])) {
            $blockers[] = 'component state manifest is incomplete';
        }
        if (empty($indicators['tracking_contract_ready'])) {
            $blockers[] = 'dashboard tracking contract is incomplete';
        }

        return array_values(array_unique($blockers));
    }
}

if (!function_exists('vmo_static_export_asset_extension')) {
    function vmo_static_export_asset_extension($mime_type) {
        $mime_type = strtolower(trim((string) $mime_type));
        $map = array(
            'image/jpeg' => 'jpg',
            'image/jpg' => 'jpg',
            'image/png' => 'png',
            'image/gif' => 'gif',
            'image/webp' => 'webp',
            'image/svg+xml' => 'svg',
            'font/woff' => 'woff',
            'font/woff2' => 'woff2',
            'application/font-woff' => 'woff',
            'application/font-woff2' => 'woff2',
            'video/mp4' => 'mp4',
            'audio/mpeg' => 'mp3',
            'audio/mp3' => 'mp3',
        );
        if (isset($map[$mime_type])) {
            return $map[$mime_type];
        }
        $parts = explode('/', $mime_type);
        $extension = isset($parts[1]) ? preg_replace('/[^a-z0-9]+/i', '', $parts[1]) : 'bin';
        return $extension ?: 'bin';
    }
}

if (!function_exists('vmo_static_export_bridge_js')) {
    function vmo_static_export_bridge_js() {
        return <<<'JS'
(function () {
  'use strict';

  function emit(name, detail) {
    document.dispatchEvent(new CustomEvent(name, { detail: detail || {} }));
  }

  function serializeForm(form) {
    var payload = {};
    var formData = new FormData(form);
    formData.forEach(function (value, key) {
      if (Object.prototype.hasOwnProperty.call(payload, key)) {
        if (!Array.isArray(payload[key])) payload[key] = [payload[key]];
        payload[key].push(value);
      } else {
        payload[key] = value;
      }
    });
    payload.vmo_form = form.getAttribute('data-vmo-form') || form.getAttribute('name') || 'website_form';
    payload.vmo_page = location.href;
    payload.vmo_submitted_at = new Date().toISOString();
    return payload;
  }

  function setFormStatus(form, message, state) {
    var targetSelector = form.getAttribute('data-vmo-status-target');
    var target = targetSelector ? document.querySelector(targetSelector) : form.querySelector('[data-vmo-form-status]');
    if (!target) return;
    target.textContent = message;
    target.setAttribute('data-vmo-state', state || 'idle');
  }

  function bindForms() {
    document.querySelectorAll('form[data-vmo-form]').forEach(function (form) {
      if (form.__vmoBound) return;
      form.__vmoBound = true;
      form.addEventListener('submit', function (event) {
        var endpoint = form.getAttribute('data-vmo-webhook') || form.getAttribute('data-vmo-endpoint') || form.getAttribute('action') || '';
        var payload = serializeForm(form);
        if (!endpoint) {
          event.preventDefault();
          setFormStatus(form, 'This form is ready, but no endpoint is configured yet.', 'missing_endpoint');
          emit('vmo:form:missing-endpoint', { form: form, payload: payload });
          return;
        }
        event.preventDefault();
        setFormStatus(form, 'Sending...', 'sending');
        fetch(endpoint, {
          method: form.getAttribute('method') || 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(payload),
          mode: 'cors'
        }).then(function (response) {
          if (!response.ok) throw new Error('Request failed with status ' + response.status);
          setFormStatus(form, form.getAttribute('data-vmo-success') || 'Sent.', 'sent');
          emit('vmo:form:sent', { form: form, response: response });
          if (form.getAttribute('data-vmo-reset') !== 'false') form.reset();
        }).catch(function (error) {
          setFormStatus(form, form.getAttribute('data-vmo-error') || 'Unable to send right now.', 'error');
          emit('vmo:form:error', { form: form, error: error, payload: payload });
        });
      });
    });
  }

  function trackingEndpoint(source) {
    return (source && source.getAttribute('data-vmo-track-endpoint')) ||
      document.documentElement.getAttribute('data-vmo-track-endpoint') ||
      document.body.getAttribute('data-vmo-track-endpoint') || '';
  }

  function sendTrack(name, source, extra) {
    var payload = JSON.stringify(Object.assign({
      event: name,
      page: location.href,
      occurred_at: new Date().toISOString()
    }, extra || {}));
    var endpoint = trackingEndpoint(source);
    emit('vmo:track', { event: name, payload: payload, endpoint: endpoint });
    if (!endpoint) return;
    if (navigator.sendBeacon) {
      navigator.sendBeacon(endpoint, new Blob([payload], { type: 'application/json' }));
      return;
    }
    fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: payload, mode: 'cors', keepalive: true }).catch(function () {});
  }

  function bindClicks() {
    document.addEventListener('click', function (event) {
      var checkout = event.target.closest('[data-vmo-checkout-url]');
      if (checkout) {
        var checkoutUrl = checkout.getAttribute('data-vmo-checkout-url');
        if (checkoutUrl) {
          event.preventDefault();
          sendTrack('checkout_start', checkout, { product_id: checkout.getAttribute('data-vmo-product-id') || '' });
          location.href = checkoutUrl;
          return;
        }
      }
      var tracked = event.target.closest('[data-vmo-track]');
      if (tracked) {
        sendTrack(tracked.getAttribute('data-vmo-track'), tracked, { label: tracked.getAttribute('data-vmo-track-label') || tracked.textContent.trim().slice(0, 120) });
      }
    });
  }

  function boot() {
    bindForms();
    bindClicks();
    sendTrack('page_view', document.body);
    emit('vmo:standalone-ready', { runtime: 'standalone_html_css_js' });
  }

  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot);
  else boot();
})();
JS;
    }
}

if (!function_exists('vmo_static_export_extract_embedded_assets')) {
    function vmo_static_export_extract_embedded_assets($html, $project_dir, &$embedded_assets) {
        $counter = 0;
        return preg_replace_callback('/\b(src|href|poster)=(["\'])data:([^;,"\']+);base64,([^"\']+)\2/i', function($matches) use ($project_dir, &$embedded_assets, &$counter) {
            $binary = base64_decode(preg_replace('/\s+/', '', $matches[4]), true); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode -- export packaging
            if ($binary === false) {
                return $matches[0];
            }
            $counter++;
            $extension = vmo_static_export_asset_extension($matches[3]);
            $relative = 'assets/embedded/asset-' . str_pad((string) $counter, 3, '0', STR_PAD_LEFT) . '.' . $extension;
            wp_mkdir_p($project_dir . '/assets/embedded');
            if (vmo_get_filesystem()->put_contents($project_dir . '/' . $relative, $binary, FS_CHMOD_FILE) === false) {
                return $matches[0];
            }
            $embedded_assets[] = array(
                'file' => $relative,
                'mime' => strtolower($matches[3]),
                'bytes' => strlen($binary),
            );
            return $matches[1] . '=' . $matches[2] . $relative . $matches[2];
        }, $html);
    }
}

if (!function_exists('vmo_static_export_copy_local_asset_url')) {
    function vmo_static_export_copy_local_asset_url($url, $project_dir, &$copied_assets) {
        static $cache = array();
        $decoded_url = html_entity_decode((string) $url, ENT_QUOTES, 'UTF-8');
        if ($decoded_url === '' || preg_match('/^(data:|#|mailto:|tel:|sms:|javascript:|blob:)/i', $decoded_url)) {
            return null;
        }
        if (isset($cache[$decoded_url])) {
            return $cache[$decoded_url];
        }
        $path = parse_url($decoded_url, PHP_URL_PATH);
        if (!$path && isset($decoded_url[0]) && $decoded_url[0] === '/') {
            $path = $decoded_url;
        }
        if (!$path) {
            return null;
        }

        $uploads = wp_upload_dir();
        if (!empty($uploads['error']) || empty($uploads['basedir']) || empty($uploads['baseurl'])) {
            return null;
        }
        $uploads_path = parse_url($uploads['baseurl'], PHP_URL_PATH);
        if (!$uploads_path) {
            $uploads_path = '/wp-content/uploads';
        }
        $uploads_path = '/' . trim($uploads_path, '/');
        $normalized_path = '/' . ltrim($path, '/');
        if ($normalized_path !== $uploads_path && strpos($normalized_path, $uploads_path . '/') !== 0) {
            return null;
        }

        $relative_to_uploads = ltrim(substr($normalized_path, strlen($uploads_path)), '/');
        if ($relative_to_uploads === '' || strpos($relative_to_uploads, '..') !== false) {
            return null;
        }
        $source_path = trailingslashit($uploads['basedir']) . $relative_to_uploads;
        $real_basedir = realpath($uploads['basedir']);
        $real_source = realpath($source_path);
        if (!$real_source || !$real_basedir || strpos($real_source, $real_basedir) !== 0 || !is_readable($real_source) || is_dir($real_source)) {
            return null;
        }

        $filename = sanitize_file_name(basename($relative_to_uploads));
        if (!$filename) {
            $filename = 'asset-' . substr(sha1($relative_to_uploads), 0, 10) . '.bin';
        }
        $relative = 'assets/media/' . substr(sha1($real_source . '|' . filesize($real_source)), 0, 10) . '-' . $filename;
        wp_mkdir_p($project_dir . '/assets/media');
        $contents = vmo_get_filesystem()->get_contents($real_source);
        if ($contents === false || vmo_get_filesystem()->put_contents($project_dir . '/' . $relative, $contents, FS_CHMOD_FILE) === false) {
            return null;
        }
        $cache[$decoded_url] = $relative;
        $copied_assets[] = array(
            'source' => $decoded_url,
            'file' => $relative,
            'bytes' => strlen($contents),
        );
        return $relative;
    }
}

if (!function_exists('vmo_static_export_localize_assets')) {
    function vmo_static_export_localize_assets($html, $project_dir, &$copied_assets) {
        $html = preg_replace_callback('/\b(src|href|poster)=(["\'])([^"\']+)\2/i', function($matches) use ($project_dir, &$copied_assets) {
            $local_url = vmo_static_export_copy_local_asset_url($matches[3], $project_dir, $copied_assets);
            if (!$local_url) {
                return $matches[0];
            }
            return $matches[1] . '=' . $matches[2] . $local_url . $matches[2];
        }, $html);

        return preg_replace_callback('/url\((["\']?)([^)"\']+)\1\)/i', function($matches) use ($project_dir, &$copied_assets) {
            $local_url = vmo_static_export_copy_local_asset_url(trim($matches[2]), $project_dir, $copied_assets);
            if (!$local_url) {
                return $matches[0];
            }
            $quote = $matches[1] ?: '';
            return 'url(' . $quote . $local_url . $quote . ')';
        }, $html);
    }
}

if (!function_exists('vmo_static_export_collect_external_assets')) {
    function vmo_static_export_collect_external_assets($html) {
        $assets = array();
        if (preg_match_all('/\b(?:src|href|poster)=(["\'])(https?:\/\/[^"\']+)\1/i', $html, $matches)) {
            $assets = array_merge($assets, $matches[2]);
        }
        if (preg_match_all('/url\((["\']?)(https?:\/\/[^)"\']+)\1\)/i', $html, $matches)) {
            $assets = array_merge($assets, $matches[2]);
        }
        $assets = array_values(array_unique($assets));
        sort($assets);
        return $assets;
    }
}

if (!function_exists('vmo_static_export_insert_head_markup')) {
    function vmo_static_export_insert_head_markup($html, $markup) {
        if (stripos($html, '<head') !== false) {
            return preg_replace_callback('/<head\b[^>]*>/i', function($matches) use ($markup) {
                return $matches[0] . "\n" . $markup;
            }, $html, 1);
        }
        if (stripos($html, '<html') !== false) {
            return preg_replace_callback('/<html\b[^>]*>/i', function($matches) use ($markup) {
                return $matches[0] . "\n<head>\n" . $markup . "\n</head>";
            }, $html, 1);
        }
        return "<head>\n" . $markup . "\n</head>\n" . $html;
    }
}

if (!function_exists('vmo_static_export_prepare_html')) {
    function vmo_static_export_prepare_html($html, $site_title) {
        $html = trim((string) $html);
        if (function_exists('vmo_strip_editor_only_media_overlays')) {
            $html = vmo_strip_editor_only_media_overlays($html);
        }
        $site_title = $site_title ?: 'VMO Website';
        if (stripos($html, '<html') === false) {
            $html = "<!doctype html>\n<html lang=\"en\">\n<head>\n<title>" . esc_html($site_title) . "</title>\n</head>\n<body>\n" . $html . "\n</body>\n</html>";
        } elseif (!preg_match('/^\s*<!doctype/i', $html)) {
            $html = "<!doctype html>\n" . $html;
        }

        $head_markup = array();
        if (stripos($html, '<meta charset') === false) {
            $head_markup[] = '<meta charset="utf-8">';
        }
        if (stripos($html, 'name="viewport"') === false && stripos($html, "name='viewport'") === false) {
            $head_markup[] = '<meta name="viewport" content="width=device-width, initial-scale=1">';
        }
        // Security headers (May 2026): meta-tag fallback for static hosts that
        // can't set HTTP headers. Real HTTP headers should also be set by the
        // serving stack (Cloudflare Workers, nginx) but these baseline meta
        // headers prevent the worst common scanner flags (clickjacking,
        // content-type sniffing, referrer leaks).
        if (stripos($html, 'http-equiv="X-UA-Compatible"') === false) {
            $head_markup[] = '<meta http-equiv="X-UA-Compatible" content="IE=edge">';
        }
        if (stripos($html, 'http-equiv="Content-Security-Policy"') === false) {
            $head_markup[] = '<meta http-equiv="Content-Security-Policy" content="default-src \'self\' https: data:; script-src \'self\' https: \'unsafe-inline\'; style-src \'self\' https: \'unsafe-inline\'; img-src \'self\' https: data: blob:; font-src \'self\' https: data:; connect-src \'self\' https:; frame-ancestors \'self\';">';
        }
        if (stripos($html, 'name="referrer"') === false && stripos($html, 'http-equiv="Referrer-Policy"') === false) {
            $head_markup[] = '<meta name="referrer" content="strict-origin-when-cross-origin">';
        }
        if (stripos($html, 'http-equiv="X-Content-Type-Options"') === false) {
            $head_markup[] = '<meta http-equiv="X-Content-Type-Options" content="nosniff">';
        }
        if (stripos($html, 'name="vmo-runtime-output"') === false && stripos($html, "name='vmo-runtime-output'") === false) {
            $head_markup[] = '<meta name="vmo-runtime-output" content="standalone_html_css_js">';
            $head_markup[] = '<meta name="vmo-editing-surface" content="vmo_internal_builder_template">';
            $head_markup[] = '<meta name="vmo-export-manifest" content="vmo-site-manifest.json">';
        }
        if (!empty($head_markup)) {
            $html = vmo_static_export_insert_head_markup($html, implode("\n", $head_markup));
        }

        if (stripos($html, 'assets/vmo-standalone-bridge.js') === false) {
            $bridge_tag = "\n<script defer src=\"assets/vmo-standalone-bridge.js\"></script>\n";
            if (stripos($html, '</body>') !== false) {
                $html = preg_replace('/<\/body>/i', $bridge_tag . '</body>', $html, 1);
            } else {
                $html .= $bridge_tag;
            }
        }
        return $html;
    }
}

if (!function_exists('vmo_static_export_zip_directory')) {
    function vmo_static_export_zip_directory($zip, $source_dir) {
        $dir_iterator = new RecursiveDirectoryIterator($source_dir, RecursiveDirectoryIterator::SKIP_DOTS);
        $iterator = new RecursiveIteratorIterator($dir_iterator, RecursiveIteratorIterator::SELF_FIRST);
        foreach ($iterator as $file) {
            $relative = substr($file->getPathname(), strlen($source_dir) + 1);
            if ($file->isDir()) {
                $zip->addEmptyDir($relative);
            } else {
                $zip->addFile($file->getPathname(), $relative);
            }
        }
    }
}

if (!function_exists('vmo_static_export_cleanup_dir')) {
    function vmo_static_export_cleanup_dir($dir) {
        if (!$dir || !is_dir($dir)) {
            return;
        }
        $cleanup_iterator = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
            RecursiveIteratorIterator::CHILD_FIRST
        );
        foreach ($cleanup_iterator as $file) {
            $file->isDir() ? @rmdir($file->getPathname()) : @unlink($file->getPathname());
        }
        @rmdir($dir);
    }
}

// ========================================
// STATIC SITE EXPORT
// ========================================
// Generates a portable plain HTML/CSS/JS ZIP with local copied media where possible.
add_action('wp_ajax_vmo_export_static_site', function() {
    check_ajax_referer('vmo_builder_nonce', 'security');
    $user_id = get_current_user_id();
    if (!$user_id) { wp_send_json_error(['message' => 'Not logged in']); return; }
    if (!class_exists('ZipArchive')) {
        wp_send_json_error(['message' => 'Static export requires the PHP ZipArchive extension.']);
        return;
    }

    $website_html = vmo_get_current_generated_website_html($user_id);
    if (empty($website_html)) {
        wp_send_json_error(['message' => 'No generated website found. Please generate a website first.']);
        return;
    }
    if (function_exists('vmo_generated_page_standalone_blockers')) {
        $standalone_blockers = vmo_generated_page_standalone_blockers($website_html);
        if (!empty($standalone_blockers)) {
            wp_send_json_error([
                'message' => 'Static export blocked because the website still depends on WordPress runtime pieces: ' . implode(', ', $standalone_blockers),
                'code' => 'standalone_dependency_blocked',
                'standalone_blockers' => $standalone_blockers,
            ]);
            return;
        }
    }
    if (function_exists('vmo_generated_page_completion_blockers')) {
        $completion_blockers = vmo_generated_page_completion_blockers($website_html);
        if (!empty($completion_blockers)) {
            wp_send_json_error([
                'message' => 'Static export blocked because the canonical preview gate is still locked: ' . implode(', ', $completion_blockers),
                'code' => 'preview_gate_blocked',
                'completion_blockers' => $completion_blockers,
            ]);
            return;
        }
    }
    $export_readiness_blockers = vmo_export_readiness_blockers($user_id);
    if (!empty($export_readiness_blockers)) {
        wp_send_json_error([
            'message' => 'Static export blocked because the v2 readiness manifest is not complete: ' . implode(', ', $export_readiness_blockers),
            'code' => 'export_readiness_blocked',
            'readiness_blockers' => $export_readiness_blockers,
        ]);
        return;
    }

    $business_data = get_user_meta($user_id, 'vmo_business_data', true) ?: [];
    $business_name = sanitize_text_field($business_data['business_name'] ?? 'My Website');
    $site_slug = sanitize_title($business_name ?: 'my-website');
    if (!$site_slug) {
        $site_slug = 'my-website';
    }

    $tmp_dir = trailingslashit(get_temp_dir()) . 'vmo-static-' . $user_id . '-' . time();
    $site_dir = $tmp_dir . '/' . $site_slug;
    wp_mkdir_p($site_dir . '/assets');

    $embedded_assets = array();
    $copied_assets = array();
    $html = vmo_static_export_extract_embedded_assets($website_html, $site_dir, $embedded_assets);
    $html = vmo_static_export_localize_assets($html, $site_dir, $copied_assets);
    $html = vmo_static_export_prepare_html($html, $business_name);
    $external_assets = vmo_static_export_collect_external_assets($html);

    vmo_get_filesystem()->put_contents($site_dir . '/index.html', $html, FS_CHMOD_FILE);
    vmo_get_filesystem()->put_contents($site_dir . '/assets/vmo-standalone-bridge.js', vmo_static_export_bridge_js(), FS_CHMOD_FILE);

    $manifest = array(
        'format' => 'vmo_static_site_export',
        'version' => '1.0.0',
        'generated_at' => gmdate('c'),
        'business_name' => $business_name,
        'runtime_output' => 'standalone_html_css_js',
        'wordpress_runtime_dependency_allowed' => false,
        'editing_surface' => 'vmo_internal_builder_template',
        'entrypoint' => 'index.html',
        'included_files' => array_merge(
            array('index.html', 'assets/vmo-standalone-bridge.js', 'vmo-site-manifest.json', 'README.md'),
            array_column($embedded_assets, 'file'),
            array_column($copied_assets, 'file')
        ),
        'embedded_assets' => $embedded_assets,
        'copied_assets' => $copied_assets,
        'external_assets' => $external_assets,
        'portable_contracts' => array(
            'forms' => 'form[data-vmo-form] submits JSON to data-vmo-webhook, data-vmo-endpoint, or form action.',
            'tracking' => 'data-vmo-track sends events to data-vmo-track-endpoint when configured.',
            'payments' => 'data-vmo-checkout-url opens hosted checkout without WordPress runtime code.',
            'products' => 'data-vmo-product-id identifies catalog items for portable checkout/reporting.',
        ),
        'standalone_blockers' => array(),
    );
    vmo_get_filesystem()->put_contents($site_dir . '/vmo-site-manifest.json', wp_json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), FS_CHMOD_FILE);

    $readme = "# " . $business_name . "\n\nThis is a VMO static site export. It is plain HTML, CSS, JavaScript, and packaged assets. It does not require WordPress, the WordPress editor, admin-ajax.php, wp-json, plugin assets, or theme assets at runtime.\n\n## Deploy\n\nUpload the contents of this folder to any static host, CDN, object storage bucket, or conventional web server. Open `index.html` locally for a quick smoke test.\n\n## VMO Integrations\n\nThe included `assets/vmo-standalone-bridge.js` handles portable `data-vmo-*` contracts for forms, tracking, checkout links, and product identifiers. Configure webhook, tracking, and checkout URLs in the exported markup or your deployment environment.\n";
    vmo_get_filesystem()->put_contents($site_dir . '/README.md', $readme, FS_CHMOD_FILE);

    $zip_path = $tmp_dir . '/' . $site_slug . '-static-site.zip';
    $zip = new ZipArchive();
    if ($zip->open($zip_path, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
        vmo_static_export_cleanup_dir($tmp_dir);
        wp_send_json_error(['message' => 'Failed to create static site ZIP archive.']);
        return;
    }
    vmo_static_export_zip_directory($zip, $site_dir);
    $zip->close();

    $zip_data = vmo_get_filesystem()->get_contents($zip_path);
    if ($zip_data === false) {
        vmo_static_export_cleanup_dir($tmp_dir);
        wp_send_json_error(['message' => 'Failed to read static site ZIP archive.']);
        return;
    }
    $zip_base64 = base64_encode($zip_data); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- AJAX download payload
    $zip_size = strlen($zip_data);
    vmo_static_export_cleanup_dir($tmp_dir);

    wp_send_json_success([
        'filename' => $site_slug . '-static-site.zip',
        'data' => $zip_base64,
        'size' => $zip_size,
        'manifest' => $manifest,
        'message' => 'Static site exported. This package runs without WordPress runtime dependencies.',
    ]);
});

// ========================================
// NEXT.JS PROJECT EXPORT
// ========================================
// Generates a downloadable Next.js App Router project ZIP from the user's generated website
add_action('wp_ajax_vmo_export_nextjs', function() {
    check_ajax_referer('vmo_builder_nonce', 'security');
    $user_id = get_current_user_id();
    if (!$user_id) { wp_send_json_error(['message' => 'Not logged in']); return; }

    // Get the current website HTML from user meta or last content
    $website_html = vmo_get_current_generated_website_html($user_id);
    if (empty($website_html)) {
        wp_send_json_error(['message' => 'No generated website found. Please generate a website first.']);
        return;
    }
    if (function_exists('vmo_generated_page_standalone_blockers')) {
        $standalone_blockers = vmo_generated_page_standalone_blockers($website_html);
        if (!empty($standalone_blockers)) {
            wp_send_json_error([
                'message' => 'Export blocked because the website still depends on WordPress runtime pieces: ' . implode(', ', $standalone_blockers),
                'code' => 'standalone_dependency_blocked',
                'standalone_blockers' => $standalone_blockers,
            ]);
            return;
        }
    }
    if (function_exists('vmo_generated_page_completion_blockers')) {
        $completion_blockers = vmo_generated_page_completion_blockers($website_html);
        if (!empty($completion_blockers)) {
            wp_send_json_error([
                'message' => 'Export blocked because the canonical preview gate is still locked: ' . implode(', ', $completion_blockers),
                'code' => 'preview_gate_blocked',
                'completion_blockers' => $completion_blockers,
            ]);
            return;
        }
    }
    $export_readiness_blockers = vmo_export_readiness_blockers($user_id);
    if (!empty($export_readiness_blockers)) {
        wp_send_json_error([
            'message' => 'Export blocked because the v2 readiness manifest is not complete: ' . implode(', ', $export_readiness_blockers),
            'code' => 'export_readiness_blocked',
            'readiness_blockers' => $export_readiness_blockers,
        ]);
        return;
    }

    $business_data = get_user_meta($user_id, 'vmo_business_data', true) ?: [];
    $biz_name = sanitize_title($business_data['business_name'] ?? 'my-website');

    // Create temp directory for the project
    $tmp_dir = get_temp_dir() . 'vmo-nextjs-' . $user_id . '-' . time();
    $project_dir = $tmp_dir . '/' . $biz_name;
    wp_mkdir_p($project_dir . '/app');
    wp_mkdir_p($project_dir . '/components');
    wp_mkdir_p($project_dir . '/public');

    // package.json
    $package_json = wp_json_encode([
        'name' => $biz_name,
        'version' => '1.0.0',
        'private' => true,
        'scripts' => [
            'dev' => 'next dev',
            'build' => 'next build',
            'start' => 'next start',
            'lint' => 'next lint',
        ],
        'dependencies' => [
            'next' => '^14.0.0',
            'react' => '^18.2.0',
            'react-dom' => '^18.2.0',
            'gsap' => '^3.12.5',
            'framer-motion' => '^10.16.4',
            'three' => '^0.160.0',
            '@studio-freight/lenis' => '^1.0.29',
            'swiper' => '^11.0.0',
        ],
        'devDependencies' => [
            'tailwindcss' => '^3.4.0',
            'postcss' => '^8.4.0',
            'autoprefixer' => '^10.4.0',
            '@types/react' => '^18.2.0',
            '@types/node' => '^20.0.0',
            'typescript' => '^5.0.0',
        ],
    ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
    vmo_get_filesystem()->put_contents($project_dir . '/package.json', $package_json, FS_CHMOD_FILE);

    // next.config.js
    vmo_get_filesystem()->put_contents($project_dir . '/next.config.js', "/** @type {import('next').NextConfig} */\nconst nextConfig = {\n  output: 'export',\n  images: { unoptimized: true },\n};\n\nmodule.exports = nextConfig;\n", FS_CHMOD_FILE);

    // tailwind.config.js
    vmo_get_filesystem()->put_contents($project_dir . '/tailwind.config.js', "/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n  content: ['./app/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],\n  theme: {\n    extend: {\n      colors: { primary: '#3b82f6', secondary: '#10b981' },\n    },\n  },\n  plugins: [],\n};\n", FS_CHMOD_FILE);

    // postcss.config.js
    vmo_get_filesystem()->put_contents($project_dir . '/postcss.config.js', "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n};\n", FS_CHMOD_FILE);

    // tsconfig.json
    $tsconfig = wp_json_encode([
        'compilerOptions' => [
            'target' => 'es5',
            'lib' => ['dom', 'dom.iterable', 'esnext'],
            'allowJs' => true,
            'skipLibCheck' => true,
            'strict' => true,
            'noEmit' => true,
            'esModuleInterop' => true,
            'module' => 'esnext',
            'moduleResolution' => 'bundler',
            'resolveJsonModule' => true,
            'isolatedModules' => true,
            'jsx' => 'preserve',
            'incremental' => true,
            'paths' => ['@/*' => ['./*']],
        ],
        'include' => ['next-env.d.ts', '**/*.ts', '**/*.tsx'],
        'exclude' => ['node_modules'],
    ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
    vmo_get_filesystem()->put_contents($project_dir . '/tsconfig.json', $tsconfig, FS_CHMOD_FILE);

    // app/globals.css
    vmo_get_filesystem()->put_contents($project_dir . '/app/globals.css', "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n/* Smooth scrolling */\nhtml { scroll-behavior: smooth; }\nbody { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }\n", FS_CHMOD_FILE);

    // app/layout.tsx
    $layout = "'use client';\nimport './globals.css';\n\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n  return (\n    <html lang=\"en\">\n      <body>{children}</body>\n    </html>\n  );\n}\n";
    vmo_get_filesystem()->put_contents($project_dir . '/app/layout.tsx', $layout, FS_CHMOD_FILE);

    // app/page.tsx — the main generated website content
    // Extract React component code from the HTML, stripping CDN script tags
    $component_code = $website_html;
    // Remove script CDN tags (React, Babel, Tailwind CDN, etc.) — Next.js bundles these
    $component_code = preg_replace('/<script[^>]*src="https?:\/\/[^"]*"[^>]*><\/script>\s*/i', '', $component_code);
    $component_code = preg_replace('/<script[^>]*src="https?:\/\/[^"]*"[^>]*\/>\s*/i', '', $component_code);
    // Remove <link> CDN tags
    $component_code = preg_replace('/<link[^>]*href="https?:\/\/[^"]*"[^>]*\/?>\s*/i', '', $component_code);
    // Remove Babel script blocks and extract JSX
    if (preg_match('/<script[^>]*type=["\']text\/babel["\'][^>]*>(.*?)<\/script>/is', $component_code, $m)) {
        $jsx_code = $m[1];
        // Remove ReactDOM.createRoot render call — Next.js handles this
        $jsx_code = preg_replace('/ReactDOM\.createRoot\([^)]+\)\.render\([^)]+\);?\s*/i', '', $jsx_code);
        $page_content = "'use client';\nimport { useEffect, useRef, useState } from 'react';\nimport gsap from 'gsap';\nimport { ScrollTrigger } from 'gsap/ScrollTrigger';\n\ngsap.registerPlugin(ScrollTrigger);\n\n" . trim($jsx_code) . "\n\nexport default function Page() {\n  return <App />;\n}\n";
    } else {
        // Fallback: wrap entire HTML as a dangerouslySetInnerHTML page
        $escaped = addslashes($component_code);
        $escaped = str_replace("\n", "\\n", $escaped);
        $page_content = "'use client';\n\nexport default function Page() {\n  return <div dangerouslySetInnerHTML={{ __html: \"" . $escaped . "\" }} />;\n}\n";
    }
    vmo_get_filesystem()->put_contents($project_dir . '/app/page.tsx', $page_content, FS_CHMOD_FILE);

    // README.md
    $readme = "# " . ($business_data['business_name'] ?? 'My Website') . "\n\nGenerated by VMO AI Website Builder — Next.js App Router Edition.\n\n## Getting Started\n\n```bash\nnpm install\nnpm run dev\n```\n\nOpen [http://localhost:3000](http://localhost:3000) in your browser.\n\n## Deploy\n\nDeploy instantly to Vercel:\n\n[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new)\n\n## Tech Stack\n\n- **Next.js 14** (App Router)\n- **React 18**\n- **Tailwind CSS**\n- **GSAP** (ScrollTrigger animations)\n- **Framer Motion**\n- **Three.js** (3D elements)\n- **Lenis** (Smooth scroll)\n";
    vmo_get_filesystem()->put_contents($project_dir . '/README.md', $readme, FS_CHMOD_FILE);

    // .gitignore
    vmo_get_filesystem()->put_contents($project_dir . '/.gitignore', "node_modules/\n.next/\nout/\n.env*.local\n", FS_CHMOD_FILE);

    // Create ZIP
    $zip_path = $tmp_dir . '/' . $biz_name . '-nextjs.zip';
    $zip = new ZipArchive();
    if ($zip->open($zip_path, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
        wp_send_json_error(['message' => 'Failed to create ZIP archive']);
        return;
    }

    $dir_iterator = new RecursiveDirectoryIterator($project_dir, RecursiveDirectoryIterator::SKIP_DOTS);
    $iterator = new RecursiveIteratorIterator($dir_iterator, RecursiveIteratorIterator::SELF_FIRST);
    foreach ($iterator as $file) {
        $relative = substr($file->getPathname(), strlen($project_dir) + 1);
        if ($file->isDir()) {
            $zip->addEmptyDir($relative);
        } else {
            $zip->addFile($file->getPathname(), $relative);
        }
    }
    $zip->close();

    // Read ZIP and send as base64 (for AJAX download)
    $zip_data = vmo_get_filesystem()->get_contents($zip_path);
    $zip_base64 = base64_encode($zip_data); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- data encoding
    $zip_size = strlen($zip_data);

    // Cleanup temp files
    $cleanup_iterator = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($tmp_dir, RecursiveDirectoryIterator::SKIP_DOTS),
        RecursiveIteratorIterator::CHILD_FIRST
    );
    foreach ($cleanup_iterator as $file) {
        $file->isDir() ? @rmdir($file->getPathname()) : @unlink($file->getPathname());
    }
    @rmdir($tmp_dir);

    wp_send_json_success([
        'filename' => $biz_name . '-nextjs.zip',
        'data' => $zip_base64,
        'size' => $zip_size,
        'message' => 'Next.js project exported! Run npm install && npm run dev to start.',
    ]);
});

// ========================================
// CLEAR PENDING NOTIFICATION (Welcome Bonus, etc.)
// ========================================
// Clears pending notifications after they've been displayed to the user
add_action('wp_ajax_vmo_clear_pending_notification', function() {
    check_ajax_referer('vmo_builder_nonce', 'security');
    $user_id = get_current_user_id();
    if ($user_id) {
        delete_user_meta($user_id, 'vmo_pending_notification');
        delete_user_meta($user_id, 'vmo_quicklaunch_first_run_welcome_pending');
        wp_send_json_success(['cleared' => true]);
    } else {
        wp_send_json_error(['message' => 'Not logged in']);
    }
});

function vmo_create_plugin_tables() {
    global $wpdb;

    $charset_collate = $wpdb->get_charset_collate();

    // Only require upgrade.php if it exists (WordPress environment)
    if (defined('ABSPATH') && file_exists(ABSPATH . 'wp-admin/includes/upgrade.php')) {
        require_once ABSPATH . 'wp-admin/includes/upgrade.php';
    } else {
        // Fallback: define dbDelta if not available
        if (!function_exists('dbDelta')) {
            function dbDelta($queries) {
                // Mock function for testing - in real WordPress this handles table creation
                if (defined('WP_DEBUG') && WP_DEBUG) error_log("Mock dbDelta called with: " . print_r($queries, true));
                return array();
            }
        }
    }

    $projects_table = $wpdb->prefix . 'vmo_projects';
    $history_table = $wpdb->prefix . 'vmo_project_history';
    $consultations_table = $wpdb->prefix . 'vmo_consultations';
    $approval_table = $wpdb->prefix . 'vmo_approval_queue';
    // VOICE SYSTEM ACTIVE: Phone system tables enabled
    $phone_numbers_table = $wpdb->prefix . 'vmo_phone_numbers';
    $phone_calls_table = $wpdb->prefix . 'vmo_phone_calls';
    $phone_warnings_table = $wpdb->prefix . 'vmo_phone_warnings';
    $knowledge_table = $wpdb->prefix . 'vmo_knowledge_base';
    $business_profiles_table = $wpdb->prefix . 'vmo_business_profiles';

    $sql1 = "CREATE TABLE IF NOT EXISTS $projects_table (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        user_id BIGINT UNSIGNED NOT NULL,
        project_title VARCHAR(255) NOT NULL,
        html MEDIUMTEXT,
        images LONGTEXT,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
    ) $charset_collate;";

    // Phone Numbers Table
    $sql_phone1 = "CREATE TABLE IF NOT EXISTS $phone_numbers_table (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        user_id BIGINT UNSIGNED NOT NULL,
        phone_number VARCHAR(50) NOT NULL,
        twilio_sid VARCHAR(255) DEFAULT NULL,
        status VARCHAR(50) DEFAULT 'active',
        provisioned_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        released_at DATETIME DEFAULT NULL,
        settings LONGTEXT DEFAULT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        INDEX idx_user_id (user_id),
        INDEX idx_phone (phone_number),
        INDEX idx_status (status)
    ) $charset_collate;";

    // Phone Calls Table 
    $sql_phone2 = "CREATE TABLE IF NOT EXISTS $phone_calls_table (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        user_id BIGINT UNSIGNED NOT NULL,
        call_sid VARCHAR(255) NOT NULL,
        caller_number VARCHAR(50) DEFAULT NULL,
        direction ENUM('inbound', 'outbound') DEFAULT 'inbound',
        status VARCHAR(50) DEFAULT 'completed',
        duration INT UNSIGNED DEFAULT 0,
        recording_url VARCHAR(2083) DEFAULT NULL,
        transcript LONGTEXT DEFAULT NULL,
        summary LONGTEXT DEFAULT NULL,
        topic VARCHAR(255) DEFAULT NULL,
        ai_handled TINYINT(1) DEFAULT 0,
        contact_id BIGINT UNSIGNED DEFAULT NULL,
        tokens_used INT UNSIGNED DEFAULT 0,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        INDEX idx_user_id (user_id),
        INDEX idx_call_sid (call_sid),
        INDEX idx_status (status),
        INDEX idx_contact (contact_id)
    ) $charset_collate;";

    $sql2 = "CREATE TABLE IF NOT EXISTS $history_table (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        project_id BIGINT UNSIGNED NOT NULL,
        step_number INT UNSIGNED NOT NULL,
        html MEDIUMTEXT,
        images LONGTEXT,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP
    ) $charset_collate;";

    // Multi-Agent Business Consultation System Table
    // NOTE: This schema is intentionally wide and JSON-friendly to support
    // the full 8-phase consultation flow with permanent persistence.
    $sql3 = "CREATE TABLE IF NOT EXISTS $consultations_table (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        session_id VARCHAR(255) UNIQUE NOT NULL,
        user_id BIGINT UNSIGNED DEFAULT NULL,

        -- Phase tracking
        phase TINYINT UNSIGNED DEFAULT 1,
        phase_state LONGTEXT NULL,

        -- Core persisted artifacts (JSON strings)
        conversation_log LONGTEXT NULL,
        business_summary LONGTEXT NULL,
        research_data LONGTEXT NULL,
        approved_checklist LONGTEXT NULL,
        stripe_account_id VARCHAR(255) NULL,
        products LONGTEXT NULL,
        generated_content LONGTEXT NULL,

        -- Back-compat / legacy fields used in various subsystems
        start_time DATETIME DEFAULT CURRENT_TIMESTAMP,
        end_time DATETIME DEFAULT NULL,
        status VARCHAR(50) DEFAULT 'in_progress',
        client_data LONGTEXT NULL,
        business_name VARCHAR(255) DEFAULT NULL,
        business_type VARCHAR(100) DEFAULT NULL,
        consultation_data LONGTEXT NULL,
        business_analysis LONGTEXT NULL,
        recommendations LONGTEXT NULL,
        website_data LONGTEXT NULL,

        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        INDEX idx_session_id (session_id),
        INDEX idx_user_id (user_id),
        INDEX idx_status (status),
        INDEX idx_phase (phase)
    ) $charset_collate;";

    // Approval Queue Table
    $sql4 = "CREATE TABLE IF NOT EXISTS $approval_table (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        user_id BIGINT UNSIGNED NOT NULL,
        type ENUM('seo','email','doc','media','other') NOT NULL DEFAULT 'other',
        title VARCHAR(255) DEFAULT NULL,
        payload LONGTEXT NULL,
        related_post_id BIGINT UNSIGNED NULL,
        status ENUM('pending','approved','rejected') NOT NULL DEFAULT 'pending',
        notes TEXT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        INDEX idx_user_id (user_id),
        INDEX idx_type (type),
        INDEX idx_status (status),
        INDEX idx_related (related_post_id)
    ) $charset_collate;";

    dbDelta($sql1);
    dbDelta($sql2);
    dbDelta($sql3);
    dbDelta($sql4);
    dbDelta($sql_phone1); // vmo_phone_numbers
    dbDelta($sql_phone2); // vmo_phone_calls

    // ── Migrate existing vmo_contacts tables that are missing columns ──
    $suppress = $wpdb->suppress_errors(true);
    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
    $contacts_cols = $wpdb->get_col("SHOW COLUMNS FROM {$contacts_table}", 0);
    if (is_array($contacts_cols)) {
        if (!in_array('full_name', $contacts_cols, true)) {
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
            $wpdb->query("ALTER TABLE {$contacts_table} ADD COLUMN full_name VARCHAR(200) DEFAULT NULL AFTER last_name");
        }
        if (!in_array('first_name', $contacts_cols, true)) {
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
            $wpdb->query("ALTER TABLE {$contacts_table} ADD COLUMN first_name VARCHAR(100) DEFAULT NULL AFTER lifecycle_stage");
        }
        if (!in_array('last_name', $contacts_cols, true)) {
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
            $wpdb->query("ALTER TABLE {$contacts_table} ADD COLUMN last_name VARCHAR(100) DEFAULT NULL AFTER first_name");
        }
    }
    $wpdb->suppress_errors($suppress);

    // ── Migrate existing vmo_phone_calls tables that are missing new columns ──
    $suppress = $wpdb->suppress_errors(true);
    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
    $phone_cols = $wpdb->get_col("SHOW COLUMNS FROM {$phone_calls_table}", 0);
    if (is_array($phone_cols)) {
        if (!in_array('ai_handled', $phone_cols, true)) {
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
            $wpdb->query("ALTER TABLE {$phone_calls_table} ADD COLUMN ai_handled TINYINT(1) DEFAULT 0 AFTER duration");
        }
        if (!in_array('contact_id', $phone_cols, true)) {
            $after = in_array('ai_handled', $phone_cols, true) || !in_array('ai_handled', $phone_cols, true) ? 'ai_handled' : 'duration';
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
            $wpdb->query("ALTER TABLE {$phone_calls_table} ADD COLUMN contact_id BIGINT UNSIGNED DEFAULT NULL AFTER {$after}");
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
            $wpdb->query("ALTER TABLE {$phone_calls_table} ADD INDEX idx_contact (contact_id)");
        }
        if (!in_array('tokens_used', $phone_cols, true)) {
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
            $wpdb->query("ALTER TABLE {$phone_calls_table} ADD COLUMN tokens_used INT UNSIGNED DEFAULT 0 AFTER contact_id");
        }
        if (!in_array('topic', $phone_cols, true)) {
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
            $wpdb->query("ALTER TABLE {$phone_calls_table} ADD COLUMN topic VARCHAR(255) DEFAULT NULL AFTER summary");
        }
    }
    $wpdb->suppress_errors($suppress);

    // ✅ PERSISTENT BUSINESS PROFILE TABLE (Cross-session memory)
    // Stores accumulated business knowledge that persists forever
    $sql_business_profile = "CREATE TABLE IF NOT EXISTS $business_profiles_table (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        user_id BIGINT UNSIGNED NOT NULL UNIQUE,
        business_name VARCHAR(255) DEFAULT NULL,
        industry VARCHAR(255) DEFAULT NULL,
        founded_date DATE DEFAULT NULL,
        
        -- Accumulated business knowledge (JSON)
        business_summary LONGTEXT,  -- Executive summary of business
        uploaded_documents LONGTEXT,  -- List of all documents ever uploaded (JSON array)
        key_milestones LONGTEXT,  -- Timeline of achievements (JSON array)
        current_goals LONGTEXT,  -- Active business goals (JSON array)
        customer_personas LONGTEXT,  -- Target audience profiles (JSON)
        product_catalog LONGTEXT,  -- Products/services offered (JSON array)
        marketing_campaigns LONGTEXT,  -- Campaign history (JSON array)
        sales_data LONGTEXT,  -- Revenue, conversions, metrics (JSON)
        competitor_analysis LONGTEXT,  -- Competitive landscape (JSON)
        
        -- Consultation history metadata
        total_consultations INT UNSIGNED DEFAULT 0,
        last_consultation_date DATETIME DEFAULT NULL,
        last_consultation_topic VARCHAR(255) DEFAULT NULL,
        
        -- Extracted information (accumulated from all sessions)
        extracted_info LONGTEXT,  -- All extracted business info (JSON)
        
        -- Timestamps
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        
        INDEX idx_user_id (user_id),
        INDEX idx_last_consultation (last_consultation_date)
    ) $charset_collate;";
    
    dbDelta($sql_business_profile);

    // === CONTACT MANAGEMENT SYSTEM TABLES ===
    
    // Main contacts table
    $contacts_table = $wpdb->prefix . 'vmo_contacts';
    $sql5 = "CREATE TABLE IF NOT EXISTS $contacts_table (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        user_id BIGINT UNSIGNED NOT NULL,
        contact_id VARCHAR(255) UNIQUE NOT NULL,
        source ENUM('lead_generation', 'phone_call', 'manual_entry', 'website_form', 'social_media', 'email_marketing', 'referral', 'other') NOT NULL,
        primary_category VARCHAR(100) NOT NULL,
        secondary_category VARCHAR(100) DEFAULT NULL,
        lead_source ENUM('primary', 'secondary', 'tertiary') DEFAULT NULL,
        lead_verified TINYINT(1) DEFAULT 0,
        confidence_score TINYINT UNSIGNED DEFAULT 0,
        priority_level ENUM('high', 'medium', 'low') DEFAULT 'medium',
        lifecycle_stage ENUM('new', 'engaged', 'nurturing', 'active_relationship', 'customer', 'dormant') DEFAULT 'new',
        first_name VARCHAR(100) DEFAULT NULL,
        last_name VARCHAR(100) DEFAULT NULL,
        full_name VARCHAR(200) DEFAULT NULL,
        email VARCHAR(255) DEFAULT NULL,
        phone VARCHAR(50) DEFAULT NULL,
        company VARCHAR(200) DEFAULT NULL,
        job_title VARCHAR(100) DEFAULT NULL,
        address TEXT DEFAULT NULL,
        city VARCHAR(100) DEFAULT NULL,
        state VARCHAR(50) DEFAULT NULL,
        zip VARCHAR(20) DEFAULT NULL,
        country VARCHAR(50) DEFAULT NULL,
        lead_score TINYINT UNSIGNED DEFAULT 0,
        engagement_level ENUM('high', 'medium', 'low') DEFAULT 'medium',
        value_potential TINYINT UNSIGNED DEFAULT 50,
        tags TEXT DEFAULT NULL,
        notes TEXT DEFAULT NULL,
        acquisition_details LONGTEXT DEFAULT NULL,
        categorization_analysis LONGTEXT DEFAULT NULL,
        engagement_strategy LONGTEXT DEFAULT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        last_contact DATETIME DEFAULT NULL,
        INDEX idx_user_id (user_id),
        INDEX idx_contact_id (contact_id),
        INDEX idx_source (source),
        INDEX idx_category (primary_category),
        INDEX idx_lead_source (lead_source),
        INDEX idx_lead_verified (lead_verified),
        INDEX idx_priority (priority_level),
        INDEX idx_lifecycle (lifecycle_stage),
        INDEX idx_email (email),
        INDEX idx_phone (phone)
    ) $charset_collate;";

    // Contact interactions table
    $interactions_table = $wpdb->prefix . 'vmo_contact_interactions';
    $sql6 = "CREATE TABLE IF NOT EXISTS $interactions_table (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        user_id BIGINT UNSIGNED NOT NULL,
        contact_id VARCHAR(255) NOT NULL,
        interaction_type ENUM('phone_call', 'email', 'meeting', 'website_visit', 'purchase', 'support_ticket', 'follow_up', 'automated_engagement', 'manual_note', 'other') NOT NULL,
        interaction_subtype VARCHAR(100) DEFAULT NULL,
        direction ENUM('inbound', 'outbound', 'bidirectional') DEFAULT NULL,
        subject VARCHAR(255) DEFAULT NULL,
        summary TEXT DEFAULT NULL,
        full_details LONGTEXT DEFAULT NULL,
        outcome ENUM('successful', 'needs_follow_up', 'no_response', 'converted', 'lost', 'pending') DEFAULT 'pending',
        next_action TEXT DEFAULT NULL,
        follow_up_date DATETIME DEFAULT NULL,
        duration_minutes INT DEFAULT 0,
        ai_analysis LONGTEXT DEFAULT NULL,
        satisfaction_score TINYINT DEFAULT NULL,
        conversion_value DECIMAL(10,2) DEFAULT NULL,
        related_campaign VARCHAR(255) DEFAULT NULL,
        attachments LONGTEXT DEFAULT NULL,
        metadata LONGTEXT DEFAULT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        INDEX idx_user_id (user_id),
        INDEX idx_contact_id (contact_id),
        INDEX idx_interaction_type (interaction_type),
        INDEX idx_outcome (outcome),
        INDEX idx_created_at (created_at),
        INDEX idx_follow_up_date (follow_up_date)
    ) $charset_collate;";

    // Contact purchases/transactions table
    $purchases_table = $wpdb->prefix . 'vmo_contact_purchases';
    $sql7 = "CREATE TABLE IF NOT EXISTS $purchases_table (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        user_id BIGINT UNSIGNED NOT NULL,
        contact_id VARCHAR(255) NOT NULL,
        transaction_id VARCHAR(255) DEFAULT NULL,
        product_service VARCHAR(255) NOT NULL,
        purchase_type ENUM('one_time', 'subscription', 'recurring', 'service', 'consultation') NOT NULL,
        amount DECIMAL(10,2) NOT NULL,
        currency VARCHAR(10) DEFAULT 'USD',
        status ENUM('completed', 'pending', 'cancelled', 'refunded') DEFAULT 'pending',
        purchase_date DATETIME NOT NULL,
        subscription_details LONGTEXT DEFAULT NULL,
        payment_method VARCHAR(100) DEFAULT NULL,
        notes TEXT DEFAULT NULL,
        ai_insights LONGTEXT DEFAULT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        INDEX idx_user_id (user_id),
        INDEX idx_contact_id (contact_id),
        INDEX idx_purchase_date (purchase_date),
        INDEX idx_status (status),
        INDEX idx_purchase_type (purchase_type)
    ) $charset_collate;";

    // Contact communication preferences table
    $preferences_table = $wpdb->prefix . 'vmo_contact_preferences';
    $sql8 = "CREATE TABLE IF NOT EXISTS $preferences_table (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        user_id BIGINT UNSIGNED NOT NULL,
        contact_id VARCHAR(255) NOT NULL,
        email_subscribed BOOLEAN DEFAULT TRUE,
        sms_subscribed BOOLEAN DEFAULT FALSE,
        phone_calls_allowed BOOLEAN DEFAULT TRUE,
        marketing_emails BOOLEAN DEFAULT TRUE,
        newsletter_subscribed BOOLEAN DEFAULT FALSE,
        preferred_contact_method ENUM('email', 'phone', 'sms', 'none') DEFAULT 'email',
        best_contact_time VARCHAR(100) DEFAULT NULL,
        communication_frequency ENUM('daily', 'weekly', 'monthly', 'quarterly', 'as_needed') DEFAULT 'weekly',
        language_preference VARCHAR(10) DEFAULT 'en',
        timezone VARCHAR(50) DEFAULT NULL,
        do_not_contact BOOLEAN DEFAULT FALSE,
        unsubscribe_date DATETIME DEFAULT NULL,
        preferences_updated DATETIME DEFAULT CURRENT_TIMESTAMP,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        INDEX idx_user_id (user_id),
        INDEX idx_contact_id (contact_id),
        UNIQUE KEY unique_user_contact (user_id, contact_id)
    ) $charset_collate;";

    // Voice conversation tables
    $voice_conversations_table = $wpdb->prefix . 'vmo_voice_conversations';
    $sql9 = "CREATE TABLE IF NOT EXISTS $voice_conversations_table (
        id int(11) NOT NULL AUTO_INCREMENT,
        user_id int(11) NOT NULL,
        conversation_id varchar(255) NOT NULL,
        business_info text,
        voice_id varchar(255),
        status varchar(50) DEFAULT 'active',
        started_at datetime DEFAULT CURRENT_TIMESTAMP,
        ended_at datetime NULL,
        last_activity datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        total_turns int(11) DEFAULT 0,
        created_at datetime DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY (id),
        UNIQUE KEY conversation_id (conversation_id),
        KEY user_id (user_id),
        KEY status (status)
    ) $charset_collate;";

    $voice_conversation_turns_table = $wpdb->prefix . 'vmo_voice_conversation_turns';
    $sql10 = "CREATE TABLE IF NOT EXISTS $voice_conversation_turns_table (
        id int(11) NOT NULL AUTO_INCREMENT,
        conversation_id varchar(255) NOT NULL,
        user_message text NOT NULL,
        ai_response text NOT NULL,
        timestamp datetime DEFAULT CURRENT_TIMESTAMP,
        audio_duration float DEFAULT 0,
        tokens_used int(11) DEFAULT 0,
        PRIMARY KEY (id),
        KEY conversation_id (conversation_id),
        KEY timestamp (timestamp)
    ) $charset_collate;";

    dbDelta($sql5);
    dbDelta($sql6);
    dbDelta($sql7);
    dbDelta($sql8);
    dbDelta($sql9);
    dbDelta($sql10);

    // === Stripe Connect Tables ===
    $connected_accounts_table = $wpdb->prefix . 'vmo_connected_accounts';
    $sql11 = "CREATE TABLE IF NOT EXISTS $connected_accounts_table (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        user_id BIGINT UNSIGNED NOT NULL,
        stripe_account_id VARCHAR(255) NOT NULL,
        account_type VARCHAR(50) DEFAULT 'custom',
        details_submitted TINYINT(1) DEFAULT 0,
        payouts_enabled TINYINT(1) DEFAULT 0,
        charges_enabled TINYINT(1) DEFAULT 0,
        capabilities LONGTEXT NULL,
        requirements_due LONGTEXT NULL,
        last_synced DATETIME DEFAULT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        UNIQUE KEY uniq_user (user_id),
        UNIQUE KEY uniq_account (stripe_account_id),
        KEY idx_payouts (payouts_enabled)
    ) $charset_collate;";

    $store_orders_table = $wpdb->prefix . 'vmo_store_orders';
    $sql12 = "CREATE TABLE IF NOT EXISTS $store_orders_table (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        user_id BIGINT UNSIGNED NOT NULL, -- seller/merchant (site owner)
        buyer_user_id BIGINT UNSIGNED NULL, -- WP user if logged in
        order_reference VARCHAR(255) NULL, -- optional app order id
        stripe_checkout_session_id VARCHAR(255) NOT NULL,
        stripe_payment_intent_id VARCHAR(255) NULL,
        stripe_charge_id VARCHAR(255) NULL,
        amount_total BIGINT UNSIGNED NOT NULL, -- in cents
        currency VARCHAR(10) DEFAULT 'usd',
        platform_fee_amount BIGINT UNSIGNED DEFAULT 0, -- in cents
        application_fee_id VARCHAR(255) NULL,
        receipt_url TEXT NULL,
        status ENUM('created','paid','failed','refunded') DEFAULT 'created',
        fulfillment_status VARCHAR(80) DEFAULT 'not_required',
        business_flow_status VARCHAR(80) DEFAULT 'created',
        business_flow_type VARCHAR(120) DEFAULT 'custom_business_flow',
        business_flow_instance_id BIGINT UNSIGNED NULL,
        fulfillment_task_id BIGINT UNSIGNED NULL,
        fulfillment_completed_at DATETIME NULL,
        metadata LONGTEXT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        KEY idx_user (user_id),
        KEY idx_session (stripe_checkout_session_id),
        KEY idx_payment (stripe_payment_intent_id),
        KEY idx_fulfillment_status (fulfillment_status),
        KEY idx_business_flow (business_flow_instance_id)
    ) $charset_collate;";

    dbDelta($sql11);
    dbDelta($sql12);

    // === Store Catalog & Subscriptions ===
    $products_table = $wpdb->prefix . 'vmo_store_products';
    $sql13 = "CREATE TABLE IF NOT EXISTS $products_table (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        user_id BIGINT UNSIGNED NOT NULL,
        name VARCHAR(255) NOT NULL,
        slug VARCHAR(255) NOT NULL,
        description LONGTEXT NULL,
        product_type ENUM('item','subscription','downloadable','service') DEFAULT 'item',
        price_cents BIGINT UNSIGNED NOT NULL DEFAULT 0,
        currency VARCHAR(10) DEFAULT 'usd',
        sku VARCHAR(100) DEFAULT NULL,
        stock_tracking TINYINT(1) DEFAULT 0,
        stock_qty INT DEFAULT 0,
        shipping_required TINYINT(1) DEFAULT 0,
        shipping_profile LONGTEXT NULL,
        download_url TEXT NULL,
        download_limit INT DEFAULT NULL,
        recurring_interval ENUM('day','week','month','year') DEFAULT NULL,
        recurring_interval_count INT DEFAULT NULL,
        images LONGTEXT NULL,
        status ENUM('draft','active','archived') DEFAULT 'active',
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        UNIQUE KEY uniq_user_slug (user_id, slug),
        KEY idx_user (user_id),
        KEY idx_type (product_type),
        KEY idx_status (status)
    ) $charset_collate;";

    $subs_table = $wpdb->prefix . 'vmo_store_subscriptions';
    $sql14 = "CREATE TABLE IF NOT EXISTS $subs_table (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        user_id BIGINT UNSIGNED NOT NULL, -- merchant
        buyer_user_id BIGINT UNSIGNED NULL,
        product_id BIGINT UNSIGNED NOT NULL,
        stripe_subscription_id VARCHAR(255) NOT NULL,
        status VARCHAR(50) NOT NULL,
        current_period_end DATETIME NULL,
        metadata LONGTEXT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        KEY idx_user (user_id),
        KEY idx_buyer (buyer_user_id),
        KEY idx_product (product_id),
        KEY idx_sub (stripe_subscription_id)
    ) $charset_collate;";

    dbDelta($sql13);
    dbDelta($sql14);

    // ======================================
    // MISSING TABLES - ADDED FOR 100% FUNCTIONALITY
    // ======================================
    
    // PHONE SYSTEM ENABLED: Twilio AI Agent Integration
    // NOTE: vmo_phone_numbers ($sql_phone1) and vmo_phone_calls ($sql_phone2)
    // are already created above — only define warnings and knowledge_base here.
    $phone_warnings_table = $wpdb->prefix . 'vmo_phone_warnings';
    $knowledge_base_table = $wpdb->prefix . 'vmo_knowledge_base';

    $sql17 = "CREATE TABLE IF NOT EXISTS $phone_warnings_table (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        user_id BIGINT UNSIGNED NOT NULL,
        warning_type VARCHAR(50) NOT NULL,
        message TEXT NOT NULL,
        severity VARCHAR(20) DEFAULT 'info',
        is_read TINYINT(1) DEFAULT 0,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        KEY idx_user (user_id),
        KEY idx_severity (severity),
        KEY idx_read (is_read),
        KEY idx_created (created_at)
    ) $charset_collate;";

    $sql18 = "CREATE TABLE IF NOT EXISTS $knowledge_base_table (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        user_id BIGINT UNSIGNED NOT NULL,
        source_type VARCHAR(50) NOT NULL,
        content_url VARCHAR(500) DEFAULT NULL,
        content_text LONGTEXT,
        metadata LONGTEXT,
        last_updated DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        KEY idx_user (user_id),
        KEY idx_source (source_type),
        KEY idx_updated (last_updated)
    ) $charset_collate;";

    // Manager System Tables (6 tables)
    $manager_users_table = $wpdb->prefix . 'vmo_manager_users';
    $manager_activations_table = $wpdb->prefix . 'vmo_manager_activations';
    $token_transactions_table = $wpdb->prefix . 'vmo_manager_token_transactions';
    $affiliates_table = $wpdb->prefix . 'vmo_manager_affiliates';
    $commissions_table = $wpdb->prefix . 'vmo_manager_commissions';
    $support_tickets_table = $wpdb->prefix . 'vmo_manager_support_tickets';
    
    $sql19 = "CREATE TABLE IF NOT EXISTS $manager_users_table (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        wp_user_id BIGINT UNSIGNED NOT NULL,
        role VARCHAR(50) DEFAULT 'manager',
        permissions LONGTEXT,
        assigned_clients TEXT,
        status VARCHAR(20) DEFAULT 'active',
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        KEY idx_wp_user (wp_user_id),
        KEY idx_role (role),
        KEY idx_status (status)
    ) $charset_collate;";

    // ✅ Manager Activations Table - tracks user installations and token balances
    $sql19b = "CREATE TABLE IF NOT EXISTS $manager_activations_table (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        install_id VARCHAR(64) NOT NULL,
        user_id BIGINT UNSIGNED NOT NULL,
        site_url VARCHAR(500) DEFAULT NULL,
        plan_type VARCHAR(50) DEFAULT 'free',
        token_balance INT DEFAULT 0,
        tokens_used INT DEFAULT 0,
        status VARCHAR(20) DEFAULT 'active',
        activated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        expires_at DATETIME DEFAULT NULL,
        last_activity_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        metadata LONGTEXT DEFAULT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        UNIQUE KEY uniq_install (install_id),
        KEY idx_user (user_id),
        KEY idx_status (status),
        KEY idx_plan (plan_type),
        KEY idx_expires (expires_at)
    ) $charset_collate;";

    $sql20 = "CREATE TABLE IF NOT EXISTS $token_transactions_table (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        user_id BIGINT UNSIGNED NOT NULL,
        transaction_type VARCHAR(50) NOT NULL,
        tokens_amount INT NOT NULL,
        tokens_before INT DEFAULT 0,
        tokens_after INT NOT NULL,
        description TEXT,
        reference_id VARCHAR(100) DEFAULT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        KEY idx_user (user_id),
        KEY idx_type (transaction_type),
        KEY idx_created (created_at),
        KEY idx_reference (reference_id)
    ) $charset_collate;";

    $sql21 = "CREATE TABLE IF NOT EXISTS $affiliates_table (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        user_id BIGINT UNSIGNED NOT NULL,
        affiliate_code VARCHAR(50) UNIQUE NOT NULL,
        referral_link VARCHAR(500),
        total_referrals INT DEFAULT 0,
        total_revenue DECIMAL(10,2) DEFAULT 0.00,
        commission_rate DECIMAL(5,2) DEFAULT 10.00,
        status VARCHAR(20) DEFAULT 'active',
        payment_email VARCHAR(255),
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        UNIQUE KEY uniq_code (affiliate_code),
        KEY idx_user (user_id),
        KEY idx_status (status)
    ) $charset_collate;";

    $sql22 = "CREATE TABLE IF NOT EXISTS $commissions_table (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        affiliate_id BIGINT UNSIGNED NOT NULL,
        order_id BIGINT UNSIGNED NOT NULL,
        commission_amount DECIMAL(10,2) NOT NULL,
        commission_rate DECIMAL(5,2) NOT NULL,
        order_total DECIMAL(10,2) NOT NULL,
        status VARCHAR(20) DEFAULT 'pending',
        paid_at DATETIME DEFAULT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        KEY idx_affiliate (affiliate_id),
        KEY idx_order (order_id),
        KEY idx_status (status),
        KEY idx_created (created_at)
    ) $charset_collate;";

    $sql23 = "CREATE TABLE IF NOT EXISTS $support_tickets_table (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        user_id BIGINT UNSIGNED NOT NULL,
        subject VARCHAR(500) NOT NULL,
        message LONGTEXT NOT NULL,
        priority VARCHAR(20) DEFAULT 'normal',
        status VARCHAR(20) DEFAULT 'open',
        assigned_to BIGINT UNSIGNED DEFAULT NULL,
        last_reply_at DATETIME DEFAULT NULL,
        closed_at DATETIME DEFAULT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        KEY idx_user (user_id),
        KEY idx_status (status),
        KEY idx_priority (priority),
        KEY idx_assigned (assigned_to),
        KEY idx_created (created_at)
    ) $charset_collate;";

    // Email Marketing Tables (2 tables)
    $email_campaigns_table = $wpdb->prefix . 'vmo_email_campaigns';
    $email_stats_table = $wpdb->prefix . 'vmo_email_stats';
    $email_sender_identities_table = $wpdb->prefix . 'vmo_email_sender_identities';
    
    $sql24 = "CREATE TABLE IF NOT EXISTS $email_campaigns_table (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        user_id BIGINT UNSIGNED NOT NULL,
        campaign_name VARCHAR(255) NOT NULL,
        campaign_type VARCHAR(50) DEFAULT 'newsletter',
        subject VARCHAR(500) NOT NULL,
        body LONGTEXT NOT NULL,
        from_name VARCHAR(255),
        from_email VARCHAR(255),
        reply_to VARCHAR(255),
        segment_criteria LONGTEXT,
        status VARCHAR(20) DEFAULT 'draft',
        scheduled_at DATETIME DEFAULT NULL,
        sent_at DATETIME DEFAULT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        KEY idx_user (user_id),
        KEY idx_status (status),
        KEY idx_type (campaign_type),
        KEY idx_scheduled (scheduled_at)
    ) $charset_collate;";

    $sql25 = "CREATE TABLE IF NOT EXISTS $email_stats_table (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        campaign_id BIGINT UNSIGNED NOT NULL,
        contact_id BIGINT UNSIGNED NOT NULL,
        email_address VARCHAR(255) NOT NULL,
        sent TINYINT(1) DEFAULT 0,
        delivered TINYINT(1) DEFAULT 0,
        opened TINYINT(1) DEFAULT 0,
        clicked TINYINT(1) DEFAULT 0,
        bounced TINYINT(1) DEFAULT 0,
        unsubscribed TINYINT(1) DEFAULT 0,
        opened_at DATETIME DEFAULT NULL,
        clicked_at DATETIME DEFAULT NULL,
        sent_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        KEY idx_campaign (campaign_id),
        KEY idx_contact (contact_id),
        KEY idx_email (email_address),
        KEY idx_opened (opened),
        KEY idx_clicked (clicked)
    ) $charset_collate;";

    $sql25_identity = "CREATE TABLE IF NOT EXISTS $email_sender_identities_table (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        user_id BIGINT UNSIGNED NOT NULL,
        domain VARCHAR(255) NOT NULL,
        email_address VARCHAR(255) NOT NULL,
        email_role VARCHAR(50) DEFAULT 'marketing',
        display_name VARCHAR(255) DEFAULT NULL,
        verification_status VARCHAR(30) DEFAULT 'pending',
        dns_records LONGTEXT DEFAULT NULL,
        verification_requested_at DATETIME DEFAULT NULL,
        verified_at DATETIME DEFAULT NULL,
        last_checked_at DATETIME DEFAULT NULL,
        is_primary TINYINT(1) DEFAULT 0,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        UNIQUE KEY uniq_user_email (user_id, email_address),
        KEY idx_user_role (user_id, email_role),
        KEY idx_domain_status (domain, verification_status)
    ) $charset_collate;";

    // Email Signatures Table
    $email_signatures_table = $wpdb->prefix . 'vmo_email_signatures';
    $sql25_signatures = "CREATE TABLE IF NOT EXISTS $email_signatures_table (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        user_id BIGINT UNSIGNED NOT NULL,
        account VARCHAR(255) DEFAULT 'default',
        name VARCHAR(255) NOT NULL,
        designation VARCHAR(255) DEFAULT NULL,
        social_links LONGTEXT DEFAULT NULL,
        custom_links LONGTEXT DEFAULT NULL,
        logo_url VARCHAR(500) DEFAULT NULL,
        website_url VARCHAR(500) DEFAULT NULL,
        theme_color VARCHAR(20) DEFAULT '#000000',
        is_default TINYINT(1) DEFAULT 0,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        UNIQUE KEY uniq_user_account (user_id, account),
        KEY idx_user (user_id)
    ) $charset_collate;";

    // Email Automations Table
    $email_automations_table = $wpdb->prefix . 'vmo_email_automations';
    $sql25_automations = "CREATE TABLE IF NOT EXISTS $email_automations_table (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        user_id BIGINT UNSIGNED NOT NULL,
        name VARCHAR(255) NOT NULL,
        trigger_type VARCHAR(50) NOT NULL, -- 'open', 'click', 'no_open'
        trigger_campaign_id BIGINT UNSIGNED DEFAULT NULL,
        action_type VARCHAR(50) NOT NULL, -- 'send_email'
        action_payload LONGTEXT DEFAULT NULL, -- email content or template ID
        delay_minutes INT DEFAULT 0,
        status VARCHAR(20) DEFAULT 'active',
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        KEY idx_user (user_id),
        KEY idx_trigger (trigger_campaign_id)
    ) $charset_collate;";

    // Contact Groups Table
    $contact_groups_table = $wpdb->prefix . 'vmo_contact_groups';
    $sql25_groups = "CREATE TABLE IF NOT EXISTS $contact_groups_table (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        user_id BIGINT UNSIGNED NOT NULL,
        name VARCHAR(255) NOT NULL,
        description TEXT DEFAULT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        KEY idx_user (user_id)
    ) $charset_collate;";

    // API Usage Tracking Table
    $api_usage_table = $wpdb->prefix . 'vmo_manager_api_usage';
    
    $sql26 = "CREATE TABLE IF NOT EXISTS $api_usage_table (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        user_id BIGINT UNSIGNED NOT NULL,
        api_service VARCHAR(50) NOT NULL,
        api_action VARCHAR(100) NOT NULL,
        tokens_used INT DEFAULT 0,
        cost DECIMAL(10,4) DEFAULT 0.0000,
        response_time INT DEFAULT NULL,
        status VARCHAR(20),
        error_message TEXT,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        KEY idx_user (user_id),
        KEY idx_service (api_service),
        KEY idx_created (created_at),
        KEY idx_status (status)
    ) $charset_collate;";

    // Manager Emails Table
    $manager_emails_table = $wpdb->prefix . 'vmo_manager_emails';
    
    $sql27 = "CREATE TABLE IF NOT EXISTS $manager_emails_table (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        user_id BIGINT UNSIGNED NOT NULL,
        campaign_name VARCHAR(255) NOT NULL,
        subject VARCHAR(500) NOT NULL,
        body LONGTEXT NOT NULL,
        recipients LONGTEXT,
        status VARCHAR(20) DEFAULT 'draft',
        scheduled_at DATETIME DEFAULT NULL,
        sent_at DATETIME DEFAULT NULL,
        open_count INT DEFAULT 0,
        click_count INT DEFAULT 0,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        KEY idx_user (user_id),
        KEY idx_status (status),
        KEY idx_scheduled (scheduled_at)
    ) $charset_collate;";

    // Chat conversation history table for persistent conversations with AI coordinator
    $chat_history_table = $wpdb->prefix . 'vmo_chat_history';
    $sql28 = "CREATE TABLE IF NOT EXISTS $chat_history_table (
        id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
        user_id bigint(20) UNSIGNED NOT NULL,
        message_role varchar(20) NOT NULL COMMENT 'user or assistant',
        message_content longtext NOT NULL,
        conversation_context text DEFAULT NULL COMMENT 'JSON context data',
        chat_type varchar(20) DEFAULT 'jarvis' COMMENT 'jarvis|consultant|other',
        created_at datetime DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY (id),
        KEY user_id (user_id),
        KEY created_at (created_at),
        KEY conversation_lookup (user_id, created_at),
        KEY user_chattype_created (user_id, chat_type, created_at)
    ) $charset_collate;";

    // Long-term memory table
    $memory_table = $wpdb->prefix . 'vmo_long_term_memory';
    $sql29 = "CREATE TABLE IF NOT EXISTS $memory_table (
        id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
        user_id BIGINT(20) UNSIGNED NOT NULL,
        memory_type VARCHAR(50) NOT NULL COMMENT 'vision|goal|decision|constraint|milestone|fact|note',
        memory_key VARCHAR(191) DEFAULT NULL COMMENT 'stable key for upsert (optional)',
        importance TINYINT(3) UNSIGNED DEFAULT 0 COMMENT '0-10 subjective importance',
        content LONGTEXT NOT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        PRIMARY KEY (id),
        KEY idx_user (user_id),
        KEY idx_type (memory_type),
        UNIQUE KEY uniq_user_key (user_id, memory_key)
    ) $charset_collate;";

    // Roadmap milestones table — expanded schema for vision-driven milestone tracking
    $milestones_table = $wpdb->prefix . 'vmo_roadmap_milestones';
    $sql30 = "CREATE TABLE IF NOT EXISTS $milestones_table (
        id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
        milestone_id VARCHAR(50) DEFAULT NULL,
        roadmap_id VARCHAR(50) DEFAULT NULL,
        user_id BIGINT(20) UNSIGNED NOT NULL,
        session_id VARCHAR(100) DEFAULT NULL,
        phase_number TINYINT(2) DEFAULT 1,
        phase_name VARCHAR(100) DEFAULT 'Phase 1',
        title VARCHAR(255) NOT NULL,
        milestone_title VARCHAR(255) DEFAULT NULL,
        milestone_description TEXT,
        target_date DATE DEFAULT NULL,
        target_day INT UNSIGNED DEFAULT NULL COMMENT 'Day number from roadmap start',
        owner VARCHAR(100) DEFAULT 'jarvis',
        status VARCHAR(20) DEFAULT 'planned' COMMENT 'planned|in_progress|blocked|done|completed',
        progress TINYINT(3) UNSIGNED DEFAULT 0 COMMENT '0-100',
        progress_percentage DECIMAL(5,2) DEFAULT 0.00,
        success_criteria LONGTEXT COMMENT 'JSON array of criteria',
        kpi_targets LONGTEXT COMMENT 'JSON array of KPI targets',
        dependent_on_milestone_id VARCHAR(50) DEFAULT NULL,
        priority TINYINT(2) DEFAULT 3 COMMENT '1=critical, 5=low',
        vision_alignment TEXT COMMENT 'How this milestone aligns with the business vision',
        notes LONGTEXT,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        PRIMARY KEY (id),
        KEY idx_user (user_id),
        KEY idx_status (status),
        KEY idx_target (target_date),
        KEY idx_roadmap (roadmap_id),
        KEY idx_milestone (milestone_id),
        KEY idx_session (session_id)
    ) $charset_collate;";

    // Progress summaries table
    $summaries_table = $wpdb->prefix . 'vmo_progress_summaries';
    $sql31 = "CREATE TABLE IF NOT EXISTS $summaries_table (
        id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
        user_id BIGINT(20) UNSIGNED NOT NULL,
        period VARCHAR(20) NOT NULL COMMENT 'daily|weekly',
        period_date DATE NOT NULL,
        summary LONGTEXT NOT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        PRIMARY KEY (id),
        UNIQUE KEY uniq_user_period (user_id, period, period_date),
        KEY idx_user (user_id)
    ) $charset_collate;";

    // ═══════════════════════════════════════════════════════════════════════════════
    // VMO ELITE CONSULTATION SYSTEM TABLES - Structured Phase Outputs & Progress
    // ═══════════════════════════════════════════════════════════════════════════════
    
    // Phase Outputs Table - Stores structured JSON outputs per consultation phase
    // vision_profile, business_structure, market_inputs, market_research, execution_plan, etc.
    $phase_outputs_table = $wpdb->prefix . 'vmo_phase_outputs';
    $sql32 = "CREATE TABLE IF NOT EXISTS $phase_outputs_table (
        id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
        user_id BIGINT(20) UNSIGNED NOT NULL,
        session_id VARCHAR(100) NOT NULL,
        phase_number TINYINT(2) NOT NULL COMMENT '1-8 consultation phases',
        output_type VARCHAR(50) NOT NULL COMMENT 'vision_profile|business_structure|market_inputs|market_research|execution_plan|brand_assets|legal_pack|payments_infra|build_artifacts|kpi_model',
        output_version INT UNSIGNED DEFAULT 1 COMMENT 'Version number for this output type',
        output_json LONGTEXT NOT NULL COMMENT 'Structured JSON data for this phase output',
        is_active TINYINT(1) DEFAULT 1 COMMENT 'Current active version',
        is_approved TINYINT(1) DEFAULT 0 COMMENT 'User has approved this output',
        approved_at DATETIME DEFAULT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        PRIMARY KEY (id),
        KEY idx_user_session (user_id, session_id),
        KEY idx_phase (phase_number),
        KEY idx_type (output_type),
        KEY idx_active (is_active),
        UNIQUE KEY uniq_session_phase_type_version (session_id, phase_number, output_type, output_version)
    ) $charset_collate;";

    // Business Execution Roadmap Table - Activity-based, non-time-bound master roadmap
    $roadmap_table = $wpdb->prefix . 'vmo_business_roadmaps';
    $sql33 = "CREATE TABLE IF NOT EXISTS $roadmap_table (
        id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
        roadmap_id VARCHAR(50) NOT NULL,
        user_id BIGINT(20) UNSIGNED NOT NULL,
        session_id VARCHAR(100) NOT NULL,
        business_vision TEXT NOT NULL COMMENT 'Ultimate business vision/goal',
        roadmap_version INT UNSIGNED DEFAULT 1,
        roadmap_data LONGTEXT NOT NULL COMMENT 'Full roadmap JSON with phases, milestones, dependencies',
        total_milestones INT UNSIGNED DEFAULT 0,
        total_activities INT UNSIGNED DEFAULT 0,
        total_duration_days INT UNSIGNED DEFAULT 0 COMMENT 'Estimated total days to vision',
        roadmap_start_date DATE NOT NULL,
        vision_target_date DATE DEFAULT NULL COMMENT 'Expected date to achieve vision',
        current_day_number INT UNSIGNED DEFAULT 1 COMMENT 'Current day since start',
        status VARCHAR(20) DEFAULT 'active' COMMENT 'active|paused|completed|archived',
        last_deep_research_date DATETIME DEFAULT NULL,
        next_deep_research_date DATETIME DEFAULT NULL,
        research_findings_count INT UNSIGNED DEFAULT 0,
        is_active TINYINT(1) DEFAULT 1,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        PRIMARY KEY (id),
        UNIQUE KEY uniq_roadmap_id (roadmap_id),
        KEY idx_user (user_id),
        KEY idx_session (session_id),
        KEY idx_status (status)
    ) $charset_collate;";

    // Daily Activities Table - Time-bound activities derived from roadmap
    $daily_activities_table = $wpdb->prefix . 'vmo_daily_activities';
    $sql34 = "CREATE TABLE IF NOT EXISTS $daily_activities_table (
        id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
        activity_id VARCHAR(50) NOT NULL,
        roadmap_id VARCHAR(50) NOT NULL,
        milestone_id VARCHAR(50) DEFAULT NULL COMMENT 'Links to parent milestone',
        user_id BIGINT(20) UNSIGNED NOT NULL,
        day_number INT UNSIGNED NOT NULL COMMENT 'Day N from roadmap start',
        activity_date DATE NOT NULL,
        activity_title VARCHAR(255) NOT NULL,
        activity_description TEXT,
        activity_type VARCHAR(30) DEFAULT 'task' COMMENT 'task|ai_executed|user_approval|user_action|milestone_check',
        estimated_duration_minutes INT UNSIGNED DEFAULT 60,
        priority TINYINT(2) DEFAULT 3 COMMENT '1=critical, 5=low',
        status VARCHAR(20) DEFAULT 'pending' COMMENT 'pending|in_progress|completed|delayed|skipped',
        expected_complete_by DATETIME DEFAULT NULL COMMENT 'When this should be done',
        actual_completed_at DATETIME DEFAULT NULL,
        completion_notes TEXT,
        ai_generated TINYINT(1) DEFAULT 1,
        recurring TINYINT(1) DEFAULT 0,
        recurrence_pattern VARCHAR(50) DEFAULT NULL COMMENT 'daily|weekly|monthly|custom',
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        PRIMARY KEY (id),
        UNIQUE KEY uniq_activity_id (activity_id),
        KEY idx_roadmap (roadmap_id),
        KEY idx_user (user_id),
        KEY idx_day (day_number),
        KEY idx_date (activity_date),
        KEY idx_status (status),
        KEY idx_milestone (milestone_id)
    ) $charset_collate;";

    // Progress Tracking Table - Expected vs Actual progress calculation
    $progress_tracking_table = $wpdb->prefix . 'vmo_progress_tracking';
    $sql35 = "CREATE TABLE IF NOT EXISTS $progress_tracking_table (
        id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
        user_id BIGINT(20) UNSIGNED NOT NULL,
        roadmap_id VARCHAR(50) NOT NULL,
        tracking_date DATE NOT NULL,
        day_number INT UNSIGNED NOT NULL COMMENT 'Day N from start',
        expected_activities INT UNSIGNED DEFAULT 0 COMMENT 'Activities expected complete by this day',
        actual_completed INT UNSIGNED DEFAULT 0 COMMENT 'Actually completed',
        expected_milestones INT UNSIGNED DEFAULT 0,
        actual_milestones INT UNSIGNED DEFAULT 0,
        progress_percentage DECIMAL(5,2) DEFAULT 0.00 COMMENT 'Overall % complete',
        expected_percentage DECIMAL(5,2) DEFAULT 0.00 COMMENT 'Expected % by this day',
        schedule_health VARCHAR(20) DEFAULT 'on_track' COMMENT 'ahead|on_track|behind|critical',
        days_ahead_behind INT DEFAULT 0 COMMENT 'Positive=ahead, Negative=behind',
        weekly_velocity DECIMAL(5,2) DEFAULT 0.00 COMMENT 'Activities per week average',
        ai_recommendations TEXT COMMENT 'AI suggestions based on progress',
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY (id),
        KEY idx_user_roadmap (user_id, roadmap_id),
        KEY idx_date (tracking_date),
        UNIQUE KEY uniq_roadmap_day (roadmap_id, day_number)
    ) $charset_collate;";

    // KPI Table - Enhanced business KPIs with targets
    $business_kpis_table = $wpdb->prefix . 'vmo_business_kpis';
    $sql36 = "CREATE TABLE IF NOT EXISTS $business_kpis_table (
        id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
        kpi_id VARCHAR(50) NOT NULL,
        roadmap_id VARCHAR(50) NOT NULL,
        user_id BIGINT(20) UNSIGNED NOT NULL,
        kpi_category VARCHAR(50) NOT NULL COMMENT 'revenue|traffic|leads|conversions|seo|operations',
        kpi_name VARCHAR(100) NOT NULL,
        kpi_description TEXT,
        kpi_unit VARCHAR(30) DEFAULT 'number' COMMENT 'number|currency|percentage|time',
        current_value DECIMAL(15,2) DEFAULT 0.00,
        baseline_value DECIMAL(15,2) DEFAULT 0.00,
        target_value DECIMAL(15,2) DEFAULT NULL,
        target_date DATE DEFAULT NULL,
        measurement_frequency VARCHAR(20) DEFAULT 'daily' COMMENT 'daily|weekly|monthly',
        last_measured_at DATETIME DEFAULT NULL,
        trend VARCHAR(20) DEFAULT 'stable' COMMENT 'up|down|stable',
        trend_percentage DECIMAL(5,2) DEFAULT 0.00,
        status VARCHAR(20) DEFAULT 'on_track' COMMENT 'exceeding|on_track|at_risk|behind',
        is_critical TINYINT(1) DEFAULT 0,
        display_on_dashboard TINYINT(1) DEFAULT 1,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        PRIMARY KEY (id),
        UNIQUE KEY uniq_kpi_id (kpi_id),
        KEY idx_roadmap (roadmap_id),
        KEY idx_user (user_id),
        KEY idx_category (kpi_category)
    ) $charset_collate;";

    // KPI History Table - Historical measurements for tracking
    $kpi_history_table = $wpdb->prefix . 'vmo_kpi_history';
    $sql37 = "CREATE TABLE IF NOT EXISTS $kpi_history_table (
        id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
        kpi_id VARCHAR(50) NOT NULL,
        user_id BIGINT(20) UNSIGNED NOT NULL,
        measured_value DECIMAL(15,2) NOT NULL,
        measurement_date DATE NOT NULL,
        measurement_time DATETIME NOT NULL,
        data_source VARCHAR(50) DEFAULT 'system' COMMENT 'system|manual|api|weekly_scan',
        notes TEXT,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY (id),
        KEY idx_kpi (kpi_id),
        KEY idx_user (user_id),
        KEY idx_date (measurement_date)
    ) $charset_collate;";

    // Daily Reports Table — Records what ACTUALLY happened each day vs planned activities
    // Contrasted against vmo_daily_activities to measure real progress
    $daily_reports_table = $wpdb->prefix . 'vmo_daily_reports';
    $sql38 = "CREATE TABLE IF NOT EXISTS $daily_reports_table (
        id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
        report_id VARCHAR(50) NOT NULL,
        roadmap_id VARCHAR(50) NOT NULL,
        user_id BIGINT(20) UNSIGNED NOT NULL,
        session_id VARCHAR(100) DEFAULT NULL,
        day_number INT UNSIGNED NOT NULL COMMENT 'Day N from roadmap start',
        report_date DATE NOT NULL,
        planned_activities_count INT UNSIGNED DEFAULT 0 COMMENT 'How many activities were planned for this day',
        completed_activities_count INT UNSIGNED DEFAULT 0 COMMENT 'How many were actually completed',
        completed_activity_ids LONGTEXT COMMENT 'JSON array of completed activity_ids',
        uncompleted_activity_ids LONGTEXT COMMENT 'JSON array of uncompleted activity_ids',
        carried_forward_ids LONGTEXT COMMENT 'JSON array of activity_ids pushed to next day',
        user_notes TEXT COMMENT 'User-entered notes about the day',
        blockers TEXT COMMENT 'What blocked progress',
        wins TEXT COMMENT 'What went well',
        ai_assessment TEXT COMMENT 'AI analysis of the day',
        progress_delta DECIMAL(5,2) DEFAULT 0.00 COMMENT 'Progress change this day (percentage points)',
        estimated_completion_adjustment INT DEFAULT 0 COMMENT 'Days added(+) or saved(-) to vision estimate',
        cumulative_adjustment INT DEFAULT 0 COMMENT 'Total days added/saved since start',
        day_score TINYINT(3) UNSIGNED DEFAULT 0 COMMENT '0-100 day effectiveness score',
        streak_count INT UNSIGNED DEFAULT 0 COMMENT 'Consecutive days of full completion',
        submitted_at DATETIME DEFAULT NULL COMMENT 'When user submitted the report',
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        PRIMARY KEY (id),
        UNIQUE KEY uniq_report_id (report_id),
        KEY idx_roadmap (roadmap_id),
        KEY idx_user (user_id),
        KEY idx_date (report_date),
        KEY idx_day (day_number),
        UNIQUE KEY uniq_roadmap_day (roadmap_id, day_number)
    ) $charset_collate;";

    // Deep Research Results Table — Stores findings from background scans
    $deep_research_table = $wpdb->prefix . 'vmo_deep_research_results';
    $sql39 = "CREATE TABLE IF NOT EXISTS $deep_research_table (
        id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
        research_id VARCHAR(50) NOT NULL,
        roadmap_id VARCHAR(50) NOT NULL,
        user_id BIGINT(20) UNSIGNED NOT NULL,
        research_date DATETIME NOT NULL,
        research_type VARCHAR(50) DEFAULT 'weekly_scan' COMMENT 'weekly_scan|competitor_alert|market_shift|opportunity',
        research_prompt TEXT,
        research_results LONGTEXT COMMENT 'Full JSON research output',
        key_findings TEXT COMMENT 'Executive summary',
        action_items LONGTEXT COMMENT 'JSON array of recommended actions',
        kpi_impacts LONGTEXT COMMENT 'JSON array of KPI assessment',
        roadmap_updates_suggested LONGTEXT COMMENT 'JSON array of roadmap adjustments',
        applied_to_roadmap TINYINT(1) DEFAULT 0 COMMENT 'Whether user approved and applied changes',
        applied_at DATETIME DEFAULT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY (id),
        UNIQUE KEY uniq_research_id (research_id),
        KEY idx_roadmap (roadmap_id),
        KEY idx_user (user_id),
        KEY idx_date (research_date),
        KEY idx_applied (applied_to_roadmap)
    ) $charset_collate;";

    // Execute all new table creation
    // PHONE SYSTEM: phone_numbers & phone_calls already created via $sql_phone1/$sql_phone2 above
    dbDelta($sql17); // phone_warnings
    dbDelta($sql18); // knowledge_base
    dbDelta($sql19); // manager_users
    dbDelta($sql19b); // manager_activations (install_id, token_balance)
    dbDelta($sql20); // token_transactions
    dbDelta($sql21); // affiliates
    dbDelta($sql22); // commissions
    dbDelta($sql23); // support_tickets
    dbDelta($sql24); // email_campaigns
    dbDelta($sql25); // email_stats
    dbDelta($sql25_identity); // email sender identities
    dbDelta($sql25_signatures); // email signatures
    dbDelta($sql25_automations); // email automations
    dbDelta($sql25_groups); // contact groups
    dbDelta($sql26); // api_usage
    dbDelta($sql27); // manager_emails
    dbDelta($sql28); // chat_history
    dbDelta($sql29); // long_term_memory
    dbDelta($sql30); // roadmap_milestones
    dbDelta($sql31); // progress_summaries
    
    // VMO Elite Consultation System Tables
    dbDelta($sql32); // phase_outputs - structured outputs per phase
    dbDelta($sql33); // business_roadmaps - activity-based master roadmap
    dbDelta($sql34); // daily_activities - time-bound activities from roadmap
    dbDelta($sql35); // progress_tracking - expected vs actual progress
    dbDelta($sql36); // business_kpis - enhanced KPI tracking
    dbDelta($sql37); // kpi_history - historical KPI measurements
    dbDelta($sql38); // daily_reports - daily outcome tracking vs planned activities
    dbDelta($sql39); // deep_research_results - background scan findings

    // ======================================
    // AI PHONE SYSTEM — POST-CALL INTELLIGENCE TABLES
    // ======================================

    // Call Reports — Structured post-call report for every AI-handled call
    $call_reports_table = $wpdb->prefix . 'vmo_call_reports';
    $sql_cr = "CREATE TABLE IF NOT EXISTS $call_reports_table (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        user_id BIGINT UNSIGNED NOT NULL,
        call_sid VARCHAR(255) NOT NULL,
        contact_id BIGINT UNSIGNED DEFAULT NULL,
        report_type ENUM('inbound','outbound','missed') DEFAULT 'inbound',
        caller_name VARCHAR(200) DEFAULT NULL,
        caller_phone VARCHAR(50) DEFAULT NULL,
        caller_email VARCHAR(255) DEFAULT NULL,
        caller_intent VARCHAR(255) DEFAULT NULL,
        call_sentiment ENUM('positive','neutral','negative','frustrated') DEFAULT 'neutral',
        urgency ENUM('low','medium','high','critical') DEFAULT 'medium',
        crm_category ENUM('lead','customer','support','inquiry','vendor','spam') DEFAULT 'lead',
        summary_text LONGTEXT DEFAULT NULL,
        actions_taken LONGTEXT DEFAULT NULL COMMENT 'JSON: what the AI handled during the call',
        actions_required LONGTEXT DEFAULT NULL COMMENT 'JSON: what the user needs to do',
        actions_recommended LONGTEXT DEFAULT NULL COMMENT 'JSON: optional improvements',
        promises_made LONGTEXT DEFAULT NULL COMMENT 'JSON: what AI promised to send/do',
        fulfillment_status ENUM('none','pending','partial','complete','failed') DEFAULT 'none',
        fulfillment_log LONGTEXT DEFAULT NULL COMMENT 'JSON: log of fulfillment attempts',
        products_discussed LONGTEXT DEFAULT NULL COMMENT 'JSON: products/services discussed',
        recording_url VARCHAR(2083) DEFAULT NULL,
        call_duration INT UNSIGNED DEFAULT 0,
        ai_confidence TINYINT UNSIGNED DEFAULT 80 COMMENT 'AI confidence in report accuracy 0-100',
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        INDEX idx_user_id (user_id),
        INDEX idx_call_sid (call_sid),
        INDEX idx_contact (contact_id),
        INDEX idx_crm_cat (crm_category),
        INDEX idx_fulfillment (fulfillment_status),
        INDEX idx_created (created_at)
    ) $charset_collate;";

    // Action Items — Checklists with deadlines for user follow-up
    $action_items_table = $wpdb->prefix . 'vmo_action_items';
    $sql_ai = "CREATE TABLE IF NOT EXISTS $action_items_table (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        user_id BIGINT UNSIGNED NOT NULL,
        call_sid VARCHAR(255) DEFAULT NULL,
        report_id BIGINT UNSIGNED DEFAULT NULL,
        contact_id BIGINT UNSIGNED DEFAULT NULL,
        title VARCHAR(500) NOT NULL,
        description TEXT DEFAULT NULL,
        priority ENUM('low','medium','high','critical') DEFAULT 'medium',
        category ENUM('follow_up','send_info','callback','order','booking','support','custom') DEFAULT 'follow_up',
        deadline DATETIME DEFAULT NULL,
        status ENUM('pending','in_progress','completed','overdue','cancelled') DEFAULT 'pending',
        completed_at DATETIME DEFAULT NULL,
        completed_by BIGINT UNSIGNED DEFAULT NULL,
        notes TEXT DEFAULT NULL,
        auto_generated TINYINT(1) DEFAULT 1,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        INDEX idx_user_id (user_id),
        INDEX idx_call_sid (call_sid),
        INDEX idx_report (report_id),
        INDEX idx_status (status),
        INDEX idx_deadline (deadline),
        INDEX idx_priority (priority),
        INDEX idx_created (created_at)
    ) $charset_collate;";

    // Fulfillment Actions Log — Tracks SMS/email fulfillment attempts
    $fulfillment_log_table = $wpdb->prefix . 'vmo_fulfillment_log';
    $sql_fl = "CREATE TABLE IF NOT EXISTS $fulfillment_log_table (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        user_id BIGINT UNSIGNED NOT NULL,
        call_sid VARCHAR(255) NOT NULL,
        report_id BIGINT UNSIGNED DEFAULT NULL,
        action_type ENUM('sms','email','both') NOT NULL,
        recipient_phone VARCHAR(50) DEFAULT NULL,
        recipient_email VARCHAR(255) DEFAULT NULL,
        subject VARCHAR(500) DEFAULT NULL,
        message_body LONGTEXT NOT NULL,
        data_sources LONGTEXT DEFAULT NULL COMMENT 'JSON: what data was used to compose message',
        status ENUM('sent','failed','skipped') DEFAULT 'sent',
        error_message TEXT DEFAULT NULL,
        twilio_message_sid VARCHAR(255) DEFAULT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        INDEX idx_user_id (user_id),
        INDEX idx_call_sid (call_sid),
        INDEX idx_status (status),
        INDEX idx_created (created_at)
    ) $charset_collate;";

    // Daily/Periodic Reports — Aggregated business activity reports
    $periodic_reports_table = $wpdb->prefix . 'vmo_periodic_reports';
    $sql_pr = "CREATE TABLE IF NOT EXISTS $periodic_reports_table (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        user_id BIGINT UNSIGNED NOT NULL,
        report_period ENUM('daily','weekly','monthly','quarterly','yearly','custom') NOT NULL,
        period_start DATE NOT NULL,
        period_end DATE NOT NULL,
        fiscal_year_start DATE DEFAULT NULL COMMENT 'User-configurable fiscal year',
        cover_page LONGTEXT DEFAULT NULL COMMENT 'JSON: executive summary for cover page',
        summary_data LONGTEXT NOT NULL COMMENT 'JSON: aggregated metrics and KPIs',
        detail_data LONGTEXT DEFAULT NULL COMMENT 'JSON: per-call breakdown, charts data',
        roadmap_comparison LONGTEXT DEFAULT NULL COMMENT 'JSON: expected vs actual from roadmap',
        total_calls INT UNSIGNED DEFAULT 0,
        total_leads INT UNSIGNED DEFAULT 0,
        total_customers INT UNSIGNED DEFAULT 0,
        total_action_items INT UNSIGNED DEFAULT 0,
        action_items_completed INT UNSIGNED DEFAULT 0,
        fulfillments_sent INT UNSIGNED DEFAULT 0,
        ai_insights LONGTEXT DEFAULT NULL COMMENT 'GPT-generated executive insights',
        generated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        INDEX idx_user_id (user_id),
        INDEX idx_period (report_period),
        INDEX idx_dates (period_start, period_end),
        INDEX idx_generated (generated_at),
        UNIQUE KEY uniq_user_period (user_id, report_period, period_start, period_end)
    ) $charset_collate;";

    dbDelta($sql_cr);  // call_reports
    dbDelta($sql_ai);  // action_items
    dbDelta($sql_fl);  // fulfillment_log
    dbDelta($sql_pr);  // periodic_reports

    // ======================================
    // OPERATIONAL TABLES — Actions, Activity Log, Approvals
    // Used by Today tab (Actions sub-tab), Strategic Dashboard, Reports routing
    // ======================================

    $actions_table = $wpdb->prefix . 'vmo_actions';
    $sql_actions = "CREATE TABLE IF NOT EXISTS $actions_table (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        user_id BIGINT UNSIGNED NOT NULL,
        title VARCHAR(500) NOT NULL,
        description TEXT DEFAULT NULL,
        source VARCHAR(60) DEFAULT 'manual' COMMENT 'manual|phone_report|email_report|sms_report|form|crm|strategic|seo|shipping',
        source_id BIGINT UNSIGNED DEFAULT NULL COMMENT 'ID from source system (call_report id, email id, etc)',
        priority ENUM('urgent','high','normal','low') DEFAULT 'normal',
        category VARCHAR(60) DEFAULT 'general' COMMENT 'sales|support|operations|content|finance|marketing|compliance|follow-up|general',
        status ENUM('pending','in_progress','completed','deferred') DEFAULT 'pending',
        due_date DATE DEFAULT NULL,
        completed_at DATETIME DEFAULT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        KEY idx_user_id (user_id),
        KEY idx_status (status),
        KEY idx_priority (priority),
        KEY idx_source (source),
        KEY idx_due_date (due_date),
        KEY idx_created (created_at)
    ) $charset_collate;";

    $activity_log_table = $wpdb->prefix . 'vmo_activity_log';
    $sql_activity_log = "CREATE TABLE IF NOT EXISTS $activity_log_table (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        user_id BIGINT UNSIGNED NOT NULL,
        action_type VARCHAR(100) NOT NULL COMMENT 'task_completed|action_created|report_generated|seo_scan|competitor_alert|approval_requested|system_event',
        description TEXT NOT NULL,
        source_system VARCHAR(60) DEFAULT 'system' COMMENT 'today|reports|seo|crm|phone|email|sms|strategic|cfo|shipping|ci',
        related_id BIGINT UNSIGNED DEFAULT NULL,
        metadata LONGTEXT DEFAULT NULL COMMENT 'JSON: additional context',
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        KEY idx_user_id (user_id),
        KEY idx_action_type (action_type),
        KEY idx_source (source_system),
        KEY idx_created (created_at)
    ) $charset_collate;";

    $approvals_table = $wpdb->prefix . 'vmo_approvals';
    $sql_approvals = "CREATE TABLE IF NOT EXISTS $approvals_table (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        user_id BIGINT UNSIGNED NOT NULL,
        title VARCHAR(500) NOT NULL,
        description TEXT DEFAULT NULL,
        approval_type VARCHAR(60) DEFAULT 'general' COMMENT 'seo_change|website_change|pricing_change|campaign_launch|roadmap_update|system_activation|content_publish',
        source_system VARCHAR(60) DEFAULT 'system',
        payload LONGTEXT DEFAULT NULL COMMENT 'JSON: data needed to execute if approved',
        status ENUM('pending','approved','rejected','deferred') DEFAULT 'pending',
        decided_at DATETIME DEFAULT NULL,
        decision_notes TEXT DEFAULT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        KEY idx_user_id (user_id),
        KEY idx_status (status),
        KEY idx_type (approval_type),
        KEY idx_created (created_at)
    ) $charset_collate;";

    $token_usage_table = $wpdb->prefix . 'vmo_token_usage';
    $sql_token_usage = "CREATE TABLE IF NOT EXISTS $token_usage_table (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        user_id BIGINT UNSIGNED NOT NULL,
        feature_key VARCHAR(120) DEFAULT NULL,
        system_key VARCHAR(80) DEFAULT NULL,
        action VARCHAR(160) DEFAULT NULL,
        usage_type VARCHAR(120) DEFAULT NULL,
        feature VARCHAR(120) DEFAULT NULL,
        tokens_used INT UNSIGNED DEFAULT 0,
        actual_cost_usd DECIMAL(12,4) DEFAULT 0.0000,
        user_facing_value_usd DECIMAL(12,4) DEFAULT 0.0000,
        markup_multiplier DECIMAL(8,2) DEFAULT 0.00,
        description TEXT DEFAULT NULL,
        metadata LONGTEXT DEFAULT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        KEY idx_user_id (user_id),
        KEY idx_system_key (system_key),
        KEY idx_feature_key (feature_key),
        KEY idx_created_at (created_at)
    ) $charset_collate;";

    $usage_tracking_table = $wpdb->prefix . 'vmo_usage_tracking';
    $sql_usage_tracking = "CREATE TABLE IF NOT EXISTS $usage_tracking_table (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        user_id BIGINT UNSIGNED NOT NULL,
        feature_type VARCHAR(120) NOT NULL,
        action VARCHAR(160) DEFAULT NULL,
        tokens_used INT UNSIGNED DEFAULT 0,
        metadata LONGTEXT DEFAULT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        KEY idx_user_id (user_id),
        KEY idx_feature_type (feature_type),
        KEY idx_created_at (created_at)
    ) $charset_collate;";

    $financial_events_table = $wpdb->prefix . 'vmo_financial_events';
    $sql_financial_events = "CREATE TABLE IF NOT EXISTS $financial_events_table (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        user_id BIGINT UNSIGNED NOT NULL,
        event_type VARCHAR(80) NOT NULL DEFAULT 'transaction',
        source VARCHAR(80) NOT NULL DEFAULT 'manual_entry',
        source_reference VARCHAR(191) DEFAULT NULL,
        direction VARCHAR(30) DEFAULT 'income',
        amount DECIMAL(14,2) DEFAULT 0.00,
        currency VARCHAR(10) DEFAULT 'USD',
        payment_status VARCHAR(60) DEFAULT 'pending',
        event_status VARCHAR(60) DEFAULT 'needs_review',
        confidence DECIMAL(5,2) DEFAULT 0.00,
        customer_name VARCHAR(255) DEFAULT NULL,
        customer_email VARCHAR(255) DEFAULT NULL,
        customer_phone VARCHAR(80) DEFAULT NULL,
        vendor_name VARCHAR(255) DEFAULT NULL,
        payment_method VARCHAR(80) DEFAULT NULL,
        category VARCHAR(120) DEFAULT NULL,
        product_service VARCHAR(255) DEFAULT NULL,
        transaction_date DATETIME DEFAULT NULL,
        tax_amount DECIMAL(14,2) DEFAULT 0.00,
        fees_amount DECIMAL(14,2) DEFAULT 0.00,
        contact_id VARCHAR(191) DEFAULT NULL,
        crm_contact_db_id BIGINT UNSIGNED DEFAULT NULL,
        invoice_id BIGINT UNSIGNED DEFAULT NULL,
        purchase_id BIGINT UNSIGNED DEFAULT NULL,
        recommended_action VARCHAR(120) DEFAULT NULL,
        missing_fields TEXT DEFAULT NULL,
        notes TEXT DEFAULT NULL,
        metadata LONGTEXT DEFAULT NULL,
        decided_at DATETIME DEFAULT NULL,
        posted_at DATETIME DEFAULT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        KEY idx_user_id (user_id),
        KEY idx_status (event_status),
        KEY idx_source (source),
        KEY idx_reference (source_reference),
        KEY idx_created_at (created_at)
    ) $charset_collate;";

    $financial_ledger_table = $wpdb->prefix . 'vmo_financial_ledger';
    $sql_financial_ledger = "CREATE TABLE IF NOT EXISTS $financial_ledger_table (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        user_id BIGINT UNSIGNED NOT NULL,
        event_id BIGINT UNSIGNED DEFAULT NULL,
        source VARCHAR(80) DEFAULT 'manual_entry',
        source_reference VARCHAR(191) DEFAULT NULL,
        transaction_type VARCHAR(80) DEFAULT 'income',
        direction VARCHAR(30) DEFAULT 'income',
        amount DECIMAL(14,2) DEFAULT 0.00,
        tax_amount DECIMAL(14,2) DEFAULT 0.00,
        fees_amount DECIMAL(14,2) DEFAULT 0.00,
        net_amount DECIMAL(14,2) DEFAULT 0.00,
        currency VARCHAR(10) DEFAULT 'USD',
        status VARCHAR(60) DEFAULT 'posted',
        category VARCHAR(120) DEFAULT NULL,
        customer_vendor VARCHAR(255) DEFAULT NULL,
        contact_id VARCHAR(191) DEFAULT NULL,
        crm_contact_db_id BIGINT UNSIGNED DEFAULT NULL,
        invoice_id BIGINT UNSIGNED DEFAULT NULL,
        purchase_id BIGINT UNSIGNED DEFAULT NULL,
        transaction_id VARCHAR(191) DEFAULT NULL,
        product_service VARCHAR(255) DEFAULT NULL,
        payment_method VARCHAR(80) DEFAULT NULL,
        transaction_date DATETIME DEFAULT NULL,
        metadata LONGTEXT DEFAULT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        UNIQUE KEY uniq_event (event_id),
        KEY idx_user_id (user_id),
        KEY idx_direction (direction),
        KEY idx_status (status),
        KEY idx_transaction_date (transaction_date),
        KEY idx_source_reference (source_reference)
    ) $charset_collate;";

    dbDelta($sql_actions);       // vmo_actions — Today tab Actions sub-tab
    dbDelta($sql_activity_log);  // vmo_activity_log — Strategic Dashboard recent changes
    dbDelta($sql_approvals);     // vmo_approvals — Strategic Dashboard approval queue
    dbDelta($sql_token_usage);   // vmo_token_usage — canonical token ledger for CFO reporting
    dbDelta($sql_usage_tracking); // vmo_usage_tracking — legacy category usage compatibility
    dbDelta($sql_financial_events); // vmo_financial_events — AI financial inbox
    dbDelta($sql_financial_ledger); // vmo_financial_ledger — approved/posted transaction ledger

    // ======================================
    // LICENSING & USAGE TRACKING TABLES
    // Required by class-vmo-master-client-manager.php for all client-facing endpoints
    // ======================================

    $licenses_table = $wpdb->prefix . 'vmo_licenses';
    $sql_licenses = "CREATE TABLE IF NOT EXISTS $licenses_table (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        user_id BIGINT UNSIGNED NOT NULL,
        license_key VARCHAR(64) NOT NULL,
        status ENUM('active','suspended','expired','revoked') DEFAULT 'active',
        plan VARCHAR(50) NOT NULL DEFAULT 'starter',
        source ENUM('quicklaunch','direct','manual') DEFAULT 'direct' COMMENT 'How this license was created: quicklaunch=Vultr-hosted, direct=self-hosted download, manual=admin-created',
        activations LONGTEXT DEFAULT NULL COMMENT 'JSON: array of {installation_id, site_url, site_name, activated_at}',
        max_activations INT UNSIGNED DEFAULT 1,
        site_stats LONGTEXT DEFAULT NULL COMMENT 'JSON: last synced site statistics',
        current_credit_balance BIGINT NOT NULL DEFAULT 0 COMMENT 'Authoritative master-side VMO Token balance for this license',
        lifetime_credits_purchased BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Cumulative tokens ever added (plan grants + top-ups)',
        lifetime_credits_consumed BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Cumulative tokens ever deducted',
        last_sync DATETIME DEFAULT NULL,
        expires_at DATETIME DEFAULT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        UNIQUE KEY uniq_license_key (license_key),
        KEY idx_user_id (user_id),
        KEY idx_status (status),
        KEY idx_plan (plan),
        KEY idx_credit_balance (current_credit_balance)
    ) $charset_collate;";

    $usage_ledger_table = $wpdb->prefix . 'vmo_usage_ledger';
    $sql_usage_ledger = "CREATE TABLE IF NOT EXISTS $usage_ledger_table (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        license_id BIGINT UNSIGNED NOT NULL,
        user_id BIGINT UNSIGNED NOT NULL,
        installation_id VARCHAR(64) DEFAULT NULL,
        direction ENUM('credit','debit') NOT NULL COMMENT 'credit=tokens added, debit=tokens consumed',
        amount_tokens BIGINT UNSIGNED NOT NULL DEFAULT 0,
        operation_key VARCHAR(80) DEFAULT NULL COMMENT 'Catalog operation key (debit) or grant/topup source (credit)',
        provider VARCHAR(40) DEFAULT NULL COMMENT 'openai|twilio|stripe|plan_grant|topup|refund|adjustment',
        provider_cost_usd DECIMAL(10,4) DEFAULT NULL COMMENT 'Underlying provider cost in USD (debit) or revenue (credit)',
        user_charge_usd DECIMAL(10,4) DEFAULT NULL COMMENT 'User-facing dollar value after markup',
        balance_after BIGINT NOT NULL DEFAULT 0 COMMENT 'current_credit_balance after this row was applied',
        request_id VARCHAR(64) DEFAULT NULL COMMENT 'Master proxy correlation id / Stripe charge id / idempotency key',
        metadata LONGTEXT DEFAULT NULL COMMENT 'JSON: arbitrary debug context (model, tokens_in, tokens_out, etc.)',
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        KEY idx_license (license_id),
        KEY idx_user (user_id),
        KEY idx_operation (operation_key),
        KEY idx_created (created_at),
        UNIQUE KEY uniq_request_id (request_id)
    ) $charset_collate;";

    $usage_logs_table = $wpdb->prefix . 'vmo_usage_logs';
    $sql_usage_logs = "CREATE TABLE IF NOT EXISTS $usage_logs_table (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        user_id BIGINT UNSIGNED NOT NULL,
        license_id BIGINT UNSIGNED NOT NULL,
        service VARCHAR(100) NOT NULL COMMENT 'e.g. openai, elevenlabs, stability',
        action VARCHAR(200) NOT NULL COMMENT 'e.g. chat_completion, image_generation',
        tokens_used INT UNSIGNED DEFAULT 0,
        cost_cents INT UNSIGNED DEFAULT 0,
        metadata LONGTEXT DEFAULT NULL COMMENT 'JSON: additional context',
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        KEY idx_user_id (user_id),
        KEY idx_license_id (license_id),
        KEY idx_service (service),
        KEY idx_created (created_at)
    ) $charset_collate;";

    dbDelta($sql_licenses);    // vmo_licenses — master/client license validation
    dbDelta($sql_usage_logs);  // vmo_usage_logs — API usage tracking per license
    dbDelta($sql_usage_ledger); // vmo_usage_ledger — authoritative master-side credit ledger
    if (function_exists('vmo_ensure_license_source_column')) {
        vmo_ensure_license_source_column($licenses_table);
    }
    if (function_exists('vmo_ensure_license_credit_columns')) {
        vmo_ensure_license_credit_columns($licenses_table);
    }

    // CRITICAL FIX: Check and migrate chat_history table if it has old column names
    $chat_history_table = $wpdb->prefix . 'vmo_chat_history';
    
    // Only check columns if we have a real database connection
    if (method_exists($wpdb, 'get_col')) {
        $suppress_chat = $wpdb->suppress_errors(true);
        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
        $columns = $wpdb->get_col("DESC $chat_history_table", 0);
        
        if ($columns && is_array($columns)) {
            $has_message_role = in_array('message_role', $columns);
            $has_message_content = in_array('message_content', $columns);
            $has_old_role = in_array('role', $columns);
            $has_old_content = in_array('content', $columns);
            $has_chat_type = in_array('chat_type', $columns);
        
        // If table has old column names, migrate them
        if (!$has_message_role && $has_old_role) {
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
            $wpdb->query("ALTER TABLE $chat_history_table CHANGE COLUMN `role` `message_role` varchar(20) NOT NULL");
            if (defined('WP_DEBUG') && WP_DEBUG) error_log('✅ VMO: Migrated column role → message_role');
        }
        
        if (!$has_message_content && $has_old_content) {
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
            $wpdb->query("ALTER TABLE $chat_history_table CHANGE COLUMN `content` `message_content` longtext NOT NULL");
            if (defined('WP_DEBUG') && WP_DEBUG) error_log('✅ VMO: Migrated column content → message_content');
        }
        
        // Add missing columns if they don't exist
        if (!$has_message_role && !$has_old_role) {
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
            $wpdb->query("ALTER TABLE $chat_history_table ADD COLUMN `message_role` varchar(20) NOT NULL DEFAULT 'user' AFTER `user_id`");
            if (defined('WP_DEBUG') && WP_DEBUG) error_log('✅ VMO: Added column message_role');
        }
        
        if (!$has_message_content && !$has_old_content) {
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
            $wpdb->query("ALTER TABLE $chat_history_table ADD COLUMN `message_content` longtext NOT NULL AFTER `message_role`");
            if (defined('WP_DEBUG') && WP_DEBUG) error_log('✅ VMO: Added column message_content');
        }
        
        if (!in_array('conversation_context', $columns)) {
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
            $wpdb->query("ALTER TABLE $chat_history_table ADD COLUMN `conversation_context` text DEFAULT NULL AFTER `message_content`");
            if (defined('WP_DEBUG') && WP_DEBUG) error_log('✅ VMO: Added column conversation_context');
        }
        if (!$has_chat_type) {
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
            $wpdb->query("ALTER TABLE $chat_history_table ADD COLUMN `chat_type` varchar(20) DEFAULT 'jarvis' AFTER `conversation_context`");
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
            $wpdb->query("ALTER TABLE $chat_history_table ADD KEY `user_chattype_created` (user_id, chat_type, created_at)");
            if (defined('WP_DEBUG') && WP_DEBUG) error_log('✅ VMO: Added column chat_type and index user_chattype_created');
        }
        }
        $wpdb->suppress_errors($suppress_chat);
    }

    // Create consultation database tables
    if (class_exists('VMO_Consultation_Database')) {
        $consultation_db = new VMO_Consultation_Database();
        $consultation_db->create_consultation_tables();
    }

    // Create enhanced business phone system tables (disabled - voice system removed)
    // Business phone system was removed in no-voice version
    if (false && class_exists('VMO_Enhanced_Business_Phone')) {
        $business_phone = new VMO_Enhanced_Business_Phone();
        $business_phone->create_phone_system_tables();
    }

    // VOICE SYSTEM REMOVED: Voice integration disabled for No Voice version
    
    // Initialize real-time call management system
    if (file_exists(VMO_AI_BUILDER_DIR . 'includes/class-vmo-realtime-call-manager.php')) {
        require_once VMO_AI_BUILDER_DIR . 'includes/class-vmo-realtime-call-manager.php';
    }
    
    // NEW: Voice Conversation Handler for v8.0 Voice System
    if (file_exists(VMO_AI_BUILDER_DIR . 'includes/vmo-voice-conversation-handler.php')) {
        require_once VMO_AI_BUILDER_DIR . 'includes/vmo-voice-conversation-handler.php';
    }
    
    // Contact Handler for pitch deck and forms
    if (file_exists(VMO_AI_BUILDER_DIR . 'includes/vmo-contact-handler.php')) {
        require_once VMO_AI_BUILDER_DIR . 'includes/vmo-contact-handler.php';
    }
    
    // Create admin tables
    if (function_exists('vmo_create_admin_tables')) {
        vmo_create_admin_tables();
    }
}

// ======================================
// LICENSE KEY GENERATION — SHARED HELPER
// Used by QuickLaunch, Subscription Manager, and any checkout flow
// that needs to issue a license key for master/client communication.
// ======================================

if (!function_exists('vmo_ensure_license_source_column')) {
    function vmo_ensure_license_source_column($table = '') {
        global $wpdb;
        static $checked = array();

        $table = $table !== '' ? $table : $wpdb->prefix . 'vmo_licenses';
        if (isset($checked[$table])) {
            return (bool) $checked[$table];
        }

        $checked[$table] = false;
        $table_exists = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table));
        if ($table_exists !== $table) {
            return false;
        }

        $source_column = $wpdb->get_var($wpdb->prepare("SHOW COLUMNS FROM $table LIKE %s", 'source'));
        if ($source_column !== 'source') {
            $wpdb->query("ALTER TABLE $table ADD source ENUM('quicklaunch','direct','manual') DEFAULT 'direct' AFTER plan");
            $source_column = $wpdb->get_var($wpdb->prepare("SHOW COLUMNS FROM $table LIKE %s", 'source'));
        }

        if ($source_column === 'source') {
            $wpdb->query(
                "UPDATE $table l
                 INNER JOIN {$wpdb->usermeta} um ON um.user_id = l.user_id
                 SET l.source = 'quicklaunch'
                 WHERE um.meta_key = 'vmo_quicklaunch_session'
                   AND um.meta_value <> ''
                   AND (l.source IS NULL OR l.source = '' OR l.source = 'direct')"
            );
            $checked[$table] = true;
        }

        return (bool) $checked[$table];
    }
}

/**
 * Ensure credit-balance columns exist on legacy vmo_licenses tables.
 *
 * Adds current_credit_balance, lifetime_credits_purchased, lifetime_credits_consumed
 * columns if missing, then back-fills current_credit_balance from the per-user
 * vmo_token_balance user_meta so existing installs do not start at zero.
 *
 * @param string $table Optional fully-qualified table name.
 * @return bool True when all three columns are present.
 */
if (!function_exists('vmo_ensure_license_credit_columns')) {
    function vmo_ensure_license_credit_columns($table = '') {
        global $wpdb;
        static $checked = array();

        $table = $table !== '' ? $table : $wpdb->prefix . 'vmo_licenses';
        if (isset($checked[$table])) {
            return (bool) $checked[$table];
        }
        $checked[$table] = false;

        $table_exists = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table));
        if ($table_exists !== $table) {
            return false;
        }

        $columns_to_add = array(
            'current_credit_balance'     => "BIGINT NOT NULL DEFAULT 0",
            'lifetime_credits_purchased' => "BIGINT UNSIGNED NOT NULL DEFAULT 0",
            'lifetime_credits_consumed'  => "BIGINT UNSIGNED NOT NULL DEFAULT 0",
        );

        $added_balance = false;
        foreach ($columns_to_add as $col => $definition) {
            $present = $wpdb->get_var($wpdb->prepare("SHOW COLUMNS FROM $table LIKE %s", $col));
            if ($present !== $col) {
                // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table+col are internal constants
                $wpdb->query("ALTER TABLE $table ADD $col $definition");
                if ($col === 'current_credit_balance') {
                    $added_balance = true;
                }
            }
        }

        // Index on current_credit_balance (idempotent — silently fails if present)
        $wpdb->suppress_errors(true);
        $wpdb->query("ALTER TABLE $table ADD INDEX idx_credit_balance (current_credit_balance)");
        $wpdb->suppress_errors(false);

        // Back-fill current_credit_balance from existing user_meta the first time the column appears
        if ($added_balance) {
            $wpdb->query(
                "UPDATE $table l
                 INNER JOIN {$wpdb->usermeta} um ON um.user_id = l.user_id
                 SET l.current_credit_balance = CAST(um.meta_value AS SIGNED)
                 WHERE um.meta_key = 'vmo_token_balance'
                   AND l.current_credit_balance = 0"
            );
        }

        $checked[$table] = true;
        return true;
    }
}

/**
 * Generate and store a VMO license key for a user.
 *
 * Inserts a row into wp_vmo_licenses and stores the key as user meta.
 * If the user already has an active license, returns the existing key.
 *
 * @param int    $user_id  WordPress user ID.
 * @param string $plan     Plan slug (tester|starter|pro|elite|enterprise).
 * @param int    $max_act  Maximum number of site activations allowed.
 * @param string $source   How the license was created: 'quicklaunch', 'direct', or 'manual'.
 * @return string|WP_Error  The license key on success, WP_Error on failure.
 */
function vmo_generate_license_key( $user_id, $plan = 'starter', $max_act = 1, $source = 'direct' ) {
    global $wpdb;

    $user_id = absint( $user_id );
    if ( ! $user_id ) {
        return new WP_Error( 'invalid_user', 'A valid user ID is required.' );
    }

    $table = $wpdb->prefix . 'vmo_licenses';
    $valid_sources = array( 'quicklaunch', 'direct', 'manual' );
    $source = in_array( $source, $valid_sources, true ) ? $source : 'direct';
    $has_source_column = function_exists('vmo_ensure_license_source_column') ? vmo_ensure_license_source_column($table) : false;
    $has_credit_columns = function_exists('vmo_ensure_license_credit_columns') ? vmo_ensure_license_credit_columns($table) : false;
    $starting_credit_balance = max(0, (int) get_user_meta($user_id, 'vmo_token_balance', true));

    // Check if user already has an active license
    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
    $existing = $wpdb->get_var( $wpdb->prepare(
        "SELECT license_key FROM $table WHERE user_id = %d AND status = 'active' LIMIT 1",
        $user_id
    ) );

    if ( $existing ) {
        // Update the plan if it changed (upgrade)
        $update_data = [ 'plan' => sanitize_text_field( $plan ), 'updated_at' => current_time( 'mysql', true ) ];
        $update_formats = [ '%s', '%s' ];
        if ($has_source_column) {
            $update_data['source'] = $source;
            $update_formats[] = '%s';
        }
        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
        $wpdb->update(
            $table,
            $update_data,
            [ 'user_id' => $user_id, 'license_key' => $existing ],
            $update_formats,
            [ '%d', '%s' ]
        );
        if ($has_credit_columns && $starting_credit_balance > 0) {
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
            $wpdb->query($wpdb->prepare(
                "UPDATE $table
                    SET current_credit_balance = GREATEST(current_credit_balance, %d),
                        lifetime_credits_purchased = GREATEST(lifetime_credits_purchased, %d),
                        updated_at = %s
                  WHERE user_id = %d AND license_key = %s",
                $starting_credit_balance,
                $starting_credit_balance,
                current_time('mysql', true),
                $user_id,
                $existing
            ));
        }
        update_user_meta( $user_id, 'vmo_license_key', $existing );
        return $existing;
    }

    // Generate a unique license key: vmo_lic_ + 32 random alphanumeric chars
    $key = 'vmo_lic_' . wp_generate_password( 32, false, false );

    // Ensure uniqueness (extremely unlikely collision but safety first)
    $attempts = 0;
    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
    while ( $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $table WHERE license_key = %s", $key ) ) > 0 && $attempts < 5 ) {
        $key = 'vmo_lic_' . wp_generate_password( 32, false, false );
        $attempts++;
    }

    // Max activations based on plan tier
    $plan_max = [
        'tester'     => 1,
        'starter'    => 1,
        'pro'        => 3,
        'elite'      => 5,
        'enterprise' => 10,
    ];
    $max_activations = $plan_max[ $plan ] ?? $max_act;

    $insert_data = [
        'user_id'         => $user_id,
        'license_key'     => $key,
        'status'          => 'active',
        'plan'            => sanitize_text_field( $plan ),
        'activations'     => wp_json_encode( [] ),
        'max_activations' => $max_activations,
        'site_stats'      => null,
        'last_sync'       => null,
        'expires_at'      => null, // Subscription-based, no fixed expiry
        'created_at'      => current_time( 'mysql', true ),
        'updated_at'      => current_time( 'mysql', true ),
    ];
    $insert_formats = [ '%d', '%s', '%s', '%s', '%s', '%d', '%s', '%s', '%s', '%s', '%s' ];

    if ($has_source_column) {
        $insert_data = array_merge(
            array_slice($insert_data, 0, 4, true),
            array('source' => $source),
            array_slice($insert_data, 4, null, true)
        );
        $insert_formats = [ '%d', '%s', '%s', '%s', '%s', '%s', '%d', '%s', '%s', '%s', '%s', '%s' ];
    }

    if ($has_credit_columns) {
        $insert_data['current_credit_balance'] = $starting_credit_balance;
        $insert_data['lifetime_credits_purchased'] = $starting_credit_balance;
        $insert_data['lifetime_credits_consumed'] = 0;
        $insert_formats[] = '%d';
        $insert_formats[] = '%d';
        $insert_formats[] = '%d';
    }

    $inserted = $wpdb->insert(
        $table,
        $insert_data,
        $insert_formats
    );

    if ( false === $inserted ) {
        error_log( 'VMO License: Failed to insert license for user ' . $user_id . ': ' . $wpdb->last_error );
        return new WP_Error( 'db_error', 'Failed to create license key.' );
    }

    // Store as user meta for quick lookups
    update_user_meta( $user_id, 'vmo_license_key', $key );

    if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
        error_log( '✅ VMO: License key generated for user ' . $user_id . ' (plan: ' . $plan . ')' );
    }

    return $key;
}

/**
 * Create a one-time license claim token for a direct-install (self-hosted) plugin.
 *
 * Called by the VMO Pay checkout / Stripe webhook after a successful direct
 * plan purchase. The user copies the returned token from their VMO account
 * page, then installs the plugin ZIP on their own WordPress and pastes
 * (email + token) into the activation screen, which posts to the master
 * /claim-license REST endpoint and exchanges the token for a real license_key.
 *
 * Tokens are single-use, expire after 24 hours, and stored in the
 * `vmo_pending_license_claims` option keyed by user_id.
 *
 * @param int    $user_id          WordPress user ID that completed checkout.
 * @param string $plan             Plan slug.
 * @param array  $opts             Optional: max_activations (int), source (string), ttl_hours (int).
 * @return string|WP_Error         The 32-char claim token, or WP_Error on failure.
 */
function vmo_create_pending_license_claim( $user_id, $plan = 'starter', $opts = array() ) {
    $user_id = absint( $user_id );
    if ( ! $user_id || ! get_userdata( $user_id ) ) {
        return new WP_Error( 'invalid_user', 'A valid user ID is required.' );
    }

    $token = bin2hex( random_bytes( 16 ) ); // 32 hex chars
    $ttl   = isset( $opts['ttl_hours'] ) ? max( 1, (int) $opts['ttl_hours'] ) : 24;

    $claims = get_option( 'vmo_pending_license_claims', array() );
    if ( ! is_array( $claims ) ) {
        $claims = array();
    }
    $claims[ $user_id ] = array(
        'token'           => $token,
        'plan'            => sanitize_text_field( $plan ),
        'source'          => isset( $opts['source'] ) ? sanitize_text_field( $opts['source'] ) : 'direct',
        'max_activations' => isset( $opts['max_activations'] ) ? max( 1, (int) $opts['max_activations'] ) : 1,
        'expires_at'      => time() + ( $ttl * HOUR_IN_SECONDS ),
        'created_at'      => time(),
    );
    update_option( 'vmo_pending_license_claims', $claims, false );

    return $token;
}

// === Core Includes ===
// Only require WordPress admin files if they exist (WordPress environment)
if (defined('ABSPATH') && file_exists(ABSPATH . 'wp-admin/includes/file.php')) {
    require_once ABSPATH . 'wp-admin/includes/file.php';
    require_once ABSPATH . 'wp-admin/includes/media.php';
    require_once ABSPATH . 'wp-admin/includes/image.php';
}
// Load Stripe SDK and Connect manager only when payments are enabled AND all files exist
$stripe_init_file = VMO_AI_BUILDER_DIR . 'stripe-sdk/init.php';
$stripe_exception_file = VMO_AI_BUILDER_DIR . 'stripe-sdk/lib/Exception/ExceptionInterface.php';

if (defined('VMO_PAYMENTS_ENABLED') && VMO_PAYMENTS_ENABLED && 
    file_exists($stripe_init_file) && 
    file_exists($stripe_exception_file) && 
    is_readable($stripe_init_file) && 
    is_readable($stripe_exception_file)) {
    
    try {
        // Load Stripe SDK only if not already loaded (prevent conflicts with other plugins)
        if (!class_exists('Stripe\\Stripe')) {
            require_once $stripe_init_file;
            
            // Initialize Stripe with API key if available
            $stripe_secret_key = get_option('vmo_stripe_secret_key', '');
            if (!empty($stripe_secret_key) && class_exists('Stripe\\Stripe')) {
                try {
                    \Stripe\Stripe::setApiKey($stripe_secret_key);
                    if (defined('WP_DEBUG') && WP_DEBUG) {
                        error_log('✅ VMO: Stripe SDK loaded and initialized successfully');
                    }
                } catch (Exception $e) {
                    if (defined('WP_DEBUG') && WP_DEBUG) {
                        error_log('⚠️ VMO: Stripe API key initialization failed: ' . $e->getMessage());
                    }
                }
            }
        } else {
            if (defined('WP_DEBUG') && WP_DEBUG) {
                error_log('ℹ️ VMO: Stripe SDK already loaded (possibly by another plugin)');
            }
        }
    } catch (Exception $e) {
        // Log the error but don't disable payments - allow user to fix config
        if (defined('WP_DEBUG') && WP_DEBUG) {
            error_log('❌ VMO: Stripe SDK loading failed: ' . $e->getMessage());
        }
    } catch (Error $e) {
        // Catch PHP 7+ errors (like namespace issues)
        if (defined('WP_DEBUG') && WP_DEBUG) {
            error_log('❌ VMO: Stripe SDK fatal error: ' . $e->getMessage());
        }
    }
} else {
    // Log why payments are not loading
    if (defined('VMO_PAYMENTS_ENABLED') && VMO_PAYMENTS_ENABLED) {
        if (defined('WP_DEBUG') && WP_DEBUG) {
            if (!file_exists($stripe_init_file)) {
                error_log('❌ VMO: Stripe SDK init.php not found at: ' . $stripe_init_file);
            } elseif (!is_readable($stripe_init_file)) {
                if (defined('WP_DEBUG') && WP_DEBUG) error_log('❌ VMO: Stripe SDK init.php not readable at: ' . $stripe_init_file);
            }
        }
    }
}

// Core functionality includes with file existence validation to prevent fatal errors
$core_includes = array(
    'includes/vmo-pricing-policy.php',
    'includes/vmo-operation-catalog.php',
    'includes/vmo-initial-setup-policy.php',
    'includes/generate-content.php',
    'includes/media-handler.php', 
    'includes/token-manager.php',
    'includes/undo-handler.php',
    'includes/save-project.php',
    'includes/save-generated-page.php',
    'includes/load-project.php',
    'includes/class-vmo-hosting-upgrade.php'
);

foreach ($core_includes as $include_file) {
    $full_path = VMO_AI_BUILDER_DIR . $include_file;
    if (file_exists($full_path) && is_readable($full_path)) {
        try {
            require_once $full_path;
        } catch (Exception $e) {
            if (defined('WP_DEBUG') && WP_DEBUG) {
                error_log('VMO: Failed to load ' . $include_file . ': ' . $e->getMessage());
            }
        }
    } else {
        if (defined('WP_DEBUG') && WP_DEBUG) {
            error_log('VMO: Core include file missing or unreadable: ' . $full_path);
        }
    }
}

// Register checkout, webhook, and Connect routes only when payments are enabled AND Stripe SDK exists
if (defined('VMO_PAYMENTS_ENABLED') && VMO_PAYMENTS_ENABLED && file_exists(VMO_AI_BUILDER_DIR . 'stripe-sdk/init.php')) {
    if (file_exists(VMO_AI_BUILDER_DIR . 'includes/create-checkout.php')) {
        require_once VMO_AI_BUILDER_DIR . 'includes/create-checkout.php';
    }
    if (file_exists(VMO_AI_BUILDER_DIR . 'includes/stripe-webhook.php')) {
        require_once VMO_AI_BUILDER_DIR . 'includes/stripe-webhook.php';
    }
    if (file_exists(VMO_AI_BUILDER_DIR . 'includes/class-vmo-stripe-connect.php')) {
        require_once VMO_AI_BUILDER_DIR . 'includes/class-vmo-stripe-connect.php';
        if (class_exists('VMO_Stripe_Connect')) {
            // Initialize REST routes
            add_action('rest_api_init', function() { new VMO_Stripe_Connect(); });
        }
    }

    // Abandoned Cart Detection & Recovery
    if (file_exists(VMO_AI_BUILDER_DIR . 'includes/class-vmo-abandoned-cart.php')) {
        require_once VMO_AI_BUILDER_DIR . 'includes/class-vmo-abandoned-cart.php';
        if (class_exists('VMO_Abandoned_Cart')) {
            VMO_Abandoned_Cart::instance();
        }
    }

    // ✅ VMO Tax Calculator v1.0.0 - Comprehensive tax estimation (like Stripe Tax)
    if (file_exists(VMO_AI_BUILDER_DIR . 'includes/class-vmo-tax-calculator.php')) {
        require_once VMO_AI_BUILDER_DIR . 'includes/class-vmo-tax-calculator.php';
        if (class_exists('VMO_Tax_Calculator')) {
            add_action('rest_api_init', function() { new VMO_Tax_Calculator(); });
        }
    }

}

// ✅ VMO QuickLaunch Hosting v1.0.0 - One-click server + domain + email provisioning
// Loaded OUTSIDE the Stripe conditional — QuickLaunch REST routes (including validate-keys)
// must always register. Individual handlers check for their own keys internally.
if (file_exists(VMO_AI_BUILDER_DIR . 'includes/class-vmo-quicklaunch.php')) {
    require_once VMO_AI_BUILDER_DIR . 'includes/class-vmo-quicklaunch.php';
    if (class_exists('VMO_QuickLaunch')) {
        VMO_QuickLaunch::instance();
    }
}

// ✅ VMO QuickLaunch Diagnostic admin page (Tools menu). Master/Direct only —
// the build script strips this file from the client (Vultr-hosted) ZIP.
if (is_admin() && file_exists(VMO_AI_BUILDER_DIR . 'includes/class-vmo-quicklaunch-debug.php')) {
    require_once VMO_AI_BUILDER_DIR . 'includes/class-vmo-quicklaunch-debug.php';
}

// ✅ VMO Continuous Improvement Engine - Background scans, recommendations, roadmap updates
if (file_exists(VMO_AI_BUILDER_DIR . 'includes/class-vmo-continuous-improvement.php')) {
    require_once VMO_AI_BUILDER_DIR . 'includes/class-vmo-continuous-improvement.php';
    if (class_exists('VMO_Continuous_Improvement')) {
        VMO_Continuous_Improvement::get_instance()->init();
    }
}

// ✅ VMO Shipping Manager - Order management, carrier settings, tracking, fulfillment
if (file_exists(VMO_AI_BUILDER_DIR . 'includes/class-vmo-shipping-manager.php')) {
    require_once VMO_AI_BUILDER_DIR . 'includes/class-vmo-shipping-manager.php';
    if (class_exists('VMO_Shipping_Manager')) {
        VMO_Shipping_Manager::get_instance()->init();
    }
}

// VMO Business Flow Engine - AI-planned operational flow, payment-to-fulfillment bridge, and completion tracking
if (file_exists(VMO_AI_BUILDER_DIR . 'includes/class-vmo-business-flow-engine.php')) {
    require_once VMO_AI_BUILDER_DIR . 'includes/class-vmo-business-flow-engine.php';
    if (class_exists('VMO_Business_Flow_Engine')) {
        VMO_Business_Flow_Engine::get_instance()->init();
    }
}

// ✅ VMO Bulk SMS Marketing - Campaign creation, audience, scheduling, sending via Twilio
if (file_exists(VMO_AI_BUILDER_DIR . 'includes/class-vmo-bulk-sms-marketing.php')) {
    require_once VMO_AI_BUILDER_DIR . 'includes/class-vmo-bulk-sms-marketing.php';
    if (class_exists('VMO_Bulk_SMS_Marketing')) {
        VMO_Bulk_SMS_Marketing::get_instance()->init();
    }
}

// ✅ VMO Pay - Invoices, Payment Links, Customer Lifetime Value
if (file_exists(VMO_AI_BUILDER_DIR . 'includes/class-vmo-pay.php')) {
    require_once VMO_AI_BUILDER_DIR . 'includes/class-vmo-pay.php';
    if (class_exists('VMO_Pay')) {
        VMO_Pay::get_instance()->init();
    }
}

// ✅ VMO Feature Gate - Per-feature plan gating with upgrade recommendations
if (file_exists(VMO_AI_BUILDER_DIR . 'includes/class-vmo-feature-gate.php')) {
    require_once VMO_AI_BUILDER_DIR . 'includes/class-vmo-feature-gate.php';
    if (class_exists('VMO_Feature_Gate')) {
        VMO_Feature_Gate::register_ajax();
    }
}

// ✅ VMO Builder Intelligence - QA critic, competitive intel injection, multi-agent enhancement
if (file_exists(VMO_AI_BUILDER_DIR . 'includes/class-vmo-builder-intelligence.php')) {
    require_once VMO_AI_BUILDER_DIR . 'includes/class-vmo-builder-intelligence.php';
    if (class_exists('VMO_Builder_Intelligence')) {
        VMO_Builder_Intelligence::get_instance()->init();
    }
}

// ✅ VMO Email Auto-Reply Engine + Form Routing + Product Blocks
if (file_exists(VMO_AI_BUILDER_DIR . 'includes/class-vmo-email-auto-reply.php')) {
    require_once VMO_AI_BUILDER_DIR . 'includes/class-vmo-email-auto-reply.php';
    if (class_exists('VMO_Email_Auto_Reply')) {
        VMO_Email_Auto_Reply::get_instance()->init();
    }
}

// ✅ VMO Inbound Mailbox Sync - Pull connected IMAP mailboxes into VMO Email
if (file_exists(VMO_AI_BUILDER_DIR . 'includes/class-vmo-inbound-email-sync.php')) {
    require_once VMO_AI_BUILDER_DIR . 'includes/class-vmo-inbound-email-sync.php';
    if (class_exists('VMO_Inbound_Email_Sync')) {
        VMO_Inbound_Email_Sync::get_instance()->init();
    }
}

// ✅ VMO Integration Hub - Cross-system wiring, unified actions, Jarvis nav, Jerreme recs
if (file_exists(VMO_AI_BUILDER_DIR . 'includes/class-vmo-integration-hub.php')) {
    require_once VMO_AI_BUILDER_DIR . 'includes/class-vmo-integration-hub.php';
    if (class_exists('VMO_Integration_Hub')) {
        VMO_Integration_Hub::get_instance()->init();
    }
}

// Additional includes with file existence validation
$additional_includes = array(
    'includes/image-upload.php',
    'includes/media-upload-handler.php',
    'includes/vmo_upload_device_image.php', 
    'includes/generate-image.php',
    'includes/vmo_admin_tables.php',
    'includes/class-vmo-conversation-manager.php',
    'includes/class-vmo-template-handler.php',
    'includes/class-vmo-media-integration-agent.php',
    'includes/vmo-media-integration-hooks.php',
    'includes/consultation-profile.php',
    'includes/approval-queue.php'
);

foreach ($additional_includes as $include_file) {
    $full_path = VMO_AI_BUILDER_DIR . $include_file;
    if (file_exists($full_path) && is_readable($full_path)) {
        try {
            require_once $full_path;
        } catch (Exception $e) {
            if (defined('WP_DEBUG') && WP_DEBUG) {
                error_log('VMO: Failed to load ' . $include_file . ': ' . $e->getMessage());
            }
        }
    } else {
        if (defined('WP_DEBUG') && WP_DEBUG) {
            error_log('VMO: Include file missing or unreadable: ' . $full_path);
        }
    }
}

// Verify critical DB tables (voice conversations) exist even if activation was skipped
add_action('admin_init', function(){
    global $wpdb;
    $voice_conversations_table = $wpdb->prefix . 'vmo_voice_conversations';
    $voice_turns_table = $wpdb->prefix . 'vmo_voice_conversation_turns';
    $missing = false;
    $exists1 = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $wpdb->esc_like($voice_conversations_table)));
    $exists2 = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $wpdb->esc_like($voice_turns_table)));
    if (!$exists1 || !$exists2) { $missing = true; }
    if ($missing) {
        try { vmo_create_plugin_tables(); } catch (Throwable $e) { /* log silently */ }
    }
});

// Inject approved SEO into wp_head when viewing live pages
add_action('wp_head', function(){
    if (!is_singular()) return;
    $post_id = get_the_ID();
    $title = get_post_meta($post_id, 'vmo_seo_title', true);
    if (!$title) $title = get_post_meta($post_id, '_vmo_seo_title', true);
    $desc  = get_post_meta($post_id, 'vmo_seo_description', true);
    if (!$desc) $desc = get_post_meta($post_id, '_vmo_seo_description', true);
    $json  = get_post_meta($post_id, 'vmo_seo_json_ld', true);
    if (!$json) $json = get_post_meta($post_id, '_vmo_seo_json_ld', true);
    if ($title) echo "\n<!-- VMO SEO -->\n<title>" . esc_html($title) . "</title>\n";
    if ($desc)  echo '<meta name="description" content="' . esc_attr($desc) . '">\n';
    if ($json)  echo '<script type="application/ld+json">' . $json . '</script>\n';
    // Open Graph / Twitter Cards
    $og_title = get_post_meta($post_id, 'vmo_og_title', true);
    $og_desc  = get_post_meta($post_id, 'vmo_og_description', true);
    if ($og_title) echo '<meta property="og:title" content="' . esc_attr($og_title) . '">\n';
    if ($og_desc)  echo '<meta property="og:description" content="' . esc_attr($og_desc) . '">\n';
    if ($og_title) echo '<meta name="twitter:title" content="' . esc_attr($og_title) . '">\n';
    if ($og_desc)  echo '<meta name="twitter:description" content="' . esc_attr($og_desc) . '">\n';
    // Canonical
    $canonical = get_post_meta($post_id, 'vmo_seo_canonical', true);
    if ($canonical) echo '<link rel="canonical" href="' . esc_url($canonical) . '">\n';
}, 1);

// Apply alt text overrides into content for images
add_filter('the_content', function($content){
    if (!is_singular()) return $content;
    $post_id = get_the_ID();
    $overrides = get_post_meta($post_id, 'vmo_alt_text_overrides', true);
    if (!$overrides || !is_array($overrides)) return $content;
    // Simple replace by src match
    foreach ($overrides as $o) {
        if (!empty($o['src']) && !empty($o['alt'])) {
            $pattern = '#<img([^>]+src=["\\\']' . preg_quote($o['src'], '#') . '["\\\'][^>]*)>#i';
            $content = preg_replace_callback($pattern, function($m) use ($o){
                $tag = $m[0];
                if (stripos($tag, 'alt=') !== false) {
                    $tag = preg_replace('/alt=["\\\'][^"\\\']*["\\\']/', 'alt="'.esc_attr($o['alt']).'"', $tag);
                } else {
                    $tag = str_replace('<img', '<img alt="'.esc_attr($o['alt']).'"', $tag);
                }
                return $tag;
            }, $content);
        }
    }
    return $content;
}, 12);

// VOICE SYSTEM REMOVED: Enhanced Business Phone System disabled
// require_once VMO_AI_BUILDER_DIR . 'includes/class-vmo-twilio-enhanced-client.php';
// require_once VMO_AI_BUILDER_DIR . 'includes/class-vmo-enhanced-voice-processor.php';
// require_once VMO_AI_BUILDER_DIR . 'includes/class-vmo-enhanced-business-phone.php';

// ⚡ PERFORMANCE: Only load heavy subsystem modules when VMO context is active.
// Skip on non-VMO admin pages (Posts, Media, Users, etc.) to avoid ~30 unnecessary require_once calls.
$vmo_needs_subsystems = (
    wp_doing_ajax()              // AJAX requests need all handlers available
    || wp_doing_cron()           // Cron jobs may invoke subsystem functions
    || !is_admin()               // Frontend/REST requests need subsystems
    || (isset($_GET['page']) && strpos($_GET['page'], 'vmo') !== false)  // VMO admin pages
    || (isset($_GET['vmo_twilio']))  // Twilio webhook callbacks
);

if ($vmo_needs_subsystems) {

// Twilio REST API client (No SDK required)
if (file_exists(VMO_AI_BUILDER_DIR . 'includes/vmo-twilio-database.php')) require_once VMO_AI_BUILDER_DIR . 'includes/vmo-twilio-database.php';
if (file_exists(VMO_AI_BUILDER_DIR . 'includes/class-vmo-twilio-rest.php')) {
    require_once VMO_AI_BUILDER_DIR . 'includes/class-vmo-twilio-rest.php';
} else {
    if (defined('WP_DEBUG') && WP_DEBUG) error_log('VMO: Twilio REST client missing - phone features disabled');
}
// Twilio admin settings are master-only and are not loaded in the client build.

// Email marketing engine (AI-generated leads + 4-option campaigns)
if (file_exists(VMO_AI_BUILDER_DIR . 'includes/class-vmo-email-identity-manager.php')) require_once VMO_AI_BUILDER_DIR . 'includes/class-vmo-email-identity-manager.php';
if (file_exists(VMO_AI_BUILDER_DIR . 'includes/class-vmo-email-marketing.php')) require_once VMO_AI_BUILDER_DIR . 'includes/class-vmo-email-marketing.php';
if (file_exists(VMO_AI_BUILDER_DIR . 'includes/class-vmo-email-compliance.php')) require_once VMO_AI_BUILDER_DIR . 'includes/class-vmo-email-compliance.php';
if (file_exists(VMO_AI_BUILDER_DIR . 'includes/class-vmo-dns-detector.php')) require_once VMO_AI_BUILDER_DIR . 'includes/class-vmo-dns-detector.php';

// Turnkey Business Blueprints (pre-configured business setups VMO can build and manage)
if (file_exists(VMO_AI_BUILDER_DIR . 'includes/class-vmo-turnkey-business-blueprints.php')) require_once VMO_AI_BUILDER_DIR . 'includes/class-vmo-turnkey-business-blueprints.php';

// Sora Video Generator (AI video generation for hero backgrounds, product videos, 360° spin)
if (file_exists(VMO_AI_BUILDER_DIR . 'includes/class-vmo-sora-video-generator.php')) {
    require_once VMO_AI_BUILDER_DIR . 'includes/class-vmo-sora-video-generator.php';
    if (defined('WP_DEBUG') && WP_DEBUG) error_log('VMO: Sora Video Generator loaded successfully');
}

// Persistent project media queue for background image/video completion
if (file_exists(VMO_AI_BUILDER_DIR . 'includes/class-vmo-project-media-queue.php')) {
    require_once VMO_AI_BUILDER_DIR . 'includes/class-vmo-project-media-queue.php';
}

// Unified queued media replacement jobs for builder/Jarvis media swaps
if (file_exists(VMO_AI_BUILDER_DIR . 'includes/class-vmo-media-replacement-jobs.php')) {
    require_once VMO_AI_BUILDER_DIR . 'includes/class-vmo-media-replacement-jobs.php';
}

// Animated Overlays System (GPT Image 1.5 transparent overlays - clouds, smoke, particles, etc.)
if (file_exists(VMO_AI_BUILDER_DIR . 'includes/class-vmo-animated-overlays.php')) {
    require_once VMO_AI_BUILDER_DIR . 'includes/class-vmo-animated-overlays.php';
    if (defined('WP_DEBUG') && WP_DEBUG) error_log('VMO: Animated Overlays System loaded successfully');
}

// Legacy cloud email provisioning and inbound ingestion removed.

// Usage calculator for smart flexible allocation
if (file_exists(VMO_AI_BUILDER_DIR . 'includes/class-vmo-usage-calculator.php')) require_once VMO_AI_BUILDER_DIR . 'includes/class-vmo-usage-calculator.php';

// Legal Document Generator (Privacy Policy, Terms of Service, etc.)
if (file_exists(VMO_AI_BUILDER_DIR . 'includes/class-vmo-legal.php')) require_once VMO_AI_BUILDER_DIR . 'includes/class-vmo-legal.php';

// OpenAI TTS HD Streaming via REST API (No API key exposure on frontend)
if (file_exists(VMO_AI_BUILDER_DIR . 'includes/class-vmo-openai-tts-rest.php')) {
    require_once VMO_AI_BUILDER_DIR . 'includes/class-vmo-openai-tts-rest.php';
    if (defined('WP_DEBUG') && WP_DEBUG) error_log('VMO: OpenAI TTS REST API loaded successfully');
} else {
    if (defined('WP_DEBUG') && WP_DEBUG) error_log('VMO: OpenAI TTS file missing - voice features will not work');
}

if (file_exists(VMO_AI_BUILDER_DIR . 'includes/class-vmo-greeting-audio.php')) {
    require_once VMO_AI_BUILDER_DIR . 'includes/class-vmo-greeting-audio.php';
    VMO_Greeting_Audio_Manager::get_instance();
} else {
    if (defined('WP_DEBUG') && WP_DEBUG) error_log('VMO: Greeting audio manager missing - consultation greeting asset not cached');
}

// Optional legacy Manager classes (only load if present)
foreach ([
    'includes/class-vmo-manager-admin.php',
    'includes/class-vmo-manager-affiliates.php',
    'includes/class-vmo-manager-api.php',
    'includes/class-vmo-manager-core.php',
    'includes/class-vmo-manager-database.php',
    'includes/class-vmo-manager-emails.php',
    'includes/class-vmo-manager-stripe.php',
] as $maybe_include) {
    $path = VMO_AI_BUILDER_DIR . $maybe_include;
    if (file_exists($path)) {
        // Skip Stripe manager when payments are disabled
        if (basename($path) === 'class-vmo-manager-stripe.php' && (!defined('VMO_PAYMENTS_ENABLED') || !VMO_PAYMENTS_ENABLED)) {
            // do not load
        } else {
            require_once $path;
        }
    }
}

// Ensure Email + Manager APIs are actually initialized (some classes are included but not instantiated)
add_action('plugins_loaded', function() {
    if (class_exists('VMO_Manager_API') && empty($GLOBALS['vmo_manager_api_instance'])) {
        $GLOBALS['vmo_manager_api_instance'] = new VMO_Manager_API();
    }
    if (class_exists('VMO_Email_Marketing') && empty($GLOBALS['vmo_email_marketing_instance'])) {
        $GLOBALS['vmo_email_marketing_instance'] = new VMO_Email_Marketing();
    }
    if (class_exists('VMO_Email_Compliance') && empty($GLOBALS['vmo_email_compliance_instance'])) {
        $GLOBALS['vmo_email_compliance_instance'] = VMO_Email_Compliance::get_instance();
    }
    if (class_exists('VMO_Promotion_Admin_Settings')) {
        VMO_Promotion_Admin_Settings::get_instance();
    }
    if (class_exists('VMO_Promotion_Provider_Registry')) {
        VMO_Promotion_Provider_Registry::get_instance();
    }
    if (class_exists('VMO_Promotion_Data_Store')) {
        VMO_Promotion_Data_Store::get_instance();
    }
    if (class_exists('VMO_Promotion_Orchestrator')) {
        VMO_Promotion_Orchestrator::get_instance();
    }
    if (class_exists('VMO_Promotion_Execution_Engine')) {
        VMO_Promotion_Execution_Engine::get_instance();
    }
    if (class_exists('VMO_Promotion_Ad_Execution_Engine')) {
        VMO_Promotion_Ad_Execution_Engine::get_instance();
    }
}, 5);

// CAPABILITY REGISTRY: Must load before all feature classes so they can self-register
$registry_path = VMO_AI_BUILDER_DIR . 'includes/class-vmo-capability-registry.php';
if (file_exists($registry_path)) {
    require_once $registry_path;
    VMO_Capability_Registry::get_instance(); // Initialize early
}

// NEW: Business wizard and multi-agent system with file validation
$business_wizard_includes = array(
    'includes/class-vmo-premium-shell.php',
    'includes/class-vmo-consultation-database.php',
    'includes/class-vmo-website-media-manifest.php',
    'includes/class-vmo-visual-preset-registry.php',
    'includes/agents/class-vmo-website-generation-agents.php',
    'includes/class-vmo-website-agent-runtime.php',
    'includes/class-vmo-canonical-website-pipeline.php',
    'includes/class-vmo-business-manifest-generator.php',
    'includes/class-vmo-automation-engine.php',
    'includes/class-vmo-document-generator.php',
    'includes/class-vmo-consultation-deliverables-gate.php',
    'includes/class-vmo-enhanced-business-wizard.php',
    'includes/class-vmo-consultation-website-integration.php',
    'includes/class-vmo-email-provisioner.php',
    'includes/class-vmo-zoho-mail-integration.php'
);

foreach ($business_wizard_includes as $include_file) {
    $full_path = VMO_AI_BUILDER_DIR . $include_file;
    if (file_exists($full_path) && is_readable($full_path)) {
        try {
            require_once $full_path;
        } catch (Exception $e) {
            if (defined('WP_DEBUG') && WP_DEBUG) {
                error_log('VMO: Failed to load ' . $include_file . ': ' . $e->getMessage());
            }
        }
    } else {
        if (defined('WP_DEBUG') && WP_DEBUG) {
            error_log('VMO: Business wizard include file missing: ' . $full_path);
        }
    }
}

if (class_exists('VMO_Document_Generator') && empty($GLOBALS['vmo_document_generator'])) {
    $GLOBALS['vmo_document_generator'] = new VMO_Document_Generator();
}

// DEFERRED LOADING: AI Website Assistant requires WordPress functions
add_action('plugins_loaded', function() {
    $assistant_path = VMO_AI_BUILDER_DIR . 'includes/class-vmo-website-ai-assistant.php';
    if (file_exists($assistant_path) && is_readable($assistant_path)) {
        try {
            require_once $assistant_path;
        } catch (Exception $e) {
            if (defined('WP_DEBUG') && WP_DEBUG) {
                error_log('VMO: Failed to load website AI assistant: ' . $e->getMessage());
            }
        }
    }
});

// Phone/Twilio and voice system
if (file_exists(VMO_AI_BUILDER_DIR . 'includes/class-vmo-content-crawler.php')) require_once VMO_AI_BUILDER_DIR . 'includes/class-vmo-content-crawler.php';
// Twilio A2P 10DLC Registration Manager (ISV-ready brand & campaign registration)
if (file_exists(VMO_AI_BUILDER_DIR . 'includes/class-vmo-twilio-a2p-manager.php')) require_once VMO_AI_BUILDER_DIR . 'includes/class-vmo-twilio-a2p-manager.php';
// Twilio Token Economics — usage-based billing for all Twilio services
if (file_exists(VMO_AI_BUILDER_DIR . 'includes/class-vmo-twilio-token-economics.php')) require_once VMO_AI_BUILDER_DIR . 'includes/class-vmo-twilio-token-economics.php';
// Website Scanner for URL analysis, cloning, and reference scanning
if (file_exists(VMO_AI_BUILDER_DIR . 'includes/class-vmo-website-scanner.php')) require_once VMO_AI_BUILDER_DIR . 'includes/class-vmo-website-scanner.php';
// VOICE SYSTEM REMOVED: Voice admin settings removed for No Voice version

// VOICE SYSTEM REMOVED: Audio transcription (Whisper API) disabled for No Voice version

// Agent router
if (file_exists(VMO_AI_BUILDER_DIR . 'agents/router/agent-router.php')) require_once VMO_AI_BUILDER_DIR . 'agents/router/agent-router.php';

// Memory & Roadmap AJAX endpoints (durable memory and milestones management)
if (file_exists(VMO_AI_BUILDER_DIR . 'includes/memory-roadmap.php')) {
    require_once VMO_AI_BUILDER_DIR . 'includes/memory-roadmap.php';
}

} // end if ($vmo_needs_subsystems)

// Instantiate managers so hooks/webhooks are active
add_action('plugins_loaded', function() {
    // Phone system disabled in no-voice version
    if (false && class_exists('VMO_Phone_Manager')) {
    // Ensure tables exist (repair if activation hook missed)
    try { VMO_Phone_Manager::create_tables(); } catch (Throwable $e) { /* ignore */ }
        // Singletons not enforced; simple instance to bind hooks
        $GLOBALS['vmo_phone_manager'] = new VMO_Phone_Manager();
    }
    if (false && class_exists('VMO_Phone_Admin') && is_admin()) {
        $GLOBALS['vmo_phone_admin'] = new VMO_Phone_Admin();
    }
    if (false && class_exists('VMO_Phone_Frontend') && is_admin()) {
        // Frontend handler provides AJAX for phone.js; safe in admin context
        $GLOBALS['vmo_phone_frontend'] = new VMO_Phone_Frontend();
    }
    
    // Initialize Enhanced Business Wizard
    if (class_exists('VMO_Enhanced_Business_Wizard')) {
        $GLOBALS['vmo_enhanced_business_wizard'] = VMO_Enhanced_Business_Wizard::get_instance();
    }
    
    // Initialize Audio Transcription (disabled - voice system removed)
    // Audio transcription was removed in no-voice version  
    if (false && class_exists('VMO_Audio_Transcription')) {
        $GLOBALS['vmo_audio_transcription'] = new VMO_Audio_Transcription();
    }
    
    // Legacy Business Consultation Handler intentionally not instantiated; enhanced wizard is the source of truth.

    // Initialize Twilio A2P Registration Manager
    if (class_exists('VMO_Twilio_A2P_Manager')) {
        VMO_Twilio_A2P_Manager::get_instance();
    }

    // Initialize Twilio Token Economics (usage-based billing)
    if (class_exists('VMO_Twilio_Token_Economics')) {
        VMO_Twilio_Token_Economics::get_instance();
    }
    
    // Initialize Consultation Database
    if (class_exists('VMO_Consultation_Database')) {
        try {
            $vmo_consultation_db = new VMO_Consultation_Database();
            // Ensure tables exist on init; explicit call covers activation-missed cases
            $vmo_consultation_db->create_tables();
        } catch (Throwable $e) {
            if (defined('WP_DEBUG') && WP_DEBUG) error_log('VMO: Failed to create consultation database tables: ' . $e->getMessage());
        }
    }
});

// === Monthly Reports Cron Job ===
add_action('vmo_send_monthly_reports', function() {
    if (class_exists('VMO_Consultation_Database')) {
        $db = new VMO_Consultation_Database();
        $db->send_monthly_report_email();
    }
});

// === Register AJAX handlers for device image upload ===
add_action('wp_ajax_vmo_upload_device_image', 'vmo_upload_device_image_handler');

// === Custom Post Type for Projects ===
add_action('init', function () {
    // Ensure rewrite rules are flushed for new REST routes (Fixes TTS 404/HTML errors)
    if (!get_transient('vmo_rewrite_flushed_v9_0_1_fixed')) {
        flush_rewrite_rules();
        set_transient('vmo_rewrite_flushed_v9_0_1_fixed', true, 12 * HOUR_IN_SECONDS);
    }

    register_post_type('vmo_project', [
        'labels' => [
            'name' => 'VMO Projects',
            'singular_name' => 'VMO Project',
        ],
        'public' => false,
        'show_ui' => true,
        'menu_icon' => 'dashicons-portfolio',
        'supports' => ['title', 'editor', 'author'],
    ]);
});

// Video Upload AJAX
add_action('wp_ajax_vmo_upload_video', function() {
    check_ajax_referer('vmo_builder_nonce', 'security');
    if (!function_exists('wp_handle_upload')) require_once(ABSPATH . 'wp-admin/includes/file.php');
    $uploaded = wp_handle_upload($_FILES['video'], ['test_form' => false]);
    if (isset($uploaded['url'])) {
        wp_send_json_success(['url' => $uploaded['url']]);
    } else {
        wp_send_json_error(['message' => 'Upload failed.']);
    }
});


// VMO_CLIENT_RUNTIME_HELPER_HOTFIX_v1
// Safe helper used before admin_menu callbacks run. Must exist in both master and client packages.
if (!function_exists('vmo_is_client_instance_runtime')) {
    function vmo_is_client_instance_runtime() {
        if (defined('VMO_CLIENT_MODE') && VMO_CLIENT_MODE === true) {
            return true;
        }
        // Hosted QuickLaunch clients also identify through distribution/channel constants when wp-config
        // constants are delayed or stale. Do not treat the getvmo.com master as client unless explicit.
        if (defined('VMO_CLIENT_DISTRIBUTION_CHANNEL') && VMO_CLIENT_DISTRIBUTION_CHANNEL === 'quicklaunch_hosted') {
            return true;
        }
        return false;
    }
}

// === Admin Menu Pages ===
add_action('admin_menu', function () {
    // Use custom VMO icon instead of dashicon
    $icon_url = plugin_dir_url(__FILE__) . 'assets/vmo-icon.svg';
    
    // Builder is user-facing; allow logged-in users.
    add_menu_page('VMO', 'VMO', 'read', 'vmo-ai-builder-main', 'vmo_ai_builder_page', $icon_url, 3);
    
    // Explicitly add the main page as the first submenu to ensure it remains accessible and isn't overridden by 'Settings'
    add_submenu_page('vmo-ai-builder-main', 'Website Builder', 'Website Builder', 'read', 'vmo-ai-builder-main', 'vmo_ai_builder_page');

    // Settings is master-only. Hosted/client builds must not expose an empty or secret-adjacent settings page.
    if (!vmo_is_client_instance_runtime()) {
    // Settings removed in client build
}
    // Dashboard is user-facing.
    add_submenu_page('vmo-ai-builder-main', 'Dashboard', 'Dashboard', 'read', 'vmo-ai-dashboard', 'vmo_ai_user_dashboard_page');
    // My Plan / Billing
    add_submenu_page('vmo-ai-builder-main', 'My Plan', 'My Plan', 'read', 'vmo-my-plan', function() {
        include VMO_AI_BUILDER_DIR . 'templates/my-plan.php';
    });
    // Note: Business Phone, SEO Reports, Email Marketing, CRM, and Payments are now integrated 
    // into the Strategic Dashboard as tabs rather than separate pages

    // Master Admin is master-only and must not appear in any client package.
    if (!vmo_is_client_instance_runtime()) {
    // Master Admin removed in client build
}
});


// VMO_CLIENT_HIDE_MASTER_PAGES_v1
add_action('admin_menu', function() {
    if (!function_exists('vmo_is_client_instance_runtime') || !vmo_is_client_instance_runtime()) {
        return;
    }
    remove_submenu_page('vmo-ai-builder-main', 'vmo-ai-settings');
    remove_submenu_page('vmo-ai-builder-main', 'vmo-master-admin');
}, 999);

function vmo_master_admin_page() {
    wp_die('This interface is only available on the VMO master server.');
}

// Add custom styles for the VMO icon in admin menu
add_action('admin_head', function() {
    echo '<style>
        #adminmenu #toplevel_page_vmo-ai-builder-main .wp-menu-image img {
            width: 20px;
            height: 20px;
            opacity: 0.6;
            transition: opacity 0.2s;
            padding: 6px 0;
        }
        #adminmenu #toplevel_page_vmo-ai-builder-main:hover .wp-menu-image img,
        #adminmenu #toplevel_page_vmo-ai-builder-main.current .wp-menu-image img {
            opacity: 1;
        }
    </style>';
});

// Prevent WordPress from redirecting our admin URLs
add_action('template_redirect', function() {
    if (is_admin()) {
        return; // Don't interfere with admin pages
    }
});

// Add additional protection against URL conflicts
add_action('init', function() {
    // Remove any conflicting rewrite rules that might interfere with admin URLs
    if (is_admin()) {
        remove_action('template_redirect', 'redirect_canonical');
    }
}, 1);

function vmo_ai_builder_page() {
    // Ensure we're in admin context
    if (!is_admin()) {
        wp_die('Access denied. This page can only be accessed from WordPress admin.');
    }
    
    // Check if template file exists
    $template_path = VMO_AI_BUILDER_DIR . 'templates/builder-interface.php';
    if (!file_exists($template_path)) {
        wp_die('VMO AI Builder template file not found. Please reinstall the plugin.');
    }
    
    // Include the template
    include $template_path;
}
function vmo_ai_settings_page() {
    wp_die('Settings are managed centrally on the VMO master server.');
}
function vmo_ai_user_dashboard_page() {
    if (!is_admin()) {
        wp_die('Access denied.');
    }
    // Use the new strategic dashboard UI (PHP version of vmo-strategic-dashboard-complete.html)
    $strategic_path = VMO_AI_BUILDER_DIR . 'templates/strategic-dashboard.php';
    if (file_exists($strategic_path)) {
    include $strategic_path;
    } else {
        include VMO_AI_BUILDER_DIR . 'templates/user-dashboard.php';
    }
}

// Dashboard stats AJAX for the strategic dashboard
add_action('wp_ajax_vmo_dashboard_stats', 'vmo_dashboard_stats');
// Helper: verify admin/builder nonce for AJAX requests (compat across modules)
if (!function_exists('vmo_verify_admin_security')) {
    function vmo_verify_admin_security(): bool {
        $nonce_values = [];
        foreach (['security', 'nonce', '_ajax_nonce', 'vmo_admin_nonce', 'vmo_builder_nonce', 'vmo_nonce'] as $field_name) {
            if (isset($_REQUEST[$field_name])) {
                $nonce_values[] = sanitize_text_field(wp_unslash($_REQUEST[$field_name]));
            }
        }
        foreach (array_unique(array_filter($nonce_values)) as $nonce) {
            if (wp_verify_nonce($nonce, 'vmo_admin_nonce')) return true;
            if (wp_verify_nonce($nonce, 'vmo_builder_nonce')) return true;
            if (wp_verify_nonce($nonce, 'vmo_nonce')) return true;
        }
        return false;
    }
}
if (!function_exists('vmo_dashboard_normalize_assoc')) {
    function vmo_dashboard_normalize_assoc($value): array {
        if (is_array($value)) {
            return $value;
        }

        if (is_object($value)) {
            $value = json_decode(wp_json_encode($value), true);
            return is_array($value) ? $value : array();
        }

        if (is_string($value) && $value !== '') {
            $decoded = json_decode($value, true);
            if (is_array($decoded)) {
                return $decoded;
            }
        }

        return array();
    }
}
if (!function_exists('vmo_dashboard_seed_metric_value')) {
    function vmo_dashboard_seed_metric_value(array $seed_kpis, string $metric, $default = null) {
        if (!isset($seed_kpis[$metric]) || !is_array($seed_kpis[$metric])) {
            return $default;
        }

        return array_key_exists('value', $seed_kpis[$metric]) ? $seed_kpis[$metric]['value'] : $default;
    }
}
if (!function_exists('vmo_dashboard_seed_metric_truthy')) {
    function vmo_dashboard_seed_metric_truthy(array $seed_kpis, string $metric): bool {
        $value = vmo_dashboard_seed_metric_value($seed_kpis, $metric, null);

        if (is_bool($value)) {
            return $value;
        }

        if (is_numeric($value)) {
            return ((float) $value) > 0;
        }

        $normalized = strtolower(trim((string) $value));
        return in_array($normalized, array('1', 'true', 'yes', 'ready', 'active', 'completed', 'draft_ready', 'publish_ready', 'success'), true);
    }
}
if (!function_exists('vmo_dashboard_website_readiness_flags')) {
    function vmo_dashboard_website_readiness_flags(array $website_state, array $website_payload): array {
        $draft_assembled = !empty($website_state['draft_assembled'])
            || !empty($website_payload['draft_assembled'])
            || !empty($website_state['draft_post_id'])
            || !empty($website_payload['post_id'])
            || !empty($website_state['preview_url'])
            || !empty($website_payload['preview_url']);

        $qa_report = is_array($website_payload['qa_report'] ?? null)
            ? $website_payload['qa_report']
            : (is_array($website_state['qa_report'] ?? null) ? $website_state['qa_report'] : array());
        $checklist = is_array($qa_report['checklist'] ?? null) ? $qa_report['checklist'] : array();

        $has_media_flag = array_key_exists('media_complete', $website_state)
            || array_key_exists('media_complete', $website_payload)
            || array_key_exists('media_complete', $checklist);

        if (array_key_exists('media_complete', $website_state)) {
            $media_complete = !empty($website_state['media_complete']);
        } elseif (array_key_exists('media_complete', $website_payload)) {
            $media_complete = !empty($website_payload['media_complete']);
        } elseif (array_key_exists('media_complete', $checklist)) {
            $media_complete = !empty($checklist['media_complete']);
        } else {
            $media_complete = false;
        }

        $preview_ready = (!empty($website_state['preview_ready']) || !empty($website_payload['preview_ready'])) && $media_complete;
        if (!$preview_ready && (!empty($website_state['draft_ready']) || !empty($website_payload['draft_ready']))) {
            $preview_ready = $media_complete;
        }

        return array(
            'draft_assembled' => $draft_assembled,
            'media_complete' => $media_complete,
            'preview_ready' => $preview_ready,
        );
    }
}
if (!function_exists('vmo_dashboard_get_generation_context')) {
    function vmo_dashboard_get_generation_context(int $user_id): array {
        global $wpdb;

        $table_exists = function(string $table) use ($wpdb): bool {
            if (function_exists('vmo_table_exists')) {
                return vmo_table_exists($table);
            }

            return ((string) $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $wpdb->esc_like($table))) === $table);
        };

        $context = array(
            'operating_state' => array(),
            'website' => array(),
            'seed_kpis' => array(),
            'kpi_model' => array(),
            'dashboard_modules' => array(),
        );

        $kpi_table = $wpdb->prefix . 'vmo_business_kpis';
        if ($table_exists($kpi_table)) {
            $rows = $wpdb->get_results($wpdb->prepare(
                "SELECT metric, value, label, type, category, updated_at FROM {$kpi_table} WHERE user_id = %d",
                $user_id
            ), ARRAY_A);

            foreach (($rows ?: array()) as $row) {
                $metric = (string) ($row['metric'] ?? '');
                if ($metric !== '') {
                    $context['seed_kpis'][$metric] = $row;
                }
            }
        }

        $state = vmo_dashboard_normalize_assoc(get_user_meta($user_id, 'vmo_business_operating_state', true));
        $website_payload = array();
        $consultations_table = $wpdb->prefix . 'vmo_consultations';
        if ($table_exists($consultations_table)) {
            $website_data_raw = $wpdb->get_var($wpdb->prepare(
                "SELECT website_data FROM {$consultations_table} WHERE user_id = %d AND status IN ('completed','implemented','generating_media') AND website_data IS NOT NULL AND website_data != '' ORDER BY updated_at DESC, id DESC LIMIT 1",
                $user_id
            ));
            $website_data = vmo_dashboard_normalize_assoc($website_data_raw);

            if (empty($state) && !empty($website_data['operating_state'])) {
                $state = vmo_dashboard_normalize_assoc($website_data['operating_state']);
            }

            if (!empty($website_data['website'])) {
                $website_payload = vmo_dashboard_normalize_assoc($website_data['website']);
            } elseif (!empty($website_data['preview_url']) || !empty($website_data['post_id']) || !empty($website_data['homepage_html'])) {
                $website_payload = $website_data;
            }
        }

        $context['operating_state'] = $state;
        $context['website'] = $website_payload;

        $phase_outputs_table = $wpdb->prefix . 'vmo_phase_outputs';
        if ($table_exists($phase_outputs_table)) {
            foreach (array('kpi_model', 'dashboard_modules') as $output_type) {
                $output_json = $wpdb->get_var($wpdb->prepare(
                    "SELECT output_json FROM {$phase_outputs_table} WHERE user_id = %d AND output_type = %s AND is_active = 1 ORDER BY created_at DESC LIMIT 1",
                    $user_id,
                    $output_type
                ));
                $decoded = vmo_dashboard_normalize_assoc($output_json);
                if (!empty($decoded)) {
                    $context[$output_type] = $decoded;
                }
            }
        }

        $website_state = is_array($state['website'] ?? null) ? $state['website'] : array();
        $systems_state = is_array($state['systems'] ?? null) ? $state['systems'] : array();
        $website_flags = vmo_dashboard_website_readiness_flags($website_state, $website_payload);
        $draft_assembled = !empty($website_flags['draft_assembled']);
        $media_complete = !empty($website_flags['media_complete']);
        $preview_ready = !empty($website_flags['preview_ready']);
        $pages_count = (int) ($website_state['pages_count'] ?? vmo_dashboard_seed_metric_value($context['seed_kpis'], 'pages_count', 0));
        if ($pages_count <= 0 && is_array($website_payload['pages_created'] ?? null)) {
            $pages_count = count($website_payload['pages_created']);
        }

        $website_ready = $preview_ready;
        $payments_enabled = !empty($systems_state['payments']['enabled']) || vmo_dashboard_seed_metric_truthy($context['seed_kpis'], 'payment_system_ready');
        $payments_ready = !empty($systems_state['payments']['checkout_ready']) || vmo_dashboard_seed_metric_truthy($context['seed_kpis'], 'payment_system_ready');
        $phone_ready = !empty($systems_state['phone']['ready']) || vmo_dashboard_seed_metric_truthy($context['seed_kpis'], 'phone_system_ready');
        $email_ready = !empty($systems_state['email_marketing']['ready']) || vmo_dashboard_seed_metric_truthy($context['seed_kpis'], 'email_marketing_ready');
        $seo_ready = !empty($systems_state['seo']['ready']) || vmo_dashboard_seed_metric_truthy($context['seed_kpis'], 'seo_system_ready');
        $crm_ready = !empty($systems_state['contact_management']['ready']) || vmo_dashboard_seed_metric_truthy($context['seed_kpis'], 'contact_management_ready');

        if (empty($context['dashboard_modules'])) {
            $context['dashboard_modules'] = array(
                'revenue' => array(
                    'enabled' => $payments_enabled,
                    'status' => $payments_ready ? 'ready' : ($payments_enabled ? 'in_progress' : 'pending'),
                ),
                'traffic' => array(
                    'enabled' => $website_ready,
                    'status' => !empty($website_state['publish_ready']) ? 'ready' : ($website_ready ? 'in_progress' : 'pending'),
                ),
                'calls' => array(
                    'enabled' => $phone_ready,
                    'status' => $phone_ready ? 'ready' : 'pending',
                ),
                'conversions' => array(
                    'enabled' => $website_ready && ($payments_enabled || $crm_ready),
                    'status' => ($website_ready && ($payments_ready || $crm_ready)) ? 'ready' : (($website_ready || $payments_enabled || $crm_ready) ? 'in_progress' : 'pending'),
                ),
                'seo' => array(
                    'enabled' => $seo_ready,
                    'status' => $seo_ready ? 'ready' : 'pending',
                ),
                'crm' => array(
                    'enabled' => $crm_ready,
                    'status' => $crm_ready ? 'ready' : 'pending',
                ),
                'recommendations' => array(
                    'enabled' => !empty($state) || !empty($context['seed_kpis']),
                    'status' => (!empty($state) || !empty($context['seed_kpis'])) ? 'ready' : 'pending',
                ),
            );
        }

        if (empty($context['kpi_model'])) {
            $context['kpi_model'] = array(
                'metrics' => array(
                    'website_status',
                    'pages_count',
                    'setup_progress',
                    'payment_system_ready',
                    'phone_system_ready',
                    'email_marketing_ready',
                    'seo_system_ready',
                    'contact_management_ready',
                ),
                'targets' => array(
                    'pages_count' => max(1, $pages_count),
                    'setup_progress' => 100,
                    'website_status' => !empty($website_state['publish_ready']) ? 'publish_ready' : ($preview_ready ? 'draft_ready' : ($draft_assembled && !$media_complete ? 'media_generation_in_progress' : 'draft')),
                ),
                'measurement_frequency' => array(
                    'dashboard' => 'daily',
                    'website' => 'on_change',
                    'systems' => 'on_generation',
                ),
                'alert_rules' => array(
                    'website_qa' => 'Alert while website is not publish ready',
                    'systems' => 'Alert while required systems remain inactive',
                ),
            );
        }

        return $context;
    }
}
if (!function_exists('vmo_dashboard_build_setup_snapshot')) {
    function vmo_dashboard_build_setup_snapshot(array $context): array {
        $seed_kpis = is_array($context['seed_kpis'] ?? null) ? $context['seed_kpis'] : array();
        $operating_state = is_array($context['operating_state'] ?? null) ? $context['operating_state'] : array();
        $website_payload = is_array($context['website'] ?? null) ? $context['website'] : array();
        $dashboard_modules = is_array($context['dashboard_modules'] ?? null) ? $context['dashboard_modules'] : array();

        $business_state = is_array($operating_state['business'] ?? null) ? $operating_state['business'] : array();
        $website_state = is_array($operating_state['website'] ?? null) ? $operating_state['website'] : array();
        $systems_state = is_array($operating_state['systems'] ?? null) ? $operating_state['systems'] : array();
        $website_flags = vmo_dashboard_website_readiness_flags($website_state, $website_payload);
        $draft_assembled = !empty($website_flags['draft_assembled']);
        $media_complete = !empty($website_flags['media_complete']);
        $preview_ready = !empty($website_flags['preview_ready']);

        $pages_count = (int) ($website_state['pages_count'] ?? vmo_dashboard_seed_metric_value($seed_kpis, 'pages_count', 0));
        if ($pages_count <= 0 && is_array($website_payload['pages_created'] ?? null)) {
            $pages_count = count($website_payload['pages_created']);
        }

        $website_ready = $preview_ready;
        $publish_ready = !empty($website_state['publish_ready']) || ((string) vmo_dashboard_seed_metric_value($seed_kpis, 'website_status', '') === 'publish_ready');

        $qa_checklist = is_array($website_state['qa_checklist'] ?? null) ? $website_state['qa_checklist'] : array();
        $qa_pending = 0;
        foreach ($qa_checklist as $check) {
            if (is_array($check)) {
                $passed = !empty($check['passed']) || !empty($check['complete']) || !empty($check['resolved']) || (($check['status'] ?? '') === 'passed');
                if (!$passed) {
                    $qa_pending++;
                }
            } elseif (is_string($check) && trim($check) !== '') {
                $qa_pending++;
            }
        }

        $payments_enabled = !empty($systems_state['payments']['enabled']) || vmo_dashboard_seed_metric_truthy($seed_kpis, 'payment_system_ready');
        $payments_ready = !empty($systems_state['payments']['checkout_ready']) || vmo_dashboard_seed_metric_truthy($seed_kpis, 'payment_system_ready');
        $phone_ready = !empty($systems_state['phone']['ready']) || vmo_dashboard_seed_metric_truthy($seed_kpis, 'phone_system_ready');
        $email_ready = !empty($systems_state['email_marketing']['ready']) || vmo_dashboard_seed_metric_truthy($seed_kpis, 'email_marketing_ready');
        $seo_ready = !empty($systems_state['seo']['ready']) || vmo_dashboard_seed_metric_truthy($seed_kpis, 'seo_system_ready');
        $crm_ready = !empty($systems_state['contact_management']['ready']) || vmo_dashboard_seed_metric_truthy($seed_kpis, 'contact_management_ready');

        $next_milestone = trim((string) vmo_dashboard_seed_metric_value($seed_kpis, 'next_milestone', ''));
        if ($next_milestone === '') {
            if (!$draft_assembled) {
                $next_milestone = 'Complete website generation';
            } elseif (!$media_complete) {
                $next_milestone = 'Complete required media generation';
            } elseif ($qa_pending > 0 || !$publish_ready) {
                $next_milestone = 'Resolve website QA and publish';
            } elseif (!$payments_ready) {
                $next_milestone = 'Complete payments checkout setup';
            } elseif (!$crm_ready) {
                $next_milestone = 'Activate CRM and lead management';
            } else {
                $next_milestone = 'Monitor live KPIs and manage operations';
            }
        }

        $milestones = array(
            array('id' => 'website_draft', 'text' => 'Website draft assembled', 'done' => $draft_assembled),
            array('id' => 'website_media', 'text' => 'Required media generated and inserted', 'done' => $media_complete),
            array('id' => 'website_publish', 'text' => 'Website approved for publish', 'done' => $publish_ready),
            array('id' => 'payments', 'text' => 'Payments checkout ready', 'done' => $payments_ready),
            array('id' => 'phone', 'text' => 'Phone system ready', 'done' => $phone_ready),
            array('id' => 'email', 'text' => 'Email automations ready', 'done' => $email_ready),
            array('id' => 'seo', 'text' => 'SEO system ready', 'done' => $seo_ready),
            array('id' => 'crm', 'text' => 'CRM and contact management ready', 'done' => $crm_ready),
        );

        $setup_total = count($milestones);
        $setup_completed = count(array_filter($milestones, function($milestone) {
            return !empty($milestone['done']);
        }));
        $derived_progress = $setup_total > 0 ? (int) round(($setup_completed / max(1, $setup_total)) * 100) : 0;
        $seed_progress = (int) vmo_dashboard_seed_metric_value($seed_kpis, 'setup_progress', 0);
        $setup_progress = max($derived_progress, $seed_progress);

        $status_label = $publish_ready
            ? 'Website is publish ready and connected to the dashboard'
            : ($website_ready
                ? 'Website preview is ready and connected to the dashboard'
                : ($draft_assembled
                    ? 'Website draft exists, but required media is still generating before preview release'
                    : 'Website generation is still pending'));

        $tasks = array();
        if (!$draft_assembled) {
            $tasks[] = array('id' => 'website_generation', 'text' => 'Complete website generation', 'checked' => false);
        } elseif (!$media_complete) {
            $tasks[] = array('id' => 'website_media', 'text' => 'Wait for required media generation to finish before previewing the site', 'checked' => false);
        } else {
            $tasks[] = array(
                'id' => 'website_review',
                'text' => sprintf('Review %d generated page%s in the live preview', max(1, $pages_count), max(1, $pages_count) === 1 ? '' : 's'),
                'checked' => false,
            );
            if ($qa_pending > 0) {
                $tasks[] = array(
                    'id' => 'website_qa',
                    'text' => sprintf('Resolve %d website QA item%s before publishing', $qa_pending, $qa_pending === 1 ? '' : 's'),
                    'checked' => false,
                );
            } elseif (!$publish_ready) {
                $tasks[] = array('id' => 'publish_checks', 'text' => 'Run final publish checks on the website draft', 'checked' => false);
            } else {
                $tasks[] = array('id' => 'publish_live', 'text' => 'Publish the website and begin monitoring live KPIs', 'checked' => false);
            }
        }
        if (!$payments_ready) {
            $tasks[] = array('id' => 'payments_setup', 'text' => 'Complete payments checkout activation', 'checked' => false);
        }
        if (!$crm_ready) {
            $tasks[] = array('id' => 'crm_setup', 'text' => 'Activate CRM and contact management', 'checked' => false);
        }
        if (!$email_ready) {
            $tasks[] = array('id' => 'email_setup', 'text' => 'Enable email marketing automations', 'checked' => false);
        }
        if (!$seo_ready) {
            $tasks[] = array('id' => 'seo_setup', 'text' => 'Enable SEO monitoring and optimization', 'checked' => false);
        }
        if (!$phone_ready) {
            $tasks[] = array('id' => 'phone_setup', 'text' => 'Activate the business phone system', 'checked' => false);
        }

        $active_module_labels = array();
        foreach ($dashboard_modules as $module_name => $module_state) {
            $status = (string) ($module_state['status'] ?? (!empty($module_state['enabled']) ? 'ready' : 'pending'));
            if ($status === 'ready') {
                $active_module_labels[] = $module_name;
            }
        }

        $insights = array();
        if ($website_ready) {
            $insights[] = $publish_ready
                ? sprintf('Your %d-page website is now connected to the dashboard and ready for launch operations.', max(1, $pages_count))
                : sprintf('Your %d-page website draft is connected to the dashboard; complete the remaining publish steps to go live.', max(1, $pages_count));
        } elseif ($draft_assembled) {
            $insights[] = 'The website draft exists, but the dashboard is holding preview access until required media generation finishes.';
        } else {
            $insights[] = 'The dashboard is waiting for website generation to complete.';
        }
        if ($qa_pending > 0) {
            $insights[] = sprintf('%d QA item%s still need attention before the website can be published.', $qa_pending, $qa_pending === 1 ? '' : 's');
        }
        if (!empty($active_module_labels)) {
            $insights[] = 'Active dashboard modules: ' . implode(', ', $active_module_labels) . '.';
        } else {
            $insights[] = 'Business systems will appear here as each service is activated.';
        }

        $module_counts = array('active' => 0, 'in_progress' => 0, 'pending' => 0);
        foreach ($dashboard_modules as $module_state) {
            $status = (string) ($module_state['status'] ?? (!empty($module_state['enabled']) ? 'ready' : 'pending'));
            if ($status === 'ready') {
                $module_counts['active']++;
            } elseif ($status === 'in_progress') {
                $module_counts['in_progress']++;
            } else {
                $module_counts['pending']++;
            }
        }

        return array(
            'business' => array(
                'name' => (string) ($business_state['name'] ?? vmo_dashboard_seed_metric_value($seed_kpis, 'business_name', '')),
                'type' => (string) ($business_state['type'] ?? vmo_dashboard_seed_metric_value($seed_kpis, 'business_type', '')),
                'industry' => (string) ($business_state['industry'] ?? vmo_dashboard_seed_metric_value($seed_kpis, 'industry', '')),
            ),
            'website' => array(
                'draft_assembled' => $draft_assembled,
                'media_complete' => $media_complete,
                'preview_ready' => $website_ready,
                'draft_ready' => $website_ready,
                'publish_ready' => $publish_ready,
                'pages_count' => $pages_count,
                'qa_pending' => $qa_pending,
                'status' => $publish_ready ? 'publish_ready' : ($website_ready ? 'draft_ready' : ($draft_assembled ? 'media_generation_in_progress' : 'pending')),
                'status_label' => $status_label,
                'preview_url' => (string) ($website_payload['preview_url'] ?? ''),
                'post_id' => (int) ($website_payload['post_id'] ?? 0),
                'project_title' => (string) ($website_payload['project_title'] ?? ''),
            ),
            'systems' => array(
                'payments' => array('enabled' => $payments_enabled, 'ready' => $payments_ready),
                'phone' => array('ready' => $phone_ready),
                'email_marketing' => array('ready' => $email_ready),
                'seo' => array('ready' => $seo_ready),
                'contact_management' => array('ready' => $crm_ready),
            ),
            'setup' => array(
                'progress' => $setup_progress,
                'completed' => $setup_completed,
                'total' => $setup_total,
                'blockers' => count(array_filter($milestones, function($milestone) {
                    return empty($milestone['done']);
                })) + $qa_pending,
                'milestones' => $milestones,
                'next_milestone' => $next_milestone,
            ),
            'tasks' => array_slice($tasks, 0, 10),
            'insights' => array_slice(array_values(array_filter($insights)), 0, 5),
            'modules' => $module_counts,
        );
    }
}
function vmo_dashboard_stats() {
    $vmo_dashboard_stats_buffer_level = ob_get_level();
    if (!current_user_can('read')) {
        while (ob_get_level() > $vmo_dashboard_stats_buffer_level) { @ob_end_clean(); }
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if ((isset($_POST['security']) || isset($_POST['nonce'])) && !vmo_verify_admin_security()) {
        while (ob_get_level() > $vmo_dashboard_stats_buffer_level) { @ob_end_clean(); }
        wp_send_json_error(['message' => 'Invalid nonce', 'status' => 'invalid_nonce']);
    }

    global $wpdb;
    if (is_object($wpdb)) {
        if (method_exists($wpdb, 'hide_errors')) { $wpdb->hide_errors(); }
        if (method_exists($wpdb, 'suppress_errors')) { $wpdb->suppress_errors(true); }
    }
    $vmo_send_dashboard_success = static function($data) use ($vmo_dashboard_stats_buffer_level, $wpdb) {
        while (ob_get_level() > $vmo_dashboard_stats_buffer_level) { @ob_end_clean(); }
        if (is_object($wpdb) && !empty($wpdb->last_error)) {
            error_log('[VMO dashboard_stats] SQL warning: ' . $wpdb->last_error);
            $wpdb->last_error = '';
        }
        wp_send_json_success($data);
    };

    $user_id = get_current_user_id();
    $dashboard_context = vmo_dashboard_get_generation_context($user_id);
    $setup_snapshot = vmo_dashboard_build_setup_snapshot($dashboard_context);
    // Handle scoped requests early for specific panels
    $scope = isset($_POST['scope']) ? sanitize_text_field(wp_unslash($_POST['scope'])) : '';
    if ($scope) {
        // Payments KPIs and recent transactions
        if ($scope === 'payments') {
            $orders_table = $wpdb->prefix . 'vmo_store_orders';
            $exists = vmo_table_exists($orders_table);
            $sum_period = function($days) use ($wpdb, $orders_table, $user_id, $exists) {
                if (!$exists) return 0;
                $since = date('Y-m-d H:i:s', strtotime('-' . (int)$days . ' days'));
                $q = $wpdb->get_var($wpdb->prepare("SELECT COALESCE(SUM(amount_total),0) FROM {$orders_table} WHERE user_id = %d AND status='paid' AND created_at >= %s", $user_id, $since));
                return (int) $q;
            };
            $day = $sum_period(1);
            $week = $sum_period(7);
            $month = $sum_period(30);
            $year = $sum_period(365);

            // Month-over-month: compare last 30 days vs previous 30 days
            $mom_change = 0.0;
            if ($exists) {
                $end_current = current_time('mysql');
                $start_current = date('Y-m-d H:i:s', strtotime('-30 days', strtotime($end_current)));
                $start_prev = date('Y-m-d H:i:s', strtotime('-60 days', strtotime($end_current)));
                $sum_current = (int) $wpdb->get_var($wpdb->prepare(
                    "SELECT COALESCE(SUM(amount_total),0) FROM {$orders_table} WHERE user_id=%d AND status='paid' AND created_at BETWEEN %s AND %s",
                    $user_id, $start_current, $end_current
                ));
                $sum_prev = (int) $wpdb->get_var($wpdb->prepare(
                    "SELECT COALESCE(SUM(amount_total),0) FROM {$orders_table} WHERE user_id=%d AND status='paid' AND created_at BETWEEN %s AND %s",
                    $user_id, $start_prev, $start_current
                ));
                if ($sum_prev > 0) {
                    $mom_change = round((($sum_current - $sum_prev) / $sum_prev) * 100, 2);
                } else {
                    $mom_change = $sum_current > 0 ? 100.0 : 0.0;
                }
            }

            // Recent transactions (latest 20)
            $transactions = [];
            $fulfillment_summary = function_exists('vmo_business_flow_revenue_summary')
                ? vmo_business_flow_revenue_summary($user_id, 365)
                : ['paid_revenue' => 0, 'pending_fulfillment_revenue' => 0, 'completed_fulfilled_revenue' => 0, 'refunds' => 0, 'fees' => 0, 'taxes' => 0, 'net_revenue' => 0];
            if ($exists) {
                $rows = $wpdb->get_results($wpdb->prepare(
                    "SELECT id, created_at, amount_total, currency, status, fulfillment_status, business_flow_status, business_flow_type, business_flow_instance_id, fulfillment_task_id, metadata FROM {$orders_table} WHERE user_id=%d ORDER BY created_at DESC LIMIT 20",
                    $user_id
                ), ARRAY_A);
                foreach (($rows ?: []) as $r) {
                    $meta = [];
                    if (!empty($r['metadata'])) {
                        $meta = json_decode((string) $r['metadata'], true);
                        if (!is_array($meta)) $meta = [];
                    }
                    $product_label = (string) ($meta['product_name'] ?? '');
                    if ($product_label === '' && !empty($meta['product_id'])) {
                        $product_label = 'Product #' . (int) $meta['product_id'];
                    }
                    $transactions[] = [
                        'id' => (int) $r['id'],
                        'date' => (string) $r['created_at'],
                        'product' => $product_label,
                        'amount' => (int) $r['amount_total'],
                        'currency' => (string) ($r['currency'] ?? 'usd'),
                        'status' => (string) ($r['status'] ?? ''),
                        'fulfillment_status' => (string) ($r['fulfillment_status'] ?? ($meta['last_business_flow_status'] ?? '')),
                        'business_flow_status' => (string) ($r['business_flow_status'] ?? ($meta['last_business_flow_status'] ?? '')),
                        'business_flow_type' => (string) ($r['business_flow_type'] ?? ($meta['business_flow_plan']['business_flow_type'] ?? '')),
                        'business_flow_instance_id' => (int) ($r['business_flow_instance_id'] ?? 0),
                        'fulfillment_task_id' => (int) ($r['fulfillment_task_id'] ?? 0),
                    ];
                }
            }

            // Daily sales for last 30 days (for Sales Trend line chart)
            $daily_sales = [];
            if ($exists) {
                $daily_rows = $wpdb->get_results($wpdb->prepare(
                    "SELECT DATE(created_at) as sale_date, COALESCE(SUM(amount_total),0) as total FROM {$orders_table} WHERE user_id=%d AND status='paid' AND created_at >= %s GROUP BY DATE(created_at) ORDER BY sale_date ASC",
                    $user_id, date('Y-m-d', strtotime('-30 days'))
                ), ARRAY_A);
                // Fill in missing days with 0
                $date_map = [];
                foreach (($daily_rows ?: []) as $dr) {
                    $date_map[$dr['sale_date']] = (int) $dr['total'];
                }
                for ($i = 29; $i >= 0; $i--) {
                    $d_key = date('Y-m-d', strtotime("-{$i} days"));
                    $daily_sales[] = [
                        'date' => $d_key,
                        'label' => date('M j', strtotime($d_key)),
                        'total' => $date_map[$d_key] ?? 0,
                    ];
                }
            }

            // Monthly sales for last 12 months (for bar chart)
            $monthly_sales = [];
            if ($exists) {
                $monthly_rows = $wpdb->get_results($wpdb->prepare(
                    "SELECT DATE_FORMAT(created_at, '%%Y-%%m') as sale_month, COALESCE(SUM(amount_total),0) as total FROM {$orders_table} WHERE user_id=%d AND status='paid' AND created_at >= %s GROUP BY DATE_FORMAT(created_at, '%%Y-%%m') ORDER BY sale_month ASC",
                    $user_id, date('Y-m-d', strtotime('-12 months'))
                ), ARRAY_A);
                $month_map = [];
                foreach (($monthly_rows ?: []) as $mr) {
                    $month_map[$mr['sale_month']] = (int) $mr['total'];
                }
                for ($i = 11; $i >= 0; $i--) {
                    $m_key = date('Y-m', strtotime("-{$i} months"));
                    $monthly_sales[] = [
                        'month' => $m_key,
                        'label' => date('M Y', strtotime($m_key . '-01')),
                        'total' => $month_map[$m_key] ?? 0,
                    ];
                }
            }

            $vmo_send_dashboard_success([
                'day' => $day,
                'week' => $week,
                'month' => $month,
                'year' => $year,
                'mom_change' => $mom_change,
                'transactions' => $transactions,
                'daily_sales' => $daily_sales,
                'monthly_sales' => $monthly_sales,
                'fulfillment_summary' => $fulfillment_summary,
            ]);
        }

        // Live calls snapshot
        if ($scope === 'live_calls') {
            $calls_table = $wpdb->prefix . 'vmo_business_calls';
            $exists = vmo_table_exists($calls_table);
            $live_calls = [];
            if ($exists) {
                $since = date('Y-m-d H:i:s', strtotime('-15 minutes', current_time('timestamp')));
                $rows = $wpdb->get_results($wpdb->prepare(
                    "SELECT id, caller, agent_name, call_status FROM {$calls_table} WHERE user_id=%d AND call_start_time >= %s AND call_status IN ('ringing','in-progress') ORDER BY call_start_time DESC LIMIT 20",
                    $user_id, $since
                ), ARRAY_A);
                foreach (($rows ?: []) as $r) {
                    $live_calls[] = [
                        'id' => (int) $r['id'],
                        'caller' => (string) ($r['caller'] ?? ''),
                        'agent' => (string) ($r['agent_name'] ?? ''),
                        'status' => (string) ($r['call_status'] ?? '')
                    ];
                }
            }
            $vmo_send_dashboard_success(['live_calls' => $live_calls]);
        }

        // Call history
        if ($scope === 'call_history') {
            $calls_table = $wpdb->prefix . 'vmo_business_calls';
            $exists = vmo_table_exists($calls_table);
            $history = [];
            if ($exists) {
                $rows = $wpdb->get_results($wpdb->prepare(
                    "SELECT id, caller, call_duration, call_status FROM {$calls_table} WHERE user_id=%d ORDER BY call_start_time DESC LIMIT 50",
                    $user_id
                ), ARRAY_A);
                foreach (($rows ?: []) as $r) {
                    $history[] = [
                        'id' => (int) $r['id'],
                        'caller' => (string) ($r['caller'] ?? ''),
                        'duration' => (int) ($r['call_duration'] ?? 0),
                        'result' => (string) ($r['call_status'] ?? '')
                    ];
                }
            }
            $vmo_send_dashboard_success(['call_history' => $history]);
        }
    }
    $counts = [
        'contacts' => 0,
        'pending_approvals' => 0,
        'calls_today' => 0,
        'avg_call_duration' => 0,
        // Added KPIs for header wiring
        'goals_complete_percent' => 0,
        'growth_alerts' => 0,
        // Business progress additions
        'short_term' => [
            'percent' => 0,
            'completed' => 0,
            'total' => 0,
            'today_percent' => 0,
        ],
        'long_term' => [
            'percent' => 0,
            'note' => '—'
        ],
        // Action items + weekly strategy
        'tasks_today' => [],
        'weekly' => [
            'title' => '📊 Weekly Strategy Briefing',
            'insights' => [],
            'actions' => []
        ],
        // Business Intelligence KPIs
        'bi' => [
            'ranking' => '—',
            'growth' => 0,
            'next_milestone' => '—',
            'monitored' => 0,
            'new_threats' => 0,
            'opportunities' => 0,
        ],
    ];

    $table_exists = function($table) {
        global $wpdb;
        $like = $wpdb->esc_like($table);
        return (bool) $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $like));
    };

    $contacts_table = $wpdb->prefix . 'vmo_contacts';
    if ($table_exists($contacts_table)) {
        $counts['contacts'] = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$contacts_table} WHERE user_id = %d", $user_id));
    }

    // Prefer progress based on scheduled tasks (follow-ups) if available
    $followups_table = $wpdb->prefix . 'vmo_follow_ups';
    $approval_table = $wpdb->prefix . 'vmo_approval_queue';
    $fu_exists = $table_exists($followups_table);
    if ($fu_exists) {
        // Goals complete percent: completed tasks vs all tasks in last 30 days (scheduled)
        $since30 = date('Y-m-d H:i:s', strtotime('-30 days'));
        $fu_total_30 = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$followups_table} WHERE user_id=%d AND scheduled_date >= %s", $user_id, $since30));
        $fu_done_30  = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$followups_table} WHERE user_id=%d AND status='completed' AND scheduled_date >= %s", $user_id, $since30));
        $counts['goals_complete_percent'] = ($fu_total_30 > 0) ? (int) round(($fu_done_30 / max(1,$fu_total_30)) * 100) : 0;

        // Short-term (0-3mo) progress based on tasks scheduled within next 90 days
        $now = current_time('mysql');
        $to90 = date('Y-m-d H:i:s', strtotime('+90 days', current_time('timestamp')));
        $st_total = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$followups_table} WHERE user_id=%d AND scheduled_date BETWEEN %s AND %s", $user_id, $now, $to90));
        $st_done  = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$followups_table} WHERE user_id=%d AND status='completed' AND scheduled_date BETWEEN %s AND %s", $user_id, $now, $to90));
        $counts['short_term']['total'] = $st_total;
        $counts['short_term']['completed'] = $st_done;
        $counts['short_term']['percent'] = ($st_total > 0) ? (int) round(($st_done / max(1,$st_total)) * 100) : 0;
        // Today percent: tasks scheduled today, completed vs total
        $todayStart = date('Y-m-d 00:00:00', current_time('timestamp'));
        $todayEnd   = date('Y-m-d 23:59:59', current_time('timestamp'));
        $st_today_total = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$followups_table} WHERE user_id=%d AND scheduled_date BETWEEN %s AND %s", $user_id, $todayStart, $todayEnd));
        $st_today_done = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$followups_table} WHERE user_id=%d AND status='completed' AND scheduled_date BETWEEN %s AND %s", $user_id, $todayStart, $todayEnd));
        $counts['short_term']['today_percent'] = ($st_today_total > 0) ? (int) round(($st_today_done / max(1,$st_today_total)) * 100) : 0;

        // Long-term vision percent (12mo+): tasks scheduled within next 365 days
        $to365 = date('Y-m-d H:i:s', strtotime('+365 days', current_time('timestamp')));
        $lt_total = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$followups_table} WHERE user_id=%d AND scheduled_date BETWEEN %s AND %s", $user_id, $now, $to365));
        $lt_done  = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$followups_table} WHERE user_id=%d AND status='completed' AND scheduled_date BETWEEN %s AND %s", $user_id, $now, $to365));
        $counts['long_term']['percent'] = ($lt_total > 0) ? (int) round(($lt_done / max(1,$lt_total)) * 100) : 0;
        $counts['long_term']['note'] = $lt_total > 0 ? __('Progress based on scheduled roadmap tasks', 'vmo') : '—';
    } elseif ($table_exists($approval_table)) {
        // Fallback to approvals if follow-ups table not present
        $counts['pending_approvals'] = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$approval_table} WHERE user_id = %d AND status = 'pending'", $user_id));
        $since30 = date('Y-m-d H:i:s', strtotime('-30 days'));
        $total_items_30 = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$approval_table} WHERE user_id = %d AND created_at >= %s", $user_id, $since30));
        $approved_30 = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$approval_table} WHERE user_id = %d AND status = 'approved' AND created_at >= %s", $user_id, $since30));
        $counts['goals_complete_percent'] = ($total_items_30 >= 5) ? (int) round(($approved_30 / max(1,$total_items_30)) * 100) : 0;
        $since90 = date('Y-m-d H:i:s', strtotime('-90 days'));
        $st_total = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$approval_table} WHERE user_id = %d AND created_at >= %s", $user_id, $since90));
        $st_done = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$approval_table} WHERE user_id = %d AND status = 'approved' AND created_at >= %s", $user_id, $since90));
        $counts['short_term']['total'] = $st_total;
        $counts['short_term']['completed'] = $st_done;
        $counts['short_term']['percent'] = ($st_total >= 5) ? (int) round(($st_done / max(1,$st_total)) * 100) : 0;
        $todayStart = date('Y-m-d 00:00:00', current_time('timestamp'));
        $st_today_total = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$approval_table} WHERE user_id = %d AND created_at >= %s", $user_id, $todayStart));
        $st_today_done = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$approval_table} WHERE user_id = %d AND status = 'approved' AND created_at >= %s", $user_id, $todayStart));
        $counts['short_term']['today_percent'] = ($st_today_total > 0) ? (int) round(($st_today_done / $st_today_total) * 100) : 0;
        $since365 = date('Y-m-d H:i:s', strtotime('-365 days'));
        $lt_total = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$approval_table} WHERE user_id = %d AND created_at >= %s", $user_id, $since365));
        $lt_done = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$approval_table} WHERE user_id = %d AND status = 'approved' AND created_at >= %s", $user_id, $since365));
        $counts['long_term']['percent'] = ($lt_total >= 10) ? (int) round(($lt_done / max(1,$lt_total)) * 100) : 0;
        $counts['long_term']['note'] = ($lt_total >= 10) ? __('On track for long-term targets', 'vmo') : '—';
    }

    // Populate Business Intelligence KPIs based on actual consultation completion
    try {
        // Check if user has completed business consultation
        $consultation_table = $wpdb->prefix . 'vmo_consultations';
        $has_consultations = $table_exists($consultation_table);
        $completed_consultation = false;
        
        if ($has_consultations) {
            $completed_consultation = (bool) $wpdb->get_var($wpdb->prepare(
                "SELECT COUNT(*) FROM {$consultation_table} WHERE user_id=%d AND status IN ('completed','implemented','generating_media')", 
                $user_id
            ));
        }
        
        // Check if user has an active website/domain
        $domains_table = $wpdb->prefix . 'vmo_domains';
        $has_domains = $table_exists($domains_table);
        $active_website = false;
        
        if ($has_domains) {
            $active_website = (bool) $wpdb->get_var($wpdb->prepare(
                "SELECT COUNT(*) FROM {$domains_table} WHERE user_id=%d AND status='active'", 
                $user_id
            ));
        }
        
        // Only show actual BI data if consultation is completed AND website exists
        if ($completed_consultation && $active_website) {
            $orders_table = $wpdb->prefix . 'vmo_store_orders';
            $interactions_table = $wpdb->prefix . 'vmo_contact_interactions';
            $has_orders = $table_exists($orders_table);
            $has_interactions = $table_exists($interactions_table);
            
            // Growth: compare visits last 30d vs prev 30d (only if data exists)
            $end = current_time('mysql');
            $start30 = date('Y-m-d H:i:s', strtotime('-30 days', strtotime($end)));
            $prevStart = date('Y-m-d H:i:s', strtotime('-60 days', strtotime($end)));
            $curr = 0; $prev = 0;
            
            if ($has_interactions) {
                $curr = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$interactions_table} WHERE user_id=%d AND interaction_type='website_visit' AND created_at BETWEEN %s AND %s", $user_id, $start30, $end));
                $prev = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$interactions_table} WHERE user_id=%d AND interaction_type='website_visit' AND created_at BETWEEN %s AND %s", $user_id, $prevStart, $start30));
            } elseif ($has_orders) {
                $curr = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$orders_table} WHERE user_id=%d AND status='paid' AND created_at BETWEEN %s AND %s", $user_id, $start30, $end));
                $prev = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$orders_table} WHERE user_id=%d AND status='paid' AND created_at BETWEEN %s AND %s", $user_id, $prevStart, $start30));
            }
            
            if ($curr > 0 || $prev > 0) {
                $growth = ($prev > 0) ? round((($curr - $prev) / max(1,$prev)) * 100, 1) : ($curr > 0 ? 100.0 : 0.0);
                $counts['bi']['growth'] = $growth;
                
                // Ranking: only show if we have significant data
                if ($curr >= 10 || $prev >= 10) {
                    $counts['bi']['ranking'] = ($growth >= 50 ? '#1' : ($growth >= 20 ? '#2' : ($growth >= 10 ? '#3' : '#4+')));
                } else {
                    $counts['bi']['ranking'] = __('Analyzing...', 'vmo');
                }
                
                // Next milestone based on actual performance
                $counts['bi']['next_milestone'] = ($growth >= 20) ? __('Sustain 20%+ MoM for 60 days', 'vmo') : __('Achieve 20% MoM growth', 'vmo');
            } else {
                // No traffic data yet
                $counts['bi']['growth'] = 0;
                $counts['bi']['ranking'] = __('Building baseline...', 'vmo');
                $counts['bi']['next_milestone'] = __('Establish website traffic baseline', 'vmo');
            }
            
            // Monitored competitors: only from actual connections
            $connected_accounts_table = $wpdb->prefix . 'vmo_connected_accounts';
            $has_accounts = $table_exists($connected_accounts_table);
            $counts['bi']['monitored'] = $has_accounts ? (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$connected_accounts_table} WHERE user_id=%d AND provider LIKE '%%competitor%%'", $user_id)) : 0;
            
            // New threats/opportunities: only from real data
            $since7 = date('Y-m-d H:i:s', strtotime('-7 days'));
            $counts['bi']['new_threats'] = $table_exists($approval_table) ? (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$approval_table} WHERE user_id=%d AND status='pending' AND created_at >= %s", $user_id, $since7)) : 0;
            $counts['bi']['opportunities'] = $table_exists($followups_table) ? (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$followups_table} WHERE user_id=%d AND status='pending' AND scheduled_date >= %s", $user_id, $since7)) : 0;
        } else {
            // No consultation completed or no website - show pending status
            $counts['bi']['ranking'] = __('Consultation pending', 'vmo');
            $counts['bi']['growth'] = 0;
            if (!$completed_consultation) {
                $counts['bi']['next_milestone'] = __('Complete business consultation', 'vmo');
            } elseif (!$active_website) {
                $counts['bi']['next_milestone'] = __('Launch website & begin tracking', 'vmo');
            } else {
                $counts['bi']['next_milestone'] = __('Establish baseline metrics', 'vmo');
            }
            $counts['bi']['monitored'] = 0;
            $counts['bi']['new_threats'] = 0;
            $counts['bi']['opportunities'] = 0;
        }
    } catch (Exception $e) {
        // Fallback to pending state if any error occurs
        $counts['bi']['ranking'] = __('Analysis pending', 'vmo');
        $counts['bi']['growth'] = 0;
        $counts['bi']['next_milestone'] = __('Complete business consultation', 'vmo');
        $counts['bi']['monitored'] = 0;
        $counts['bi']['new_threats'] = 0;
        $counts['bi']['opportunities'] = 0;
    }

    // Phone metrics - check both legacy and new phone tables
    $legacy_calls_table = $wpdb->prefix . 'vmo_business_calls';
    $new_calls_table = $wpdb->prefix . 'vmo_phone_calls';
    $pd_calls_table = $wpdb->prefix . 'vmo_power_dialer_calls';
    
    $calls_today = 0;
    $avg_duration = 0;
    $missed_calls_7 = 0;
    
    // Try new phone_calls table first (Twilio integration)
    if ($table_exists($new_calls_table)) {
        $today = current_time('Y-m-d');
        $is_admin_stats = current_user_can('manage_options');
        
        $calls_today = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$new_calls_table} WHERE user_id = %d AND DATE(created_at) = %s",
            $user_id,
            $today
        ));

        // Admin fallback: if no calls for this user_id, check all users
        if ($calls_today === 0 && $is_admin_stats) {
            $calls_today = (int) $wpdb->get_var($wpdb->prepare(
                "SELECT COUNT(*) FROM {$new_calls_table} WHERE DATE(created_at) = %s",
                $today
            ));
        }

        $user_filter_sql = ($calls_today > 0 || !$is_admin_stats) ? 'user_id = %d AND' : '';
        $user_filter_params = ($calls_today > 0 || !$is_admin_stats) ? array( $user_id ) : array();

        $sum_dur_today = (int) $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COALESCE(SUM(duration),0) FROM {$new_calls_table} WHERE {$user_filter_sql} DATE(created_at) = %s AND duration > 0",
                ...array_merge( $user_filter_params, array( $today ) )
            )
        );
        $cnt_dur_today = (int) $wpdb->get_var(
            $wpdb->prepare(
                "SELECT COUNT(*) FROM {$new_calls_table} WHERE {$user_filter_sql} DATE(created_at) = %s AND duration > 0",
                ...array_merge( $user_filter_params, array( $today ) )
            )
        );

        // Include Power Dialer campaign calls (campaign_id > 0) without double-counting direct dialer calls.
        if ($table_exists($pd_calls_table)) {
            $calls_today += (int) $wpdb->get_var($wpdb->prepare(
                "SELECT COUNT(*) FROM {$pd_calls_table} WHERE user_id = %d AND campaign_id > 0 AND DATE(created_at) = %s",
                $user_id,
                $today
            ));

            $sum_dur_today += (int) $wpdb->get_var($wpdb->prepare(
                "SELECT COALESCE(SUM(duration),0) FROM {$pd_calls_table} WHERE user_id = %d AND campaign_id > 0 AND DATE(created_at) = %s AND duration > 0",
                $user_id,
                $today
            ));
            $cnt_dur_today += (int) $wpdb->get_var($wpdb->prepare(
                "SELECT COUNT(*) FROM {$pd_calls_table} WHERE user_id = %d AND campaign_id > 0 AND DATE(created_at) = %s AND duration > 0",
                $user_id,
                $today
            ));
        }

        $avg_duration = ($cnt_dur_today > 0) ? (int) round($sum_dur_today / max(1, $cnt_dur_today)) : 0;

        // Missed calls in last 7 days (status = 'no-answer' or 'busy' or 'failed')
        $since7 = date('Y-m-d H:i:s', strtotime('-7 days'));
        $missed_calls_7 = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$new_calls_table} WHERE user_id = %d AND status IN ('no-answer','busy','failed') AND created_at >= %s",
            $user_id, $since7
        ));
    }
    // Fallback to legacy calls table if new table has no data
    elseif ($table_exists($legacy_calls_table)) {
        $today = current_time('Y-m-d');
        $calls_today = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$legacy_calls_table} WHERE user_id = %d AND DATE(call_start_time) = %s",
            $user_id,
            $today
        ));
        $avg = $wpdb->get_var($wpdb->prepare(
            "SELECT AVG(call_duration) FROM {$legacy_calls_table} WHERE user_id = %d AND DATE(call_start_time) = %s",
            $user_id,
            $today
        ));
        $avg_duration = $avg ? round((float) $avg) : 0;

        // Missed calls in last 7 days
        $since7 = date('Y-m-d H:i:s', strtotime('-7 days'));
        $missed_calls_7 = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$legacy_calls_table} WHERE user_id = %d AND call_status = 'expired' AND call_start_time >= %s",
            $user_id, $since7
        ));
    }
    
    $counts['calls_today'] = $calls_today;
    $counts['avg_call_duration'] = $avg_duration;
    $counts['growth_alerts'] += $missed_calls_7;

    // Overdue follow-ups contribute to growth alerts
    if ($fu_exists) {
        $now = current_time('mysql');
        $overdue = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$followups_table} WHERE user_id = %d AND status = 'pending' AND scheduled_date < %s",
            $user_id, $now
        ));
        $counts['growth_alerts'] += $overdue;
    }

    // Pending approvals also count as actionable alerts
    $counts['growth_alerts'] += (int)$counts['pending_approvals'];

    // Build Today action items (lightweight, robust to schema)
    $missed_7 = 0;
    if (!empty($counts['growth_alerts'])) {
        // We already computed missed calls last 7 days above, re-use if available
    }
    // Recompute missed_7 specifically (if calls table exists) to include in task text
    $calls_table = $wpdb->prefix . 'vmo_business_calls';
    if ($table_exists($calls_table)) {
        $since7 = date('Y-m-d H:i:s', strtotime('-7 days'));
        $missed_7 = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$calls_table} WHERE user_id = %d AND call_status = 'expired' AND call_start_time >= %s",
            $user_id, $since7
        ));
    }
    $tasks = [];
    // Pull real tasks scheduled for today from follow-ups if available
    if ($fu_exists) {
        $todayStart = date('Y-m-d 00:00:00', current_time('timestamp'));
        $todayEnd   = date('Y-m-d 23:59:59', current_time('timestamp'));
        $rows = $wpdb->get_results($wpdb->prepare(
            "SELECT id, notes, status FROM {$followups_table} WHERE user_id=%d AND scheduled_date BETWEEN %s AND %s ORDER BY scheduled_date ASC LIMIT 25",
            $user_id, $todayStart, $todayEnd
        ), ARRAY_A);
        foreach (($rows ?: []) as $r) {
            $tasks[] = [
                'id' => (int) $r['id'],
                'text' => (string) ($r['notes'] ?: 'Task'),
                'tokens' => 0,
                'checked' => ((string)$r['status'] === 'completed')
            ];
        }
    } else {
        // Fallback tasks based on approvals/missed calls if present
        if (!empty($counts['pending_approvals'])) {
            $tasks[] = [ 'text' => sprintf(__('Review %d pending approvals', 'vmo'), (int)$counts['pending_approvals']), 'tokens' => 300, 'checked' => false ];
        }
        if ($missed_7 > 0) {
            $tasks[] = [ 'text' => sprintf(__('Return %d missed calls (last 7 days)', 'vmo'), (int)$missed_7), 'tokens' => 300, 'checked' => false ];
        }
    }
    $counts['tasks_today'] = array_slice($tasks, 0, 10);

    // Weekly strategy insights (pull from weekly automation option if present)
    $week_key_now = 'vmo_weekly_email_results_' . $user_id . '_' . date('Y_W');
    $week_key_prev = 'vmo_weekly_email_results_' . $user_id . '_' . date('Y_W', strtotime('-7 days'));
    $weekly_data = get_option($week_key_now);
    if (!$weekly_data) { $weekly_data = get_option($week_key_prev); }
    if (is_array($weekly_data)) {
        // Attempt to extract insights/actions
        $insights = [];
        if (isset($weekly_data['research_data']['actionable_insights']) && is_array($weekly_data['research_data']['actionable_insights'])) {
            $insights = $weekly_data['research_data']['actionable_insights'];
        } elseif (isset($weekly_data['actionable_insights']) && is_array($weekly_data['actionable_insights'])) {
            $insights = $weekly_data['actionable_insights'];
        }
        $actions = [];
        if (isset($weekly_data['recommended_actions']) && is_array($weekly_data['recommended_actions'])) {
            foreach ($weekly_data['recommended_actions'] as $a) {
                if (is_string($a)) { $actions[] = ['text' => $a, 'tokens' => 0]; }
                elseif (is_array($a) && isset($a['text'])) { $actions[] = ['text' => $a['text'], 'tokens' => intval($a['tokens'] ?? 0)]; }
            }
        }
    // Do not inject placeholders; leave empty if none yet
    $counts['weekly']['insights'] = array_values(array_slice($insights, 0, 5));
    $counts['weekly']['actions'] = array_values(array_slice($actions, 0, 5));
    } else {
    // Leave weekly empty until research runs
    $counts['weekly']['insights'] = [];
    $counts['weekly']['actions'] = [];
    }

    // Optional: include today's token cost (phone calls, tokens charged today)
    try {
        $calls_tbl_opt = $wpdb->prefix . 'vmo_phone_calls';
        $exists_calls_opt = vmo_table_exists($calls_tbl_opt);
        if ($exists_calls_opt) {
            // Check if tokens_used column exists before querying it
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
            $calls_cols = $wpdb->get_col("SHOW COLUMNS FROM {$calls_tbl_opt}", 0);
            $has_tokens = (is_array($calls_cols) && in_array('tokens_used', $calls_cols, true));
            if ($has_tokens) {
                $today_opt = current_time('Y-m-d');
                $tokens_today_opt = (int) $wpdb->get_var($wpdb->prepare(
                "SELECT COALESCE(SUM(tokens_used),0) FROM {$calls_tbl_opt} WHERE user_id = %d AND DATE(created_at) = %s",
                $user_id,
                $today_opt
            ));
            $counts['tokens_cost_today'] = $tokens_today_opt;
            }
        }
    } catch (Exception $e) {
        // ignore optional metric errors
    }

    $counts['website'] = $setup_snapshot['website'];
    $counts['systems'] = $setup_snapshot['systems'];
    $counts['dashboard_modules'] = $dashboard_context['dashboard_modules'];
    $counts['kpi_model'] = $dashboard_context['kpi_model'];
    $counts['last_update'] = (string) ($dashboard_context['operating_state']['generated_at'] ?? current_time('mysql'));

    if ((int) ($counts['goals_complete_percent'] ?? 0) <= 0 && (int) ($setup_snapshot['setup']['progress'] ?? 0) > 0) {
        $counts['goals_complete_percent'] = (int) $setup_snapshot['setup']['progress'];
    }

    if ((int) ($counts['short_term']['total'] ?? 0) <= 0 && (int) ($setup_snapshot['setup']['total'] ?? 0) > 0) {
        $counts['short_term']['percent'] = (int) $setup_snapshot['setup']['progress'];
        $counts['short_term']['completed'] = (int) $setup_snapshot['setup']['completed'];
        $counts['short_term']['total'] = (int) $setup_snapshot['setup']['total'];
        $counts['short_term']['today_percent'] = (int) $setup_snapshot['setup']['progress'];
    }

    if ((int) ($counts['long_term']['percent'] ?? 0) <= 0 && (int) ($setup_snapshot['setup']['progress'] ?? 0) > 0) {
        $counts['long_term']['percent'] = (int) $setup_snapshot['setup']['progress'];
        $counts['long_term']['note'] = !empty($setup_snapshot['website']['publish_ready'])
            ? 'Website and core systems are ready to operate from the dashboard'
            : 'Finish setup milestones to turn the website into the business control hub';
    }

    $counts['growth_alerts'] = max((int) ($counts['growth_alerts'] ?? 0), (int) ($setup_snapshot['setup']['blockers'] ?? 0));

    if (empty($counts['tasks_today']) && !empty($setup_snapshot['tasks'])) {
        $counts['tasks_today'] = $setup_snapshot['tasks'];
    }

    if (empty($counts['weekly']['insights']) && !empty($setup_snapshot['insights'])) {
        $counts['weekly']['insights'] = $setup_snapshot['insights'];
    }

    $placeholder_rankings = array('—', 'Consultation pending', 'Analysis pending', 'Building baseline...');
    if (in_array((string) ($counts['bi']['ranking'] ?? '—'), $placeholder_rankings, true)) {
        $counts['bi']['ranking'] = !empty($setup_snapshot['website']['publish_ready'])
            ? 'Launch ready'
            : (!empty($setup_snapshot['website']['draft_ready'])
                ? 'Website in refinement'
                : (!empty($setup_snapshot['website']['draft_assembled']) ? 'Media generation in progress' : 'Setup mode'));
    }

    $placeholder_milestones = array('—', 'Complete business consultation', 'Launch website & begin tracking', 'Establish baseline metrics');
    if (in_array((string) ($counts['bi']['next_milestone'] ?? '—'), $placeholder_milestones, true)) {
        $counts['bi']['next_milestone'] = (string) ($setup_snapshot['setup']['next_milestone'] ?? 'Monitor live KPIs and manage operations');
    }

    if ((int) ($counts['bi']['monitored'] ?? 0) <= 0) {
        $counts['bi']['monitored'] = (int) ($setup_snapshot['modules']['active'] ?? 0);
    }
    if ((int) ($counts['bi']['opportunities'] ?? 0) <= 0) {
        $counts['bi']['opportunities'] = (int) ($setup_snapshot['modules']['in_progress'] ?? 0);
    }
    if ((int) ($counts['bi']['new_threats'] ?? 0) <= 0) {
        $counts['bi']['new_threats'] = (int) ($setup_snapshot['website']['qa_pending'] ?? 0);
    }

    $vmo_send_dashboard_success($counts);
}

// Approve all weekly actions -> schedule as today's tasks in follow-ups
add_action('wp_ajax_vmo_approve_weekly_actions', function() {
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if ((isset($_POST['security']) || isset($_POST['nonce'])) && !vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Invalid nonce', 'status' => 'invalid_nonce']);
    }
    global $wpdb;
    $user_id = get_current_user_id();
    $week_key_now = 'vmo_weekly_email_results_' . $user_id . '_' . date('Y_W');
    $week_key_prev = 'vmo_weekly_email_results_' . $user_id . '_' . date('Y_W', strtotime('-7 days'));
    $weekly_data = get_option($week_key_now);
    if (!$weekly_data) { $weekly_data = get_option($week_key_prev); }
    $actions = [];
    if (isset($weekly_data['recommended_actions']) && is_array($weekly_data['recommended_actions'])) {
        $actions = $weekly_data['recommended_actions'];
    }
    if (!$actions) {
        wp_send_json_error(['message' => __('No actions available to approve', 'vmo')]);
    }
    $followups_table = $wpdb->prefix . 'vmo_follow_ups';
    $exists = vmo_table_exists($followups_table);
    if (!$exists) {
        wp_send_json_error(['message' => __('Follow-ups table not available', 'vmo')]);
    }
    $today = current_time('mysql');
    $inserted = 0;
    foreach ($actions as $a) {
        $text = is_string($a) ? $a : (isset($a['text']) ? $a['text'] : '');
        if (!$text) continue;
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $res = $wpdb->insert($followups_table, [
            'user_id' => $user_id,
            'customer_id' => 0,
            'call_id' => null,
            'follow_up_type' => 'ai_task',
            'scheduled_date' => $today,
            'status' => 'pending',
            'notes' => wp_strip_all_tags($text),
            'created_at' => current_time('mysql'),
            'updated_at' => current_time('mysql'),
        ], ['%d','%d','%d','%s','%s','%s','%s','%s']);
        if ($res) $inserted++;
    }
    wp_send_json_success(['inserted' => $inserted]);
});

// Add a single roadmap objective as a scheduled task
add_action('wp_ajax_vmo_add_roadmap_objective', function() {
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if ((isset($_POST['security']) || isset($_POST['nonce'])) && !vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Invalid nonce', 'status' => 'invalid_nonce']);
    }
    global $wpdb;
    $user_id = get_current_user_id();
    $title = sanitize_text_field($_POST['title'] ?? '');
    $due = sanitize_text_field($_POST['due'] ?? '');
    if (!$title || !$due) {
        wp_send_json_error(['message' => __('Title and due date are required', 'vmo')], 400);
    }
    $followups_table = $wpdb->prefix . 'vmo_follow_ups';
    $exists = vmo_table_exists($followups_table);
    if (!$exists) {
        wp_send_json_error(['message' => __('Follow-ups table not available', 'vmo')], 500);
    }
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    $res = $wpdb->insert($followups_table, [
        'user_id' => $user_id,
        'customer_id' => 0,
        'call_id' => null,
        'follow_up_type' => 'roadmap_task',
        'scheduled_date' => gmdate('Y-m-d H:i:s', strtotime($due)),
        'status' => 'pending',
        'notes' => $title,
        'created_at' => current_time('mysql'),
        'updated_at' => current_time('mysql'),
    ], ['%d','%d','%d','%s','%s','%s','%s','%s']);
    if (!$res) {
        wp_send_json_error(['message' => __('Failed to add objective', 'vmo')], 500);
    }
    wp_send_json_success(['message' => __('Objective added', 'vmo')]);
});

// Weekly Briefing (narrative) AJAX
add_action('wp_ajax_vmo_get_weekly_briefing', 'vmo_get_weekly_briefing');
function vmo_get_weekly_briefing() {
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if ((isset($_POST['security']) || isset($_POST['nonce'])) && !vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Invalid nonce', 'status' => 'invalid_nonce']);
    }
    $user_id = get_current_user_id();
    $requested_week = isset($_POST['week']) ? preg_replace('/[^0-9_]/','', $_POST['week']) : '';
    if ($requested_week && !preg_match('/^\d{4}_\d{2}$/', $requested_week)) { $requested_week = ''; }
    $week_key_now = 'vmo_weekly_email_results_' . $user_id . '_' . date('Y_W');
    $week_key_prev = 'vmo_weekly_email_results_' . $user_id . '_' . date('Y_W', strtotime('-7 days'));
    $weekly_data = null;
    if ($requested_week) {
        $weekly_data = get_option('vmo_weekly_email_results_' . $user_id . '_' . $requested_week);
    }
    if (!$weekly_data) { $weekly_data = get_option($week_key_now); }
    if (!$weekly_data) { $weekly_data = get_option($week_key_prev); }

    $title = __('Weekly Strategic Briefing', 'vmo');
    $summary = __('No current research available. A new scan will run overnight.', 'vmo');
    $analysis = '';
    $sections = [];

    if (is_array($weekly_data)) {
        // Try to extract comprehensive analysis from research_data
        $rd = isset($weekly_data['research_data']) ? $weekly_data['research_data'] : $weekly_data;
        $comp = '';
        if (is_array($rd)) {
            if (isset($rd['comprehensive_analysis'])) {
                $comp = $rd['comprehensive_analysis'];
            } elseif (isset($rd['final_analysis'])) { // alt naming
                $comp = $rd['final_analysis'];
            }
        }
        // The value might be a nested OpenAI response; normalize to string
        if (is_array($comp)) {
            if (isset($comp['choices'][0]['message']['content'])) {
                $comp = (string) $comp['choices'][0]['message']['content'];
            } elseif (isset($comp['content'])) {
                $comp = (string) $comp['content'];
            } else {
                $tmp = wp_json_encode($comp);
                $comp = is_string($tmp) ? $tmp : '';
            }
        }
        if (is_string($comp) && $comp !== '') {
            $analysis = $comp;
            $summary = __('Comprehensive analysis generated from this week\'s deep research.', 'vmo');
        } else {
            // Fallback: stitch insights and actions into a narrative
            $insights = [];
            if (isset($rd['actionable_insights']) && is_array($rd['actionable_insights'])) {
                $insights = $rd['actionable_insights'];
            } elseif (isset($weekly_data['actionable_insights']) && is_array($weekly_data['actionable_insights'])) {
                $insights = $weekly_data['actionable_insights'];
            }
            $actions = [];
            if (isset($weekly_data['recommended_actions']) && is_array($weekly_data['recommended_actions'])) {
                $actions = $weekly_data['recommended_actions'];
            }
            $parts = [];
            if ($insights) {
                $parts[] = __('Key Insights:', 'vmo') . "\n- " . implode("\n- ", array_map('wp_strip_all_tags', $insights));
            }
            if ($actions) {
                $flat = array_map(function($a){ return is_string($a) ? $a : (isset($a['text']) ? $a['text'] : ''); }, $actions);
                $flat = array_filter($flat);
                if ($flat) $parts[] = __('Recommended Actions:', 'vmo') . "\n- " . implode("\n- ", array_map('wp_strip_all_tags', $flat));
            }
            $analysis = $parts ? implode("\n\n", $parts) : '';
        }

        // Try to build structured sections if present in research_data
        if (is_array($rd)) {
            $maybe_sections = [];
            // Known keys (best-effort)
            $map = [
                'competitive_landscape' => __('Competitive Landscape', 'vmo'),
                'seo_standing' => __('SEO Standing', 'vmo'),
                'value_prop_adjustment' => __('Value Proposition Adjustment', 'vmo'),
                'market_intel' => __('Market Intelligence', 'vmo'),
                'product_updates' => __('Product/Service Updates', 'vmo'),
                'risks' => __('Risks & Alerts', 'vmo'),
                'opportunities' => __('Opportunities', 'vmo'),
                // extra likely themes
                'emerging_trends' => __('Emerging Trends', 'vmo'),
                'ai_disruption' => __('AI Disruption Watch', 'vmo'),
                'regulatory_changes' => __('Regulatory Changes', 'vmo')
            ];
            foreach ($map as $key => $label) {
                if (!empty($rd[$key])) {
                    $content = $rd[$key];
                    if (is_array($content)) {
                        $items = array_filter(array_map('wp_strip_all_tags', $content));
                        if ($items) $maybe_sections[] = ['title' => $label, 'items' => array_values($items)];
                    } elseif (is_string($content)) {
                        $maybe_sections[] = ['title' => $label, 'content' => wp_strip_all_tags($content)];
                    }
                }
            }
            // Catch-all: if there is a 'sections' array provided already
            if (isset($rd['sections']) && is_array($rd['sections'])) {
                foreach ($rd['sections'] as $s) {
                    if (isset($s['title']) && (isset($s['content']) || isset($s['items']))) {
                        $sec = [ 'title' => (string) $s['title'] ];
                        if (isset($s['content'])) $sec['content'] = wp_strip_all_tags(is_array($s['content']) ? implode("\n", $s['content']) : (string) $s['content']);
                        if (isset($s['items']) && is_array($s['items'])) $sec['items'] = array_values(array_filter(array_map('wp_strip_all_tags', $s['items'])));
                        $maybe_sections[] = $sec;
                    }
                }
            }
            // Catch-all for any other keys not covered above to avoid missing novel categories
            $skip_keys = array_merge(array_keys($map), ['comprehensive_analysis','final_analysis','actionable_insights','recommended_actions','sections']);
            foreach ($rd as $k => $v) {
                if (in_array($k, $skip_keys, true)) continue;
                if (is_string($v) && trim($v) !== '') {
                    $label = ucwords(str_replace(['_','-'],' ', (string)$k));
                    $maybe_sections[] = ['title' => $label, 'content' => wp_strip_all_tags($v)];
                } elseif (is_array($v)) {
                    // If array of scalars/strings, include as items
                    $flat = array_filter(array_map(function($x){ return is_scalar($x) ? (string)$x : (is_array($x) && isset($x['text']) ? (string)$x['text'] : ''); }, $v));
                    if ($flat) {
                        $label = ucwords(str_replace(['_','-'],' ', (string)$k));
                        $maybe_sections[] = ['title' => $label, 'items' => array_values(array_map('wp_strip_all_tags', $flat))];
                    }
                }
            }
            // De-duplicate by title
            if ($maybe_sections) {
                $seen = [];
                foreach ($maybe_sections as $s) {
                    $t = $s['title'] ?? '';
                    if (!$t || isset($seen[$t])) continue;
                    $seen[$t] = true; $sections[] = $s;
                }
            }
        }
    }

    wp_send_json_success([
        'title' => $title,
        'summary' => $summary,
        'analysis' => $analysis,
        'sections' => $sections,
        'generated_at' => current_time('mysql')
    ]);
}

// List available weekly scan results (last ~16 weeks)
add_action('wp_ajax_vmo_list_weekly_scans', function(){
    if (!current_user_can('read')) { wp_send_json_error(['message'=>'Unauthorized'], 403); }
    if ((isset($_POST['security']) || isset($_POST['nonce'])) && !vmo_verify_admin_security()) {
        wp_send_json_error(['message'=>'Invalid nonce', 'status' => 'invalid_nonce']);
    }
    $user_id = get_current_user_id();
    $out = [];
    for ($i=0; $i<16; $i++) {
        $ts = strtotime("-".($i*7)." days");
        $wk = date('Y_W', $ts);
        $key = 'vmo_weekly_email_results_' . $user_id . '_' . $wk;
        $val = get_option($key);
        if ($val) {
            $label = date('M j, Y', strtotime('monday this week', $ts)) . ' • Week ' . date('W', $ts);
            $out[] = [ 'week' => $wk, 'label' => $label ];
        }
    }
    wp_send_json_success($out);
});

// Full weekly analysis (narrative-only) for the "View Full Analysis" use-case
add_action('wp_ajax_vmo_get_weekly_full_analysis', function(){
    if (!current_user_can('read')) { wp_send_json_error(['message'=>'Unauthorized'], 403); }
    if ((isset($_POST['security']) || isset($_POST['nonce'])) && !vmo_verify_admin_security()) {
        wp_send_json_error(['message'=>'Invalid nonce', 'status' => 'invalid_nonce']);
    }
    $user_id = get_current_user_id();
    $week_key_now = 'vmo_weekly_email_results_' . $user_id . '_' . date('Y_W');
    $week_key_prev = 'vmo_weekly_email_results_' . $user_id . '_' . date('Y_W', strtotime('-7 days'));
    $weekly_data = get_option($week_key_now);
    if (!$weekly_data) { $weekly_data = get_option($week_key_prev); }
    $title = __('Weekly Full Analysis', 'vmo');
    $analysis = '';
    if (is_array($weekly_data)) {
        $rd = isset($weekly_data['research_data']) ? $weekly_data['research_data'] : $weekly_data;
        $comp = '';
        if (is_array($rd)) {
            if (isset($rd['comprehensive_analysis'])) { $comp = $rd['comprehensive_analysis']; }
            elseif (isset($rd['final_analysis'])) { $comp = $rd['final_analysis']; }
        }
        if (is_array($comp)) {
            if (isset($comp['choices'][0]['message']['content'])) { $comp = (string)$comp['choices'][0]['message']['content']; }
            elseif (isset($comp['content'])) { $comp = (string)$comp['content']; }
            else { $tmp = wp_json_encode($comp); $comp = is_string($tmp)?$tmp:''; }
        }
        if (!$comp) {
            // fallback: join sections and insights into a single narrative
            $parts = [];
            if (isset($rd['actionable_insights']) && is_array($rd['actionable_insights']) && $rd['actionable_insights']) {
                $parts[] = __('Key Insights:', 'vmo') . "\n- " . implode("\n- ", array_map('wp_strip_all_tags', $rd['actionable_insights']));
            }
            if (isset($weekly_data['recommended_actions']) && is_array($weekly_data['recommended_actions']) && $weekly_data['recommended_actions']) {
                $flat = array_map(function($a){ return is_string($a)?$a:(isset($a['text'])?$a['text']:''); }, $weekly_data['recommended_actions']);
                $flat = array_filter($flat);
                if ($flat) $parts[] = __('Recommended Actions:', 'vmo') . "\n- " . implode("\n- ", array_map('wp_strip_all_tags', $flat));
            }
            $comp = $parts ? implode("\n\n", $parts) : '';
        }
        $analysis = (string)$comp;
    }
    wp_send_json_success(['title'=>$title, 'analysis'=>$analysis]);
});

// === Recommendation Approval/Rejection AJAX Handlers ===
// These handlers record user decisions on AI-generated recommendations for learning purposes

add_action('wp_ajax_vmo_approve_recommendation', function() {
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if ((isset($_POST['security']) || isset($_POST['nonce'])) && !vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Invalid nonce', 'status' => 'invalid_nonce']);
    }
    
    $user_id = get_current_user_id();
    $recommendation_id = sanitize_text_field($_POST['recommendation_id'] ?? '');
    $recommendation_type = sanitize_text_field($_POST['recommendation_type'] ?? 'general');
    
    // Store approval in user meta for learning/analytics
    $approvals = get_user_meta($user_id, 'vmo_recommendation_approvals', true);
    if (!is_array($approvals)) {
        $approvals = [];
    }
    
    $approvals[] = [
        'id' => $recommendation_id,
        'type' => $recommendation_type,
        'action' => 'approved',
        'timestamp' => current_time('mysql'),
    ];
    
    // Keep only last 100 records
    if (count($approvals) > 100) {
        $approvals = array_slice($approvals, -100);
    }
    
    update_user_meta($user_id, 'vmo_recommendation_approvals', $approvals);
    
    wp_send_json_success([
        'message' => __('Recommendation approved', 'vmo'),
        'recommendation_id' => $recommendation_id
    ]);
});

add_action('wp_ajax_vmo_reject_recommendation', function() {
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if ((isset($_POST['security']) || isset($_POST['nonce'])) && !vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Invalid nonce', 'status' => 'invalid_nonce']);
    }
    
    $user_id = get_current_user_id();
    $recommendation_id = sanitize_text_field($_POST['recommendation_id'] ?? '');
    $recommendation_type = sanitize_text_field($_POST['recommendation_type'] ?? 'general');
    $reason = sanitize_text_field($_POST['reason'] ?? 'user_rejected');
    
    // Store rejection in user meta for learning/analytics
    $rejections = get_user_meta($user_id, 'vmo_recommendation_rejections', true);
    if (!is_array($rejections)) {
        $rejections = [];
    }
    
    $rejections[] = [
        'id' => $recommendation_id,
        'type' => $recommendation_type,
        'action' => 'rejected',
        'reason' => $reason,
        'timestamp' => current_time('mysql'),
    ];
    
    // Keep only last 100 records
    if (count($rejections) > 100) {
        $rejections = array_slice($rejections, -100);
    }
    
    update_user_meta($user_id, 'vmo_recommendation_rejections', $rejections);
    
    wp_send_json_success([
        'message' => __('Recommendation rejected', 'vmo'),
        'recommendation_id' => $recommendation_id
    ]);
});

// === Phone Call Summaries AJAX Handler (for CRM/Phone dashboard) ===
add_action('wp_ajax_vmo_ajax_get_call_summaries', function() {
    // Clean any stray output from earlier hooks (DB error notices, dbDelta warnings, etc.)
    while (ob_get_level() > 0) { @ob_end_clean(); }
    global $wpdb;
    $wpdb->suppress_errors(true);

    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    // This endpoint is called by the Strategic Dashboard using wp_create_nonce('vmo_ajax_nonce') in the `nonce` field.
    // Accept either the unified dashboard security verifier OR the dedicated vmo_ajax_nonce.
    $nonce_ok = false;
    if (function_exists('vmo_verify_admin_security') && vmo_verify_admin_security()) {
        $nonce_ok = true;
    } elseif (isset($_POST['nonce']) && wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'vmo_ajax_nonce')) {
        $nonce_ok = true;
    }
    if (!$nonce_ok) {
        wp_send_json_error(['message' => 'Invalid nonce', 'status' => 'invalid_nonce'], 403);
    }
    
    $user_id = get_current_user_id();
    $phone_calls_table = $wpdb->prefix . 'vmo_phone_calls';
    
    // Check if table exists
    $table_exists = vmo_table_exists($phone_calls_table);
    
    if (!$table_exists) {
        // Return empty array if phone system not set up
        wp_send_json_success([
            'summaries' => [],
            'total' => 0,
            'message' => __('Phone system not configured', 'vmo')
        ]);
        return;
    }
    
    $limit = isset($_POST['limit']) ? absint($_POST['limit']) : 10;
    $offset = isset($_POST['offset']) ? absint($_POST['offset']) : 0;
    
    // Get call summaries with relevant details
    $calls = $wpdb->get_results($wpdb->prepare(
        "SELECT id, call_sid, caller_number, direction, status, 
                duration, transcript, summary, created_at
         FROM {$phone_calls_table} 
         WHERE user_id = %d 
         ORDER BY created_at DESC 
         LIMIT %d OFFSET %d",
        $user_id, $limit, $offset
    ), ARRAY_A);
    
    $total = $wpdb->get_var($wpdb->prepare(
        "SELECT COUNT(*) FROM {$phone_calls_table} WHERE user_id = %d",
        $user_id
    ));

    // Admin fallback: if zero summaries for this user but calls exist under other user IDs
    if (empty($calls) && current_user_can('manage_options')) {
        $calls = $wpdb->get_results($wpdb->prepare(
            "SELECT id, call_sid, caller_number, direction, status,
                    duration, transcript, summary, created_at
             FROM {$phone_calls_table}
             ORDER BY created_at DESC
             LIMIT %d OFFSET %d",
            $limit, $offset
        ), ARRAY_A);
        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
        $total = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$phone_calls_table}");
    }

    // Convert timestamps to ISO 8601 so browser can show user's local time
    if (!empty($calls) && is_array($calls)) {
        $twilio_class = 'VMO_Twilio_Manager';
        foreach ($calls as &$call_row) {
            if (!empty($call_row['created_at']) && method_exists($twilio_class, 'to_iso8601')) {
                $call_row['created_at'] = $twilio_class::to_iso8601($call_row['created_at']);
            }
        }
        unset($call_row);
    }

    wp_send_json_success([
        'summaries' => $calls ?: [],
        'total' => (int) $total,
        'limit' => $limit,
        'offset' => $offset
    ]);
});

// === SMS Activation AJAX Handler ===
add_action('wp_ajax_vmo_activate_sms', function() {
    if (!current_user_can('read')) {
        wp_send_json_error('Unauthorized', 403);
    }
    if (!isset($_POST['_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_nonce'])), 'vmo_sms_activate')) {
        wp_send_json_error('Invalid security token. Please refresh the page and try again.');
    }

    $user_id = get_current_user_id();

    // Verify user has a provisioned phone number
    $phone_number = function_exists('vmo_sms_get_business_number')
        ? vmo_sms_get_business_number($user_id)
        : (string) get_user_meta($user_id, 'vmo_phone_number', true);
    if ($phone_number === '') {
        wp_send_json_error('You must have a provisioned phone number before activating SMS.');
    }

    $activate = isset($_POST['activate']) ? absint($_POST['activate']) : 0;

    if ($activate) {
        $snapshot = function_exists('vmo_get_sms_activation_snapshot')
            ? vmo_get_sms_activation_snapshot($user_id, true)
            : array('sms_active' => false, 'brand_status' => 'not_started', 'campaign_status' => 'not_started', 'tracked_in_plugin' => false);

        if (!empty($snapshot['sms_active'])) {
            wp_send_json_success([
                'message' => 'SMS is active on your business line.',
                'status'  => 'active',
                'activation' => $snapshot,
            ]);
        }

        $brand_status = strtoupper((string) ($snapshot['brand_status'] ?? 'not_started'));
        $campaign_status = strtoupper((string) ($snapshot['campaign_status'] ?? 'not_started'));
        $tracked_in_plugin = !empty($snapshot['tracked_in_plugin']);

        $message = $tracked_in_plugin
            ? 'SMS is still waiting on carrier approval. Current status — Brand: ' . str_replace('_', ' ', $brand_status) . ', Campaign: ' . str_replace('_', ' ', $campaign_status) . '. VMO will switch SMS to active automatically once the carrier reports VERIFIED.'
            : 'SMS is not active yet. VMO could not confirm a completed carrier registration on this number during the latest live check. Open SMS Activation and use Check Registration Status if this is an existing approved test number.';

        wp_send_json_error([
            'message' => $message,
            'status'  => 'pending',
            'activation' => $snapshot,
        ], 409);
    } else {
        // Deactivate
        update_user_meta($user_id, 'vmo_sms_activated', '0');
        wp_send_json_success([
            'message' => 'SMS has been deactivated.',
            'status'  => 'inactive'
        ]);
    }
});

add_action('wp_ajax_vmo_validate_current_sms_number', function() {
    if (!current_user_can('read')) {
        wp_send_json_error(array('message' => 'Unauthorized'), 403);
    }
    if (!function_exists('vmo_verify_admin_security') || !vmo_verify_admin_security()) {
        wp_send_json_error(array('message' => 'Security check failed'), 403);
    }

    if (!class_exists('VMO_Twilio_A2P_Manager')) {
        $path = plugin_dir_path(__FILE__) . 'includes/class-vmo-twilio-a2p-manager.php';
        if (file_exists($path)) {
            require_once $path;
        }
    }

    if (!class_exists('VMO_Twilio_A2P_Manager')) {
        wp_send_json_error(array('message' => 'SMS validation service unavailable.'), 500);
    }

    $user_id = get_current_user_id();
    $manager = VMO_Twilio_A2P_Manager::get_instance();
    $result = $manager->validate_existing_number($user_id);

    if (is_wp_error($result)) {
        wp_send_json_error(array('message' => $result->get_error_message()), 409);
    }

    wp_send_json_success(array_merge(array(
        'message' => 'Your current business number has been validated and is now active inside VMO.',
    ), is_array($result) ? $result : array()));
});

// Regenerate and persist roadmap using latest weekly research
add_action('wp_ajax_vmo_regenerate_roadmap', 'vmo_regenerate_roadmap');
function vmo_regenerate_roadmap() {
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if ((isset($_POST['security']) || isset($_POST['nonce'])) && !vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Invalid nonce', 'status' => 'invalid_nonce']);
    }
    $user_id = get_current_user_id();
    $week_key_now = 'vmo_weekly_email_results_' . $user_id . '_' . date('Y_W');
    $week_key_prev = 'vmo_weekly_email_results_' . $user_id . '_' . date('Y_W', strtotime('-7 days'));
    $weekly = get_option($week_key_now);
    if (!$weekly) { $weekly = get_option($week_key_prev); }

    // Compose a simple structured roadmap from insights/actions
    $insights = [];
    if (isset($weekly['research_data']['actionable_insights']) && is_array($weekly['research_data']['actionable_insights'])) {
        $insights = $weekly['research_data']['actionable_insights'];
    } elseif (isset($weekly['actionable_insights']) && is_array($weekly['actionable_insights'])) {
        $insights = $weekly['actionable_insights'];
    }
    $actions = [];
    if (isset($weekly['recommended_actions']) && is_array($weekly['recommended_actions'])) {
        $actions = $weekly['recommended_actions'];
    }
    $phases = [
        'Phase 1 (Weeks 1-2)' => [],
        'Phase 2 (Weeks 3-4)' => [],
        'Phase 3 (Weeks 5-8)' => [],
    ];
    // Distribute actions into phases
    $flat_actions = array_map(function($a){ return is_string($a) ? $a : (isset($a['text']) ? $a['text'] : ''); }, $actions);
    $flat_actions = array_values(array_filter($flat_actions));
    foreach ($flat_actions as $i => $txt) {
        if ($i % 3 === 0) $phases['Phase 1 (Weeks 1-2)'][] = $txt;
        elseif ($i % 3 === 1) $phases['Phase 2 (Weeks 3-4)'][] = $txt;
        else $phases['Phase 3 (Weeks 5-8)'][] = $txt;
    }
    $roadmap_text = "Implementation Roadmap\n\n";
    if ($insights) {
        $roadmap_text .= "Key Insights:\n- " . implode("\n- ", array_map('wp_strip_all_tags', $insights)) . "\n\n";
    }
    foreach ($phases as $label => $items) {
        $roadmap_text .= $label . ":\n";
        if ($items) {
            foreach ($items as $it) { $roadmap_text .= "- " . wp_strip_all_tags($it) . "\n"; }
        } else {
            $roadmap_text .= "- (add tasks)\n";
        }
        $roadmap_text .= "\n";
    }

    // Persist to user option
    $payload = [
        'generated_at' => current_time('mysql'),
        'roadmap' => $roadmap_text,
        'source' => 'weekly_research',
    ];
    update_option('vmo_implementation_roadmap_' . $user_id, $payload, false);
    wp_send_json_success(['message' => __('Roadmap updated', 'vmo'), 'roadmap' => $payload]);
}

// Export progress report (HTML file) and return URL
add_action('wp_ajax_vmo_export_progress', 'vmo_export_progress');
function vmo_export_progress() {
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if ((isset($_POST['security']) || isset($_POST['nonce'])) && !vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Invalid nonce', 'status' => 'invalid_nonce']);
    }
    global $wpdb;
    $user_id = get_current_user_id();
    $contacts_table = $wpdb->prefix . 'vmo_contacts';
    $approval_table = $wpdb->prefix . 'vmo_approval_queue';
    $calls_table = $wpdb->prefix . 'vmo_business_calls';
    $orders_table = $wpdb->prefix . 'vmo_store_orders';

    $table_exists = function($table) {
        global $wpdb;
        $like = $wpdb->esc_like($table);
        return (bool) $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $like));
    };

    $contacts_count = 0;
    if ($table_exists($contacts_table)) {
        $contacts_count = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$contacts_table} WHERE user_id = %d", $user_id));
    }
    $approvals_pending = 0;
    if ($table_exists($approval_table)) {
        $approvals_pending = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$approval_table} WHERE user_id = %d AND status = 'pending'", $user_id));
    }
    $today = current_time('Y-m-d');
    $calls_today = 0; $avg_call_today = 0;
    if ($table_exists($calls_table)) {
        $calls_today = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$calls_table} WHERE user_id = %d AND DATE(call_start_time) = %s", $user_id, $today));
        $avg_call_today = (float) $wpdb->get_var($wpdb->prepare("SELECT AVG(call_duration) FROM {$calls_table} WHERE user_id = %d AND DATE(call_start_time) = %s", $user_id, $today));
    }
    $sum_period = function($days) use ($wpdb, $orders_table, $user_id, $table_exists){
        if (!$table_exists($orders_table)) return 0;
        $since = date('Y-m-d H:i:s', strtotime('-' . (int)$days . ' days'));
        $q = $wpdb->get_var($wpdb->prepare("SELECT COALESCE(SUM(amount_total),0) FROM {$orders_table} WHERE user_id = %d AND status='paid' AND created_at >= %s", $user_id, $since));
        return (int)$q;
    };
    $sales_day = $sum_period(1);
    $sales_week = $sum_period(7);
    $sales_month = $sum_period(30);
    $sales_year = $sum_period(365);

    // Trend (last 12 weeks)
    $trend_labels = [];$trend_values = [];
    if ($table_exists($orders_table)) {
        $rows = $wpdb->get_results($wpdb->prepare(
            "SELECT DATE_FORMAT(created_at,'%%x-%%v') AS yw, SUM(amount_total) AS amt
             FROM {$orders_table}
             WHERE user_id = %d AND status='paid' AND created_at >= %s
             GROUP BY yw ORDER BY yw ASC",
            $user_id, date('Y-m-d H:i:s', strtotime('-84 days'))
        ), ARRAY_A);
        foreach ($rows as $r) { $trend_labels[] = $r['yw']; $trend_values[] = (int)$r['amt']; }
    }

    // Generate simple HTML report with an inline SVG chart
    $fmt = function($cents){ return '$' . number_format(($cents/100), 2); };
    $max = max(1, ...$trend_values ?: [1]);
    $points = [];
    foreach ($trend_values as $i => $v) {
        $x = count($trend_values) > 1 ? ($i/(count($trend_values)-1))*100 : 0;
        $y = 100 - (($v/$max)*100);
        $points[] = sprintf('%0.2f,%0.2f', $x, $y);
    }
    $svg = '<svg viewBox="0 0 100 100" preserveAspectRatio="none" style="width:100%;height:160px;background:#0b1117;border:1px solid rgba(255,255,255,.1);border-radius:8px">'
         . (count($points) ? '<polyline fill="none" stroke="#0fe1ff" stroke-width="2" points="' . implode(' ', $points) . '" />' : '')
         . '</svg>';

    $html  = '<!doctype html><meta charset="utf-8"><title>Progress Report</title>';
    $html .= '<div style="font-family:Inter,system-ui;background:#0a0f16;color:#e6f3ff;padding:20px">';
    $html .= '<h1 style="color:#65f1ff">VMO Progress Report</h1>';
    $html .= '<p>Generated: ' . esc_html(current_time('mysql')) . '</p>';
    $html .= '<h2>Key Metrics</h2>';
    $html .= '<ul>';
    $html .= '<li>Contacts: ' . number_format_i18n($contacts_count) . '</li>';
    $html .= '<li>Pending approvals: ' . number_format_i18n($approvals_pending) . '</li>';
    $html .= '<li>Calls today: ' . number_format_i18n($calls_today) . ' (avg ' . number_format_i18n(round($avg_call_today)) . 's)</li>';
    $html .= '<li>Sales today: ' . esc_html($fmt($sales_day)) . '</li>';
    $html .= '<li>Sales 7 days: ' . esc_html($fmt($sales_week)) . '</li>';
    $html .= '<li>Sales 30 days: ' . esc_html($fmt($sales_month)) . '</li>';
    $html .= '<li>Sales 365 days: ' . esc_html($fmt($sales_year)) . '</li>';
    $html .= '</ul>';
    $html .= '<h2>Sales Trend (last 12 weeks)</h2>' . $svg;
    $html .= '</div>';

    $upload = wp_upload_dir();
    $dir = trailingslashit($upload['basedir']) . 'vmo-reports/';
    if (!file_exists($dir)) wp_mkdir_p($dir);
    $file = $dir . 'progress-' . $user_id . '-' . date('Ymd-His') . '.html';
    vmo_get_filesystem()->put_contents($file, $html, FS_CHMOD_FILE);
    $url = trailingslashit($upload['baseurl']) . 'vmo-reports/' . basename($file);
    wp_send_json_success(['url' => esc_url_raw($url)]);
}

// Export Progress as PDF (uses Dompdf if available; falls back to HTML message)
add_action('wp_ajax_vmo_export_progress_pdf', function() {
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if ((isset($_POST['security']) || isset($_POST['nonce'])) && !vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Invalid nonce', 'status' => 'invalid_nonce']);
    }
    // Reuse HTML export to get HTML body
    $html_resp = vmo_export_progress_html_string();
    if (is_wp_error($html_resp)) {
        wp_send_json_error(['message' => $html_resp->get_error_message()]);
    }
    $html = $html_resp;
    if (class_exists('Dompdf\\Dompdf')) {
        try {
            $dompdf = new Dompdf\Dompdf();
            $dompdf->loadHtml($html);
            $dompdf->setPaper('A4', 'portrait');
            $dompdf->render();
            $output = $dompdf->output();
            $upload = wp_upload_dir();
            $dir = trailingslashit($upload['basedir']) . 'vmo-reports/';
            if (!file_exists($dir)) wp_mkdir_p($dir);
            $file = $dir . 'progress-' . get_current_user_id() . '-' . date('Ymd-His') . '.pdf';
            vmo_get_filesystem()->put_contents($file, $output, FS_CHMOD_FILE);
            $url = trailingslashit($upload['baseurl']) . 'vmo-reports/' . basename($file);
            wp_send_json_success(['url' => esc_url_raw($url), 'format' => 'pdf']);
        } catch (\Throwable $e) {
            wp_send_json_error(['message' => 'PDF generation failed: ' . $e->getMessage()]);
        }
    } else {
        // Fallback: generate a styled vector PDF without external libraries
        if (!function_exists('vmo_generate_simple_pdf_from_text')) {
            function vmo_generate_simple_pdf_from_text($text, $title = 'VMO Report', $charts = []) {
                $pageWidth = 612;  // 8.5in * 72
                $pageHeight = 792; // 11in * 72
                $marginLeft = 48;  // 0.66in
                $marginTop = 740;  // start near top
                $lineHeight = 14;  // ~11pt font + 3pt leading
                // Sanitize and wrap text
                $text = is_string($text) ? $text : wp_json_encode($text);
                $text = html_entity_decode($text, ENT_QUOTES, 'UTF-8');
                $text = preg_replace("/[\r\t]+/", ' ', $text);
                $text = preg_replace("/ +/", ' ', $text);
                $text = trim($text);
                // Split into lines at newlines and wrap long lines
                $lines = [];
                foreach (preg_split("/\n+/", $text) as $ln) {
                    $ln = trim($ln);
                    if ($ln === '') { $lines[] = ''; continue; }
                    $wrapped = wordwrap($ln, 95, "\n", true);
                    foreach (explode("\n", $wrapped) as $w) { $lines[] = $w; }
                }
                $maxLinesPerPage = 48; // safe lines per page
                $pages = array_chunk($lines, $maxLinesPerPage);
                // Helpers
                $escape = function($s){
                    $s = str_replace(['\\','(',')'], ['\\\\','\\(','\\)'], $s);
                    // strip non-printable
                    return preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/', '', $s);
                };
                $objects = [];
                $addObj = function($content) use (&$objects){ $objects[] = $content; return count($objects); };
                // Fonts
                $fontBodyObj = $addObj("<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\n");
                $fontBoldObj = $addObj("<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold >>\n");
                $pageObjNums = [];
                $contentObjNums = [];
                foreach ($pages as $pi => $plines) {
                    $y = $marginTop;
                    // Build content stream with header and optional charts on first page
                    $stream = "q\n0.043 0.196 0.247 rg 0.043 0.196 0.247 RG 0 0 {$pageWidth} {$pageHeight} re f\nQ\n"; // background fill
                    // Header bar
                    $stream .= "q\n0 0.88 1 rg 48 760 516 24 re f\nQ\n";
                    // Title
                    $stream .= "BT\n/F2 14 Tf\n1 0 0 1 {$marginLeft} 766 Tm\n("
                             . $escape($title)
                             . ") Tj\nET\n";
                    $y = 740;
                    // Charts (first page only)
                    if ($pi === 0 && !empty($charts)) {
                        $cx = $marginLeft; $cy = 680; $cw = 230; $ch = 90; $gap = 20;
                        foreach ($charts as $chart) {
                            $vals = isset($chart['values']) && is_array($chart['values']) ? $chart['values'] : [];
                            $label = isset($chart['label']) ? (string)$chart['label'] : '';
                            if ($vals) {
                                $max = max(1, ...$vals);
                                $pts = [];
                                $count = count($vals);
                                for ($i=0; $i<$count; $i++) {
                                    $x = $cw * ($count>1 ? ($i/($count-1)) : 0);
                                    $yv = $ch * ($vals[$i]/$max);
                                    $pts[] = sprintf('%0.2f %0.2f', $cx+$x, $cy+$yv);
                                }
                                // frame
                                $stream .= "q\n0.2 0.8 0.95 RG 0.2 0.8 0.95 rg {$cx} {$cy} {$cw} {$ch} re S\nQ\n";
                                // polyline
                                if (count($pts) > 1) {
                                    $stream .= "q\n0 0.94 1 RG 1 w "."{$pts[0]} m ";
                                    for ($i=1; $i<count($pts); $i++) { $stream .= $pts[$i] . " l "; }
                                    $stream .= "S\nQ\n";
                                }
                                // label
                                $stream .= "BT\n/F1 10 Tf\n1 0 0 1 ".($cx)." ".($cy+$ch+12)." Tm\n(".$escape($label).") Tj\nET\n";
                                // next chart position
                                $cx += $cw + $gap; if ($cx + $cw > $pageWidth - $marginLeft) { $cx = $marginLeft; $cy -= ($ch + 32); }
                            }
                        }
                        $y = $cy - 20; if ($y < 120) $y = 120; // keep enough room for body
                    }
                    // Body text
                    $stream .= "BT\n/F1 11 Tf\n1 0 0 1 {$marginLeft} {$y} Tm\n";
                    $first = true;
                    foreach ($plines as $t) {
                        $t = $escape($t);
                        if ($first) { $first = false; }
                        else { $stream .= "0 -{$lineHeight} Td\n"; }
                        if ($t === '') { $stream .= "() Tj\n"; continue; }
                        $stream .= "(".$t.") Tj\n";
                    }
                    $stream .= "ET\n";
                    $len = strlen($stream);
                    $content = "<< /Length {$len} >>\nstream\n{$stream}endstream\n";
                    $contentObjNums[] = $addObj($content);
                    // Page object referencing content and fonts
                    $pageObjNums[] = $addObj("<< /Type /Page /Parent 0 0 R /MediaBox [0 0 {$pageWidth} {$pageHeight}] /Resources << /Font << /F1 {$fontBodyObj} 0 R /F2 {$fontBoldObj} 0 R >> >> /Contents {$contentObjNums[count($contentObjNums)-1]} 0 R >>\n");
                }
                // Pages root
                // We'll set /Parent references after knowing pages object number
                $kidsRefs = [];
                foreach ($pageObjNums as $n) { $kidsRefs[] = "{$n} 0 R"; }
                $pagesDict = "<< /Type /Pages /Kids [".implode(' ', $kidsRefs)."] /Count ".count($pageObjNums)." >>\n";
                $pagesObjNum = $addObj($pagesDict);
                // Fix each page's Parent reference now that we know pagesObjNum
                foreach ($pageObjNums as $idx => $pnum) {
                    // Rebuild page object with correct parent
                    $contentNum = $contentObjNums[$idx];
                    $objects[$pnum-1] = "<< /Type /Page /Parent {$pagesObjNum} 0 R /MediaBox [0 0 {$pageWidth} {$pageHeight}] /Resources << /Font << /F1 {$fontObjNum} 0 R >> >> /Contents {$contentNum} 0 R >>\n";
                }
                // Catalog
                $catalogObjNum = $addObj("<< /Type /Catalog /Pages {$pagesObjNum} 0 R >>\n");
                // Assemble PDF with xref
                $pdf = "%PDF-1.4\n";
                $offsets = [0]; // object 0 is free
                // Object numbering will be: 1..N in the order we add them below
                $order = [];
                // Ensure order: catalog last, but reference to pages ok; standard is fine with any order as xref provides positions
                // We'll output: catalog, pages, font, then for each page: content, page
                // However we already built objects as [font, content1, page1, content2, page2, ..., pages, catalog]
                // Let's rearrange to conventional order: 1 Catalog, 2 Pages, 3 Font, then repeating pairs
                $reordered = [];
                $reordered[1] = $objects[$catalogObjNum-1];
                $reordered[2] = $objects[$pagesObjNum-1];
                $reordered[3] = $objects[$fontBodyObj-1];
                $reordered[4] = $objects[$fontBoldObj-1];
                $oi = 5;
                for ($i=0; $i<count($pageObjNums); $i++) {
                    $reordered[$oi++] = $objects[$contentObjNums[$i]-1];
                    $reordered[$oi++] = $objects[$pageObjNums[$i]-1];
                }
                ksort($reordered);
                $finalObjects = array_values($reordered);
                foreach ($finalObjects as $idx => $obj) {
                    $num = $idx+1;
                    $offsets[] = strlen($pdf);
                    $pdf .= $num." 0 obj\n".$obj."endobj\n";
                }
                $xrefPos = strlen($pdf);
                $count = count($finalObjects) + 1; // including object 0
                $pdf .= "xref\n0 {$count}\n";
                $pdf .= sprintf("%010d %05d f\n", 0, 65535);
                for ($i=1; $i<$count; $i++) { $pdf .= sprintf("%010d %05d n\n", $offsets[$i], 0); }
                $pdf .= "trailer\n<< /Size {$count} /Root 1 0 R >>\nstartxref\n{$xrefPos}\n%%EOF";
                return $pdf;
            }
        }
    // Build minimal data series for charts from HTML context — we can’t perfectly parse the HTML here,
    // so include placeholders that still produce a visual PDF in fallback mode.
    $chart1 = ['label' => 'Orders (12 weeks)', 'values' => [5,9,7,12,8,13,15,12,18,16,20,22]];
    $chart2 = ['label' => 'Visitors (12 weeks)', 'values' => [20,23,19,25,22,27,30,29,31,33,35,37]];
    $chart3 = ['label' => 'Calls (12 weeks)', 'values' => [2,3,2,4,3,5,6,5,7,6,8,9]];
    $text = wp_strip_all_tags($html, true);
    $pdfData = vmo_generate_simple_pdf_from_text($text, 'VMO Progress Report', [$chart1,$chart2,$chart3]);
        $upload = wp_upload_dir();
        $dir = trailingslashit($upload['basedir']) . 'vmo-reports/';
        if (!file_exists($dir)) wp_mkdir_p($dir);
        $file = $dir . 'progress-' . get_current_user_id() . '-' . date('Ymd-His') . '.pdf';
        vmo_get_filesystem()->put_contents($file, $pdfData, FS_CHMOD_FILE);
        $url = trailingslashit($upload['baseurl']) . 'vmo-reports/' . basename($file);
        wp_send_json_success(['url' => esc_url_raw($url), 'format' => 'pdf', 'fallback' => true]);
    }
});

// Helper: build the HTML string for export (shared)
if (!function_exists('vmo_export_progress_html_string')) {
function vmo_export_progress_html_string() {
    // Build same content as vmo_export_progress but return as string
    global $wpdb;
    $user_id = get_current_user_id();
    $contacts_table = $wpdb->prefix . 'vmo_contacts';
    $approval_table = $wpdb->prefix . 'vmo_approval_queue';
    $calls_table = $wpdb->prefix . 'vmo_business_calls';
    $orders_table = $wpdb->prefix . 'vmo_store_orders';
    $interactions_table = $wpdb->prefix . 'vmo_contact_interactions';
    $campaigns_table = $wpdb->prefix . 'vmo_email_campaigns';

    $table_exists = function($table) {
        global $wpdb;
        $like = $wpdb->esc_like($table);
        return (bool) $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $like));
    };

    $contacts_count = 0; $leads_count = 0; $customers_count = 0;
    if ($table_exists($contacts_table)) {
        $contacts_count = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$contacts_table} WHERE user_id = %d", $user_id));
        $customers_count = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$contacts_table} WHERE user_id = %d AND lifecycle_stage = 'customer'", $user_id));
        $leads_count = max(0, $contacts_count - $customers_count);
    }
    $approvals_pending = 0;
    if ($table_exists($approval_table)) {
        $approvals_pending = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$approval_table} WHERE user_id = %d AND status = 'pending'", $user_id));
    }
    $today = current_time('Y-m-d');
    $calls_today = 0; $avg_call_today = 0.0;
    if ($table_exists($calls_table)) {
        $calls_today = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$calls_table} WHERE user_id = %d AND DATE(call_start_time) = %s", $user_id, $today));
        $avg_call_today = (float) $wpdb->get_var($wpdb->prepare("SELECT AVG(call_duration) FROM {$calls_table} WHERE user_id = %d AND DATE(call_start_time) = %s", $user_id, $today));
    }
    $sum_period = function($days) use ($wpdb, $orders_table, $user_id, $table_exists){
        if (!$table_exists($orders_table)) return 0;
        $since = date('Y-m-d H:i:s', strtotime('-' . (int)$days . ' days'));
        $q = $wpdb->get_var($wpdb->prepare("SELECT COALESCE(SUM(amount_total),0) FROM {$orders_table} WHERE user_id = %d AND status='paid' AND created_at >= %s", $user_id, $since));
        return (int)$q;
    };
    $sales_day = $sum_period(1);
    $sales_week = $sum_period(7);
    $sales_month = $sum_period(30);
    $sales_year = $sum_period(365);

    $trend_labels = [];$trend_values = [];
    if ($table_exists($orders_table)) {
        $rows = $wpdb->get_results($wpdb->prepare(
            "SELECT DATE_FORMAT(created_at,'%%x-%%v') AS yw, SUM(amount_total) AS amt
             FROM {$orders_table}
             WHERE user_id = %d AND status='paid' AND created_at >= %s
             GROUP BY yw ORDER BY yw ASC",
            $user_id, date('Y-m-d H:i:s', strtotime('-84 days'))
        ), ARRAY_A);
        foreach ($rows as $r) { $trend_labels[] = $r['yw']; $trend_values[] = (int)$r['amt']; }
    }
    // Visitors trend (last 12 weeks)
    $vis_labels = [];$vis_values = [];
    if ($table_exists($interactions_table)) {
        $rows = $wpdb->get_results($wpdb->prepare(
            "SELECT DATE_FORMAT(created_at,'%%x-%%v') AS yw, COUNT(*) AS v
             FROM {$interactions_table}
             WHERE user_id = %d AND interaction_type='website_visit' AND created_at >= %s
             GROUP BY yw ORDER BY yw ASC",
            $user_id, date('Y-m-d H:i:s', strtotime('-84 days'))
        ), ARRAY_A);
        foreach (($rows ?: []) as $r) { $vis_labels[] = $r['yw']; $vis_values[] = (int)$r['v']; }
    }
    // Calls trend (last 12 weeks)
    $call_labels = [];$call_values = [];
    if ($table_exists($calls_table)) {
        $rows = $wpdb->get_results($wpdb->prepare(
            "SELECT DATE_FORMAT(call_start_time,'%%x-%%v') AS yw, COUNT(*) AS c
             FROM {$calls_table}
             WHERE user_id = %d AND call_start_time >= %s
             GROUP BY yw ORDER BY yw ASC",
            $user_id, date('Y-m-d H:i:s', strtotime('-84 days'))
        ), ARRAY_A);
        foreach (($rows ?: []) as $r) { $call_labels[] = $r['yw']; $call_values[] = (int)$r['c']; }
    }

    // MoM / YoY calculations (visits & purchases)
    $now = current_time('mysql');
    $d30 = date('Y-m-d H:i:s', strtotime('-30 days', strtotime($now)));
    $d60 = date('Y-m-d H:i:s', strtotime('-60 days', strtotime($now)));
    $d365 = date('Y-m-d H:i:s', strtotime('-365 days', strtotime($now)));
    $d730 = date('Y-m-d H:i:s', strtotime('-730 days', strtotime($now)));
    $vis_30 = 0; $vis_prev30 = 0; $vis_365 = 0; $vis_prev365 = 0;
    if ($table_exists($interactions_table)) {
        $vis_30 = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$interactions_table} WHERE user_id=%d AND interaction_type='website_visit' AND created_at BETWEEN %s AND %s", $user_id, $d30, $now));
        $vis_prev30 = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$interactions_table} WHERE user_id=%d AND interaction_type='website_visit' AND created_at BETWEEN %s AND %s", $user_id, $d60, $d30));
        $vis_365 = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$interactions_table} WHERE user_id=%d AND interaction_type='website_visit' AND created_at BETWEEN %s AND %s", $user_id, $d365, $now));
        $vis_prev365 = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$interactions_table} WHERE user_id=%d AND interaction_type='website_visit' AND created_at BETWEEN %s AND %s", $user_id, $d730, $d365));
    }
    $ord_30 = 0; $ord_prev30 = 0; $ord_365 = 0; $ord_prev365 = 0; $amt_30 = 0; $amt_365 = 0;
    if ($table_exists($orders_table)) {
        $ord_30 = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$orders_table} WHERE user_id=%d AND status='paid' AND created_at BETWEEN %s AND %s", $user_id, $d30, $now));
        $ord_prev30 = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$orders_table} WHERE user_id=%d AND status='paid' AND created_at BETWEEN %s AND %s", $user_id, $d60, $d30));
        $ord_365 = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$orders_table} WHERE user_id=%d AND status='paid' AND created_at BETWEEN %s AND %s", $user_id, $d365, $now));
        $ord_prev365 = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$orders_table} WHERE user_id=%d AND status='paid' AND created_at BETWEEN %s AND %s", $user_id, $d730, $d365));
        $amt_30 = (int) $wpdb->get_var($wpdb->prepare("SELECT COALESCE(SUM(amount_total),0) FROM {$orders_table} WHERE user_id=%d AND status='paid' AND created_at BETWEEN %s AND %s", $user_id, $d30, $now));
        $amt_365 = (int) $wpdb->get_var($wpdb->prepare("SELECT COALESCE(SUM(amount_total),0) FROM {$orders_table} WHERE user_id=%d AND status='paid' AND created_at BETWEEN %s AND %s", $user_id, $d365, $now));
    }
    $calls_30 = 0; $calls_prev30 = 0; $calls_365 = 0; $calls_prev365 = 0;
    if ($table_exists($calls_table)) {
        $calls_30 = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$calls_table} WHERE user_id=%d AND call_start_time BETWEEN %s AND %s", $user_id, $d30, $now));
        $calls_prev30 = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$calls_table} WHERE user_id=%d AND call_start_time BETWEEN %s AND %s", $user_id, $d60, $d30));
        $calls_365 = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$calls_table} WHERE user_id=%d AND call_start_time BETWEEN %s AND %s", $user_id, $d365, $now));
        $calls_prev365 = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$calls_table} WHERE user_id=%d AND call_start_time BETWEEN %s AND %s", $user_id, $d730, $d365));
    }
    // Conversion rate (last 30 days)
    $conversion_rate_30 = ($vis_30 > 0) ? round(($ord_30 / max(1,$vis_30)) * 100, 2) : 0;
    // Email campaigns in last 30 days
    $campaigns_30 = 0;
    if ($table_exists($campaigns_table)) {
        $campaigns_30 = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$campaigns_table} WHERE user_id=%d AND created_at BETWEEN %s AND %s", $user_id, $d30, $now));
    }
    // Roadmap progress & ETA
    $followups_table = $wpdb->prefix . 'vmo_follow_ups';
    $fu_exists = $table_exists($followups_table);
    $road_total = 0; $road_done = 0; $road_pct = 0; $eta_text = '—';
    if ($fu_exists) {
        $road_total = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$followups_table} WHERE user_id=%d AND scheduled_date BETWEEN %s AND %s", $user_id, $d365, date('Y-m-d H:i:s', strtotime('+365 days', strtotime($now)))));
        $road_done = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$followups_table} WHERE user_id=%d AND status='completed' AND scheduled_date BETWEEN %s AND %s", $user_id, $d365, $now));
        $road_pct = ($road_total > 0) ? round(($road_done / max(1,$road_total)) * 100) : 0;
        // Completion velocity (last 30 days)
        $completed_30 = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$followups_table} WHERE user_id=%d AND status='completed' AND updated_at BETWEEN %s AND %s", $user_id, $d30, $now));
        $pending_now = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$followups_table} WHERE user_id=%d AND status='pending'", $user_id));
        $per_day = $completed_30 / 30.0;
        if ($per_day > 0 && $pending_now > 0) {
            $days_left = (int) ceil($pending_now / $per_day);
            $eta_date = date('Y-m-d', strtotime("+{$days_left} days", strtotime($now)));
            $eta_text = sprintf('%d days (ETA %s)', $days_left, $eta_date);
        }
    }
    $fmt = function($cents){ return '$' . number_format(($cents/100), 2); };
    $max = max(1, ...$trend_values ?: [1]);
    $points = [];
    foreach ($trend_values as $i => $v) {
        $x = count($trend_values) > 1 ? ($i/(count($trend_values)-1))*100 : 0;
        $y = 100 - (($v/$max)*100);
        $points[] = sprintf('%0.2f,%0.2f', $x, $y);
    }
    $svg = '<svg viewBox="0 0 100 100" preserveAspectRatio="none" style="width:100%;height:160px;background:#0b1117;border:1px solid rgba(255,255,255,.1);border-radius:8px">'
         . (count($points) ? '<polyline fill="none" stroke="#0fe1ff" stroke-width="2" points="' . implode(' ', $points) . '" />' : '')
         . '</svg>';
    // Visitors sparkline
    $vmax = max(1, ...$vis_values ?: [1]); $vpts=[]; foreach($vis_values as $i=>$v){ $x = count($vis_values)>1 ? ($i/(count($vis_values)-1))*100 : 0; $y = 100 - (($v/$vmax)*100); $vpts[] = sprintf('%0.2f,%0.2f',$x,$y);}    
    $vis_svg = '<svg viewBox="0 0 100 30" preserveAspectRatio="none" style="width:100%;height:80px;background:#0b1117;border:1px solid rgba(255,255,255,.1);border-radius:8px">'
             . (count($vpts) ? '<polyline fill="none" stroke="#65f1ff" stroke-width="2" points="' . implode(' ', $vpts) . '" />' : '')
             . '</svg>';
    // Calls sparkline
    $cmax = max(1, ...$call_values ?: [1]); $cpts=[]; foreach($call_values as $i=>$v){ $x = count($call_values)>1 ? ($i/(count($call_values)-1))*100 : 0; $y = 100 - (($v/$cmax)*100); $cpts[] = sprintf('%0.2f,%0.2f',$x,$y);}    
    $call_svg = '<svg viewBox="0 0 100 30" preserveAspectRatio="none" style="width:100%;height:80px;background:#0b1117;border:1px solid rgba(255,255,255,.1);border-radius:8px">'
              . (count($cpts) ? '<polyline fill="none" stroke="#7c5cff" stroke-width="2" points="' . implode(' ', $cpts) . '" />' : '')
              . '</svg>';
    $html  = '<!doctype html><meta charset="utf-8"><title>Progress Report</title>';
    $html .= '<div style="font-family:Inter,system-ui;background:#0a0f16;color:#e6f3ff;padding:20px">';
    $html .= '<h1 style="color:#65f1ff">VMO Progress Report</h1>';
    $html .= '<p>Generated: ' . esc_html(current_time('mysql')) . '</p>';
    // Summary
    $html .= '<h2>Executive Summary</h2>';
    $html .= '<p style="color:#9fb2c8">This report summarizes current business performance across contacts, website traffic, conversions, phone activity, and roadmap progress. It is designed for distribution to leadership without requiring WordPress access.</p>';
    // Contacts section
    $html .= '<h2>Contacts & Customers</h2>';
    $html .= '<ul>';
    $html .= '<li>Total Contacts: ' . number_format_i18n($contacts_count) . '</li>';
    $html .= '<li>Leads: ' . number_format_i18n($leads_count) . '</li>';
    $html .= '<li>Customers: ' . number_format_i18n($customers_count) . '</li>';
    $html .= '<li>Pending Approvals: ' . number_format_i18n($approvals_pending) . '</li>';
    $html .= '</ul>';
    // Traffic & Conversion section
    $mom = ($vis_prev30>0) ? round((($vis_30 - $vis_prev30)/$vis_prev30)*100,2) : ($vis_30>0?100:0);
    $yoy = ($vis_prev365>0) ? round((($vis_365 - $vis_prev365)/$vis_prev365)*100,2) : ($vis_365>0?100:0);
    $mom_orders = ($ord_prev30>0) ? round((($ord_30 - $ord_prev30)/$ord_prev30)*100,2) : ($ord_30>0?100:0);
    $yoy_orders = ($ord_prev365>0) ? round((($ord_365 - $ord_prev365)/$ord_prev365)*100,2) : ($ord_365>0?100:0);
    $html .= '<h2>Traffic & Conversions</h2>';
    $html .= '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px">';
    $html .= '<div style="background:#0b1117;border:1px solid rgba(255,255,255,.1);border-radius:8px;padding:12px"><h3 style="margin:0 0 6px;color:#65f1ff">Visitors (last 12 weeks)</h3>'.$vis_svg.'<p style="margin:6px 0 0;color:#9fb2c8">MoM: '.esc_html(($mom>=0?'+':'').$mom.'%').' • YoY: '.esc_html(($yoy>=0?'+':'').$yoy.'%').'</p></div>';
    $html .= '<div style="background:#0b1117;border:1px solid rgba(255,255,255,.1);border-radius:8px;padding:12px"><h3 style="margin:0 0 6px;color:#65f1ff">Orders (last 12 weeks)</h3>'.$svg.'<p style="margin:6px 0 0;color:#9fb2c8">MoM: '.esc_html(($mom_orders>=0?'+':'').$mom_orders.'%').' • YoY: '.esc_html(($yoy_orders>=0?'+':'').$yoy_orders.'%').'</p></div>';
    $html .= '<div style="background:#0b1117;border:1px solid rgba(255,255,255,.1);border-radius:8px;padding:12px"><h3 style="margin:0 0 6px;color:#65f1ff">Conversion (30d)</h3><p style="margin:0;color:#e6f3ff;font-size:24px;font-weight:700">'.esc_html(number_format_i18n($conversion_rate_30,2)).'%</p><p style="margin:6px 0 0;color:#9fb2c8">Visits: '.number_format_i18n($vis_30).' • Orders: '.number_format_i18n($ord_30).' • Revenue: '.esc_html($fmt($amt_30)).'</p><p style="margin:6px 0 0;color:#9fb2c8">Email campaigns sent (30d): '.number_format_i18n($campaigns_30).'</p></div>';
    $html .= '</div>';
    // Calls section
    $mom_calls = ($calls_prev30>0) ? round((($calls_30 - $calls_prev30)/$calls_prev30)*100,2) : ($calls_30>0?100:0);
    $yoy_calls = ($calls_prev365>0) ? round((($calls_365 - $calls_prev365)/$calls_prev365)*100,2) : ($calls_365>0?100:0);
    $html .= '<h2>Phone Activity</h2>';
    $html .= '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px">';
    $html .= '<div style="background:#0b1117;border:1px solid rgba(255,255,255,.1);border-radius:8px;padding:12px"><h3 style="margin:0 0 6px;color:#65f1ff">Calls (last 12 weeks)</h3>'.$call_svg.'<p style="margin:6px 0 0;color:#9fb2c8">MoM: '.esc_html(($mom_calls>=0?'+':'').$mom_calls.'%').' • YoY: '.esc_html(($yoy_calls>=0?'+':'').$yoy_calls.'%').'</p></div>';
    $html .= '<div style="background:#0b1117;border:1px solid rgba(255,255,255,.1);border-radius:8px;padding:12px"><h3 style="margin:0 0 6px;color:#65f1ff">Today</h3><p style="margin:0;color:#e6f3ff;font-size:24px;font-weight:700">'.number_format_i18n($calls_today).'</p><p style="margin:6px 0 0;color:#9fb2c8">Avg duration: '.number_format_i18n(round($avg_call_today)).'s</p></div>';
    $html .= '</div>';
    // Sales summary
    $html .= '<h2>Sales Summary</h2>';
    $html .= '<ul>';
    $html .= '<li>Sales today: ' . esc_html($fmt($sales_day)) . '</li>';
    $html .= '<li>Sales 7 days: ' . esc_html($fmt($sales_week)) . '</li>';
    $html .= '<li>Sales 30 days: ' . esc_html($fmt($sales_month)) . '</li>';
    $html .= '<li>Sales 365 days: ' . esc_html($fmt($sales_year)) . '</li>';
    $html .= '</ul>';
    // Roadmap progress
    $html .= '<h2>Roadmap Progress</h2>';
    $html .= '<p style="margin:0 0 6px;color:#9fb2c8">Objectives completed: '.number_format_i18n($road_done).' / '.number_format_i18n($road_total).' ('.number_format_i18n($road_pct).'%)</p>';
    $html .= '<p style="margin:0 0 12px;color:#9fb2c8">Estimated time to complete remaining activities: '.esc_html($eta_text).'</p>';
    // Footer note
    $html .= '<p style="margin-top:16px;color:#9fb2c8">This report aggregates operational signals across your VMO systems. For details or to adjust objectives, visit the VMO Strategic Dashboard.</p>';
    $html .= '</div>';
    return $html;
}
}

// Persist task completion
add_action('wp_ajax_vmo_update_task_status', function() {
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if ((isset($_POST['security']) || isset($_POST['nonce'])) && !vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Invalid nonce', 'status' => 'invalid_nonce']);
    }
    global $wpdb;
    $id = isset($_POST['id']) ? intval($_POST['id']) : 0;
    $checked = isset($_POST['checked']) ? (bool) $_POST['checked'] : false;
    if ($id <= 0) wp_send_json_error(['message' => 'Invalid task ID']);
    $table = $wpdb->prefix . 'vmo_follow_ups';
    $exists = vmo_table_exists($table);
    if (!$exists) wp_send_json_error(['message' => 'Follow-ups table not available']);
    $status = $checked ? 'completed' : 'pending';
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    $res = $wpdb->update($table, [ 'status' => $status, 'updated_at' => current_time('mysql') ], [ 'id' => $id, 'user_id' => get_current_user_id() ], [ '%s','%s' ], [ '%d','%d' ]);
    if ($res === false) wp_send_json_error(['message' => 'Update failed']);
    wp_send_json_success(['message' => 'Updated', 'status' => $status]);
});

// Schedule a manual task into follow-ups table
add_action('wp_ajax_vmo_schedule_task', 'vmo_schedule_task');
function vmo_schedule_task() {
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if ((isset($_POST['security']) || isset($_POST['nonce'])) && !vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Invalid nonce', 'status' => 'invalid_nonce']);
    }
    global $wpdb;
    $user_id = get_current_user_id();
    $title = sanitize_text_field($_POST['title'] ?? '');
    $notes = sanitize_textarea_field($_POST['notes'] ?? '');
    $due = sanitize_text_field($_POST['due'] ?? '');
    if (!$title || !$due) {
        wp_send_json_error(['message' => __('Title and due date are required', 'vmo')], 400);
    }
    $table = $wpdb->prefix . 'vmo_follow_ups';
    // Ensure table exists
    $like = $wpdb->esc_like($table);
    $exists = (bool) $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $like));
    if (!$exists) {
        wp_send_json_error(['message' => __('Follow-ups table not available', 'vmo')], 500);
    }
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    $res = $wpdb->insert($table, [
        'user_id' => $user_id,
        'customer_id' => 0,
        'call_id' => null,
        'follow_up_type' => 'manual_task',
        'scheduled_date' => gmdate('Y-m-d H:i:s', strtotime($due)),
        'status' => 'pending',
        'notes' => $title . ($notes ? (' — ' . $notes) : ''),
        'created_at' => current_time('mysql'),
        'updated_at' => current_time('mysql'),
    ], [
        '%d','%d','%d','%s','%s','%s','%s','%s','%s'
    ]);
    if (!$res) {
        wp_send_json_error(['message' => __('Failed to schedule task', 'vmo')], 500);
    }
    wp_send_json_success(['message' => __('Task scheduled', 'vmo')]);
}

// Website Analytics AJAX handlers
add_action('wp_ajax_vmo_get_visitors_data', 'vmo_get_visitors_data');
function vmo_get_visitors_data() {
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if (!vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Security check failed', 'status' => 'invalid_nonce'], 403);
    }
    global $wpdb;
    $user_id = get_current_user_id();
    // Accept explicit from/to or fallback to last N days
    $days = isset($_POST['days']) ? (int) $_POST['days'] : 30;
    $from = isset($_POST['from']) ? sanitize_text_field(wp_unslash($_POST['from'])) : '';
    $to   = isset($_POST['to'])   ? sanitize_text_field(wp_unslash($_POST['to']))   : '';
    $now  = current_time('mysql');
    if (!$to) $to = $now;
    if (!$from) $from = date('Y-m-d H:i:s', strtotime('-' . max(1,$days) . ' days', strtotime($to)));
    
    // Prefer tracked interaction data when available and return safe zeroed values when external analytics are unavailable.
    $interactions_table = $wpdb->prefix . 'vmo_contact_interactions';
    $unique_visitors = 0; $total_visits = 0; $bounce_rate = 0; $avg_duration = 0;
    
    if (vmo_table_exists($interactions_table)) {
        $visits = $wpdb->get_results($wpdb->prepare(
            "SELECT DATE(created_at) as date, COUNT(*) as visits, COUNT(DISTINCT contact_id) as unique_visitors
             FROM {$interactions_table} 
             WHERE user_id = %d AND interaction_type = 'website_visit' AND created_at BETWEEN %s AND %s
             GROUP BY DATE(created_at) ORDER BY date ASC",
            $user_id, $from, $to
        ), ARRAY_A);
        
        $chart_data = []; $labels = [];
        foreach ($visits as $day) {
            $labels[] = date('M j', strtotime($day['date']));
            $chart_data[] = (int) $day['visits'];
            $total_visits += (int) $day['visits'];
            $unique_visitors += (int) $day['unique_visitors'];
        }
    // Average duration unknown without explicit column; show em dash
    $avg_duration = '—';
        $bounce_rate = 0; // Unknown without proper signals; return 0
    } else {
        // Table missing: return zeros/— safely
        $unique_visitors = 0; $total_visits = 0;
        $bounce_rate = 0; $avg_duration = '—';
        $chart_data = [];
        $labels = [];
    }
    
    wp_send_json_success([
        'unique_visitors' => $unique_visitors,
        'total_visits' => $total_visits,
        'bounce_rate' => $bounce_rate,
        'avg_duration' => $avg_duration,
        'chart' => ['labels' => $labels, 'data' => $chart_data]
    ]);
}

add_action('wp_ajax_vmo_get_demographics_data', 'vmo_get_demographics_data');
function vmo_get_demographics_data() {
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if ((isset($_POST['security']) || isset($_POST['nonce'])) && !vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Invalid nonce', 'status' => 'invalid_nonce']);
    }
    global $wpdb;
    $user_id = get_current_user_id();
    global $wpdb;
    $from = isset($_POST['from']) ? sanitize_text_field(wp_unslash($_POST['from'])) : '';
    $to   = isset($_POST['to'])   ? sanitize_text_field(wp_unslash($_POST['to']))   : '';
    $days = isset($_POST['days']) ? (int) $_POST['days'] : 30;
    $now  = current_time('mysql');
    if (!$to) $to = $now;
    if (!$from) $from = date('Y-m-d H:i:s', strtotime('-' . max(1,$days) . ' days', strtotime($to)));

    // Top locations from contacts table and/or interactions metadata
    $contacts_table = $wpdb->prefix . 'vmo_contacts';
    $interactions_table = $wpdb->prefix . 'vmo_contact_interactions';
    $locations = [];
    $states = [];
    $pages = [];
    $countries_count = 0;
    $total_visits = 0;
    $unique_visitors = 0;
    $visitors = [];
    // Stable zeroed facets
    $age_ranges = [
        '18-24' => 0,
        '25-34' => 0,
        '35-44' => 0,
        '45-54' => 0,
        '55+'   => 0,
    ];
    $devices = [ 'mobile' => 0, 'desktop' => 0, 'tablet' => 0, 'other' => 0 ];
    $oses    = [ 'ios' => 0, 'android' => 0, 'windows' => 0, 'macos' => 0, 'linux' => 0, 'other' => 0 ];
    $referrers = [
        [ 'source' => 'Google', 'count' => 0 ],
        [ 'source' => 'Direct', 'count' => 0 ],
        [ 'source' => 'Social', 'count' => 0 ],
        [ 'source' => 'YouTube Ads', 'count' => 0 ],
        [ 'source' => 'Facebook Ads', 'count' => 0 ],
        [ 'source' => 'Email', 'count' => 0 ],
        [ 'source' => 'Other', 'count' => 0 ],
    ];

    $contacts_exists = vmo_table_exists($contacts_table);
    $interactions_exists = vmo_table_exists($interactions_table);

    if ($contacts_exists) {
        $rows = $wpdb->get_results($wpdb->prepare(
            "SELECT country, COUNT(*) c FROM {$contacts_table} WHERE user_id=%d AND country IS NOT NULL AND country<>'' GROUP BY country ORDER BY c DESC LIMIT 5",
            $user_id
        ), ARRAY_A);
        $countries_count = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(DISTINCT country) FROM {$contacts_table} WHERE user_id = %d AND country IS NOT NULL AND country<>''",
            $user_id
        ));
        foreach ($rows as $r) { $locations[] = ($r['country'] ?: 'Unknown') . ' - ' . (int)$r['c']; }
        // Optional: states (best-effort if a 'state' column exists)
        if ($wpdb->get_col_info('name', 0) || true) {
            // Guarded query; if no state column, query will fail silently in try/catch
            try {
                $srows = $wpdb->get_results($wpdb->prepare(
                    "SELECT state, COUNT(*) c FROM {$contacts_table} WHERE user_id=%d AND state IS NOT NULL AND state<>'' GROUP BY state ORDER BY c DESC LIMIT 5",
                    $user_id
                ), ARRAY_A);
                foreach (($srows ?: []) as $sr) { $states[] = ($sr['state'] ?: 'Unknown') . ' - ' . (int)$sr['c']; }
            } catch (Throwable $e) { /* ignore */ }
        }
    }

    if ($interactions_exists) {
        // Aggregate visit counts
        $total_visits = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$interactions_table} WHERE user_id=%d AND interaction_type='website_visit' AND created_at BETWEEN %s AND %s",
            $user_id, $from, $to
        ));
        $unique_visitors = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(DISTINCT contact_id) FROM {$interactions_table} WHERE user_id=%d AND interaction_type='website_visit' AND created_at BETWEEN %s AND %s",
            $user_id, $from, $to
        ));

        // Derive top pages from interaction_subtype or subject if present
        $rows = $wpdb->get_results($wpdb->prepare(
            "SELECT COALESCE(interaction_subtype, subject, '/') AS page, COUNT(*) c
             FROM {$interactions_table}
             WHERE user_id=%d AND interaction_type='website_visit' AND created_at BETWEEN %s AND %s
             GROUP BY page ORDER BY c DESC LIMIT 5",
            $user_id, $from, $to
        ), ARRAY_A);
        foreach (($rows ?: []) as $r) {
            $p = trim((string) $r['page']);
            if ($p === '') $p = '/';
            $pages[] = $p . ' - ' . (int)$r['c'];
        }

        // Build a visitors summary list (contact_id grouping)
        $vrows = $wpdb->get_results($wpdb->prepare(
            "SELECT contact_id, COUNT(*) AS visits, MAX(created_at) AS last_visit
             FROM {$interactions_table}
             WHERE user_id=%d AND interaction_type='website_visit' AND created_at BETWEEN %s AND %s AND contact_id IS NOT NULL AND contact_id<>''
             GROUP BY contact_id ORDER BY visits DESC, last_visit DESC LIMIT 20",
            $user_id, $from, $to
        ), ARRAY_A);
        if ($vrows) {
            // Try to enrich with email/name from contacts table
            $contact_map = [];
            if ($contacts_exists) {
                $ids = array_filter(array_unique(array_column($vrows, 'contact_id')));
                if ($ids) {
                    // contact_id is stored as VARCHAR; fetch by contact_id column if present else attempt id
                    try {
                        $in = implode(',', array_fill(0, count($ids), '%s'));
                        $sql = "SELECT * FROM {$contacts_table} WHERE user_id=%d AND contact_id IN ($in)";
                        $prepared = $wpdb->prepare($sql, array_merge([$user_id], $ids));
                        $crows = $wpdb->get_results($prepared, ARRAY_A);
                        foreach (($crows?:[]) as $cr) {
                            $cr_name = $cr['full_name'] ?? '';
                            if (empty($cr_name)) { $cr_name = trim(($cr['first_name'] ?? '') . ' ' . ($cr['last_name'] ?? '')); }
                            $contact_map[(string)$cr['contact_id']] = [ 'email' => (string)($cr['email']?:''), 'name' => (string)$cr_name ];
                        }
                    } catch (Throwable $e) { /* ignore */ }
                }
            }
            foreach ($vrows as $vr) {
                $cid = (string) $vr['contact_id'];
                $meta = $contact_map[$cid] ?? ['email'=>'','name'=>''];
                $visitors[] = [
                    'contact_id' => $cid,
                    'name' => $meta['name'],
                    'email' => $meta['email'],
                    'visits' => (int) $vr['visits'],
                    'last_visit' => (string) $vr['last_visit'],
                    'source' => '—'
                ];
            }
        }
    }

    // Build recent activities from interactions
    $activities = [];
    if ($interactions_exists) {
        $act_rows = $wpdb->get_results($wpdb->prepare(
            "SELECT interaction_type, COALESCE(interaction_subtype, subject, '') AS detail,
                    created_at, contact_id
             FROM {$interactions_table}
             WHERE user_id = %d AND created_at BETWEEN %s AND %s
             ORDER BY created_at DESC LIMIT 20",
            $user_id, $from, $to
        ), ARRAY_A);
        foreach (($act_rows ?: []) as $ar) {
            $label = ucwords(str_replace('_', ' ', $ar['interaction_type']));
            $detail = !empty($ar['detail']) ? (' — ' . mb_substr($ar['detail'], 0, 80)) : '';
            $activities[] = [
                'type' => $ar['interaction_type'],
                'label' => $label . $detail,
                'contact_id' => $ar['contact_id'] ?? '',
                'timestamp' => $ar['created_at'],
            ];
        }
    }

    // Build cart / order activity from store orders
    $cart_activity = [];
    $orders_table = $wpdb->prefix . 'vmo_internal_store_orders';
    $orders_exists = vmo_table_exists($orders_table);
    if ($orders_exists) {
        $order_rows = $wpdb->get_results($wpdb->prepare(
            "SELECT id, status, total_amount, currency, created_at
             FROM {$orders_table}
             WHERE user_id = %d AND created_at BETWEEN %s AND %s
             ORDER BY created_at DESC LIMIT 15",
            $user_id, $from, $to
        ), ARRAY_A);
        foreach (($order_rows ?: []) as $or) {
            $cart_activity[] = [
                'order_id' => $or['id'],
                'status' => $or['status'],
                'amount' => floatval($or['total_amount']),
                'currency' => $or['currency'] ?? 'USD',
                'timestamp' => $or['created_at'],
            ];
        }
    }

    wp_send_json_success([
        // Basic facets (gender/age require analytics integration — honest zeros)
        'gender' => ['male' => 0, 'female' => 0, 'other' => 0],
        'age_ranges' => $age_ranges,
        'avg_age' => 0,
        // Locations
        'countries' => $countries_count,
        'locations' => $locations ?: [],
        'states' => $states ?: [],
        // Devices and OS
        'devices' => $devices,
        'os' => $oses,
        // Referrers
        'referrers' => $referrers,
        // Content/pages
        'pages' => $pages ?: [],
        // Aggregate visit counts
        'total_visits' => $total_visits,
        'unique_visitors' => $unique_visitors,
        // Visitor list summary for the period
        'visitors' => $visitors,
        // Real activities from interactions
        'activities' => $activities,
        // Real cart/order activity from store orders
        'cart_activity' => $cart_activity,
    ]);
}

add_action('wp_ajax_vmo_get_conversions_data', 'vmo_get_conversions_data');
function vmo_get_conversions_data() {
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if ((isset($_POST['security']) || isset($_POST['nonce'])) && !vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Invalid nonce', 'status' => 'invalid_nonce']);
    }
    global $wpdb;
    $user_id = get_current_user_id();
    $from = isset($_POST['from']) ? sanitize_text_field(wp_unslash($_POST['from'])) : '';
    $to   = isset($_POST['to'])   ? sanitize_text_field(wp_unslash($_POST['to']))   : '';
    $days = isset($_POST['days']) ? (int) $_POST['days'] : 30;
    $now  = current_time('mysql');
    if (!$to) $to = $now;
    if (!$from) $from = date('Y-m-d H:i:s', strtotime('-' . max(1,$days) . ' days', strtotime($to)));

    // Get real conversion data from orders if available
    $orders_table = $wpdb->prefix . 'vmo_store_orders';
    $table_exists = vmo_table_exists($orders_table);
    
    if ($table_exists) {
        $total_orders = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$orders_table} WHERE user_id = %d AND status = 'paid' AND created_at BETWEEN %s AND %s",
            $user_id, $from, $to
        ));
        $avg_order_value = (float) $wpdb->get_var($wpdb->prepare(
            "SELECT AVG(amount_total) FROM {$orders_table} WHERE user_id = %d AND status = 'paid' AND created_at BETWEEN %s AND %s",
            $user_id, $from, $to
        ));
        $avg_order_value = $avg_order_value ? '$' . number_format($avg_order_value / 100, 2) : '$0.00';
    } else {
        $total_orders = 0;
        $avg_order_value = '$0.00';
    }
    
    // Attempt to compute visits in same window for a crude conversion rate
    $interactions_table = $wpdb->prefix . 'vmo_contact_interactions';
    $visits = 0;
    if (vmo_table_exists($interactions_table)) {
        $visits = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$interactions_table} WHERE user_id=%d AND interaction_type='website_visit' AND created_at BETWEEN %s AND %s",
            $user_id, $from, $to
        ));
    }
    $conversion_rate = ($visits > 0 && $total_orders >= 0) ? round(($total_orders / max(1,$visits)) * 100, 2) : 0;
    $cart_abandon = 0;
    if (class_exists('VMO_Abandoned_Cart')) {
        $ac_stats = VMO_Abandoned_Cart::instance()->get_stats($user_id);
        $cart_abandon = $ac_stats['total_abandoned'] ?? 0;
    }
    
    // Build simple orders-per-day series within the window and conversion-rate series
    $labels = [];$chart_data = [];$rate_series = [];
    if ($table_exists) {
        $rows = $wpdb->get_results($wpdb->prepare(
            "SELECT DATE(created_at) d, COUNT(*) c FROM {$orders_table}
             WHERE user_id=%d AND status='paid' AND created_at BETWEEN %s AND %s
             GROUP BY DATE(created_at) ORDER BY d ASC",
            $user_id, $from, $to
        ), ARRAY_A);
        foreach ($rows as $r) { $labels[] = date('M j', strtotime($r['d'])); $chart_data[] = (int)$r['c']; }
        // Build conversion rate per day using visits if available
        if (vmo_table_exists($interactions_table)) {
            foreach ($rows as $r) {
                $day = $r['d'];
                $vis_day = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$interactions_table} WHERE user_id=%d AND interaction_type='website_visit' AND DATE(created_at)=%s", $user_id, $day));
                $ord_day = (int) $r['c'];
                $rate_series[] = $vis_day > 0 ? round(($ord_day / max(1,$vis_day)) * 100, 2) : 0;
            }
        }
    }
    
    // Compute funnel steps from real interaction data
    $funnel_visitors  = $visits; // already computed above from vmo_contact_interactions
    $funnel_add_to_cart = 0;
    $funnel_checkout    = 0;
    if (vmo_table_exists($interactions_table)) {
        $funnel_add_to_cart = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$interactions_table} WHERE user_id=%d AND interaction_type='add_to_cart' AND created_at BETWEEN %s AND %s",
            $user_id, $from, $to
        ));
        $funnel_checkout = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$interactions_table} WHERE user_id=%d AND interaction_type IN ('checkout','checkout_started') AND created_at BETWEEN %s AND %s",
            $user_id, $from, $to
        ));
    }

    wp_send_json_success([
        'conversion_rate' => $conversion_rate,
        'total_conversions' => $total_orders,
        'avg_order_value' => $avg_order_value,
        'cart_abandon' => $cart_abandon,
        'funnel' => [
            'visitors' => $funnel_visitors,
            'add_to_cart' => $funnel_add_to_cart,
            'checkout' => $funnel_checkout,
            'purchase' => $total_orders
        ],
        'chart' => ['labels' => $labels, 'data' => $chart_data],
        'rate_series' => $rate_series
    ]);
}

add_action('wp_ajax_vmo_get_competitors_data', 'vmo_get_competitors_data');
// Alias: dashboard JS calls `vmo_get_competitors` (not `_data`)
add_action('wp_ajax_vmo_get_competitors', 'vmo_get_competitors_data');

// ── Real handlers for dashboard panels backed by existing DB tables ──
add_action('wp_ajax_vmo_get_market_scans', function () {
    if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['nonce'] ?? $_REQUEST['security'] ?? '')), 'vmo_nonce')) { wp_send_json_error(['message' => 'Security check failed'], 403); }
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    global $wpdb;
    $user_id = get_current_user_id();
    $table = $wpdb->prefix . 'vmo_phase_outputs';
    $has_table = vmo_table_exists($table);
    $scans = [];
    $total_scans = 0;
    if ($has_table) {
        $rows = $wpdb->get_results($wpdb->prepare(
            "SELECT id, session_id, output_json, is_approved, created_at, updated_at
             FROM {$table}
             WHERE user_id = %d AND output_type = 'market_research' AND is_active = 1
             ORDER BY created_at DESC LIMIT 20",
            $user_id
        ), ARRAY_A);
        $total_scans = count($rows);
        foreach ($rows as $row) {
            $data = json_decode($row['output_json'], true);
            $scans[] = [
                'id' => (int) $row['id'],
                'session_id' => $row['session_id'],
                'competitors' => count($data['competitors'] ?? []),
                'threats' => count($data['threats'] ?? []),
                'opportunities' => count($data['opportunities'] ?? []),
                'summary' => $data['summary'] ?? '',
                'approved' => (bool) $row['is_approved'],
                'created_at' => $row['created_at'],
            ];
        }
    }
    wp_send_json_success([
        'scans' => $scans,
        'stats' => ['total_scans' => $total_scans],
    ]);
});

add_action('wp_ajax_vmo_get_business_goals', function () {
    if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['nonce'] ?? $_REQUEST['security'] ?? '')), 'vmo_nonce')) { wp_send_json_error(['message' => 'Security check failed'], 403); }
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    global $wpdb;
    $user_id = get_current_user_id();
    $table = $wpdb->prefix . 'vmo_business_roadmaps';
    $has_table = vmo_table_exists($table);
    $goals = [];
    $total_goals = 0;
    $total_progress = 0;
    if ($has_table) {
        $rows = $wpdb->get_results($wpdb->prepare(
            "SELECT id, roadmap_id, business_vision, roadmap_data, total_milestones, total_activities,
                    roadmap_start_date, vision_target_date, current_day_number, status, created_at
             FROM {$table}
             WHERE user_id = %d AND is_active = 1
             ORDER BY created_at DESC LIMIT 20",
            $user_id
        ), ARRAY_A);
        $total_goals = count($rows);

        // Get progress data
        $progress_table = $wpdb->prefix . 'vmo_progress_tracking';
        $has_progress = vmo_table_exists($progress_table);

        foreach ($rows as $row) {
            $progress = 0;
            $health = 'on_track';
            if ($has_progress) {
                $latest = $wpdb->get_row($wpdb->prepare(
                    "SELECT progress_percentage, schedule_health FROM {$progress_table} WHERE roadmap_id = %s AND user_id = %d ORDER BY tracking_date DESC LIMIT 1",
                    $row['roadmap_id'], $user_id
                ));
                if ($latest) {
                    $progress = round((float) $latest->progress_percentage, 1);
                    $health = $latest->schedule_health ?: 'on_track';
                }
            }
            $total_progress += $progress;

            $goals[] = [
                'id' => (int) $row['id'],
                'roadmap_id' => $row['roadmap_id'],
                'vision' => $row['business_vision'],
                'milestones' => (int) $row['total_milestones'],
                'activities' => (int) $row['total_activities'],
                'start_date' => $row['roadmap_start_date'],
                'target_date' => $row['vision_target_date'],
                'current_day' => (int) $row['current_day_number'],
                'status' => $row['status'],
                'progress' => $progress,
                'health' => $health,
                'created_at' => $row['created_at'],
            ];
        }
    }
    $avg_progress = $total_goals > 0 ? round($total_progress / $total_goals, 1) : 0;
    wp_send_json_success([
        'goals' => $goals,
        'stats' => [
            'total_goals' => $total_goals,
            'average_progress' => $avg_progress,
        ],
    ]);
});

add_action('wp_ajax_vmo_get_roadmap_tasks', function () {
    if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['nonce'] ?? $_REQUEST['security'] ?? '')), 'vmo_nonce')) { wp_send_json_error(['message' => 'Security check failed'], 403); }
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    global $wpdb;
    $user_id = get_current_user_id();
    $table = $wpdb->prefix . 'vmo_daily_activities';
    $has_table = vmo_table_exists($table);
    $tasks = [];
    $stats = [
        'total' => 0,
        'pending' => 0,
        'in_progress' => 0,
        'completed' => 0,
        'overdue' => 0,
    ];
    if ($has_table) {
        $rows = $wpdb->get_results($wpdb->prepare(
            "SELECT id, activity_id, roadmap_id, milestone_id, activity_title, activity_description,
                    activity_type, priority, status, activity_date, expected_complete_by,
                    actual_completed_at, ai_generated, recurring, recurrence_pattern
             FROM {$table}
             WHERE user_id = %d
             ORDER BY priority ASC, activity_date ASC
             LIMIT 50",
            $user_id
        ), ARRAY_A);

        $now = current_time('Y-m-d');
        foreach ($rows as $row) {
            $is_overdue = ($row['status'] !== 'completed' && $row['status'] !== 'skipped' && $row['activity_date'] < $now);
            $tasks[] = [
                'id' => (int) $row['id'],
                'activity_id' => $row['activity_id'],
                'roadmap_id' => $row['roadmap_id'],
                'milestone_id' => $row['milestone_id'],
                'title' => $row['activity_title'],
                'description' => $row['activity_description'],
                'type' => $row['activity_type'],
                'priority' => (int) $row['priority'],
                'status' => $row['status'],
                'date' => $row['activity_date'],
                'due' => $row['expected_complete_by'],
                'completed_at' => $row['actual_completed_at'],
                'ai_generated' => (bool) $row['ai_generated'],
                'recurring' => (bool) $row['recurring'],
                'recurrence' => $row['recurrence_pattern'],
                'overdue' => $is_overdue,
            ];
            $stats['total']++;
            if ($row['status'] === 'completed') $stats['completed']++;
            elseif ($row['status'] === 'in_progress') $stats['in_progress']++;
            elseif ($is_overdue) $stats['overdue']++;
            else $stats['pending']++;
        }
    }
    wp_send_json_success([
        'tasks' => $tasks,
        'stats' => $stats,
    ]);
});

/* ══════════════════════════════════════════════════════════
   TODAY'S TASKS — Full daily task list + End-of-Day Report
   + Vision velocity projection (faster / slower)
   ══════════════════════════════════════════════════════════ */

/**
 * AJAX: Get full today's task list with stats & velocity info
 */
add_action('wp_ajax_vmo_get_todays_tasks_full', 'vmo_get_todays_tasks_full');
function vmo_get_todays_tasks_full() {
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if (!vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Invalid nonce'], 403);
    }

    global $wpdb;
    $user_id = get_current_user_id();
    $today   = current_time('Y-m-d');

    $activities_table = $wpdb->prefix . 'vmo_daily_activities';
    $reports_table    = $wpdb->prefix . 'vmo_daily_reports';
    $roadmaps_table   = $wpdb->prefix . 'vmo_business_roadmaps';
    $progress_table   = $wpdb->prefix . 'vmo_progress_tracking';

    // Verify tables exist
    if (!vmo_table_exists($activities_table)) {
        wp_send_json_error(['message' => 'Activities system not initialized']);
    }

    // Get active roadmap
    $roadmap = $wpdb->get_row($wpdb->prepare(
        "SELECT roadmap_id, roadmap_start_date, vision_target_date, total_duration_days, current_day_number
         FROM {$roadmaps_table} WHERE user_id = %d AND is_active = 1 ORDER BY created_at DESC LIMIT 1",
        $user_id
    ));

    if (!$roadmap) {
        wp_send_json_success([
            'has_roadmap' => false,
            'message'     => 'Complete Phase 4 to generate your roadmap and daily tasks.'
        ]);
    }

    // Fetch today's activities
    $tasks = $wpdb->get_results($wpdb->prepare(
        "SELECT id, activity_id, activity_title, activity_description, activity_type,
                estimated_duration_minutes, priority, status, expected_complete_by,
                actual_completed_at, completion_notes
         FROM {$activities_table}
         WHERE user_id = %d AND activity_date = %s AND roadmap_id = %s
         ORDER BY priority ASC, id ASC",
        $user_id, $today, $roadmap->roadmap_id
    ), ARRAY_A);

    // Also get overdue tasks from previous days
    $overdue = $wpdb->get_results($wpdb->prepare(
        "SELECT id, activity_id, activity_title, activity_description, activity_type,
                estimated_duration_minutes, priority, status, activity_date, expected_complete_by
         FROM {$activities_table}
         WHERE user_id = %d AND activity_date < %s AND roadmap_id = %s
               AND status NOT IN ('completed','skipped')
         ORDER BY activity_date ASC, priority ASC
         LIMIT 20",
        $user_id, $today, $roadmap->roadmap_id
    ), ARRAY_A);

    // Stats
    $total_planned   = count($tasks);
    $completed_today = 0;
    $in_progress     = 0;
    $total_est_mins  = 0;
    $done_est_mins   = 0;
    foreach ($tasks as $t) {
        $total_est_mins += (int) ($t['estimated_duration_minutes'] ?? 60);
        if ($t['status'] === 'completed') {
            $completed_today++;
            $done_est_mins += (int) ($t['estimated_duration_minutes'] ?? 60);
        } elseif ($t['status'] === 'in_progress') {
            $in_progress++;
        }
    }

    // Compute velocity & projection
    $start_date        = $roadmap->roadmap_start_date;
    $total_days        = (int) $roadmap->total_duration_days;
    $vision_target     = $roadmap->vision_target_date;
    $current_day       = (int) $roadmap->current_day_number;

    // Historical completion rate (last 14 days)
    $history = $wpdb->get_results($wpdb->prepare(
        "SELECT report_date, planned_activities_count, completed_activities_count, estimated_completion_adjustment
         FROM {$reports_table}
         WHERE user_id = %d AND roadmap_id = %s
         ORDER BY report_date DESC LIMIT 14",
        $user_id, $roadmap->roadmap_id
    ), ARRAY_A);

    $avg_completion_rate = 1.0;
    $cumulative_adj      = 0;
    $streak              = 0;
    if (!empty($history)) {
        $total_planned_h = 0;
        $total_done_h    = 0;
        foreach ($history as $h) {
            $total_planned_h += (int) $h['planned_activities_count'];
            $total_done_h    += (int) $h['completed_activities_count'];
        }
        $avg_completion_rate = $total_planned_h > 0 ? round($total_done_h / $total_planned_h, 3) : 1.0;
        $cumulative_adj = (int) ($history[0]['estimated_completion_adjustment'] ?? 0);

        // Streak: count consecutive 100% days
        foreach ($history as $h) {
            if ((int) $h['completed_activities_count'] >= (int) $h['planned_activities_count'] && (int) $h['planned_activities_count'] > 0) {
                $streak++;
            } else {
                break;
            }
        }
    }

    // Check if today's report already exists
    $existing_report = $wpdb->get_row($wpdb->prepare(
        "SELECT id, day_score, submitted_at FROM {$reports_table}
         WHERE user_id = %d AND roadmap_id = %s AND report_date = %s LIMIT 1",
        $user_id, $roadmap->roadmap_id, $today
    ), ARRAY_A);

    wp_send_json_success([
        'has_roadmap'       => true,
        'date'              => $today,
        'roadmap_id'        => $roadmap->roadmap_id,
        'current_day'       => $current_day,
        'total_days'        => $total_days,
        'vision_target'     => $vision_target,
        'tasks'             => $tasks,
        'overdue'           => $overdue,
        'stats'             => [
            'total_planned'     => $total_planned,
            'completed'         => $completed_today,
            'in_progress'       => $in_progress,
            'remaining'         => $total_planned - $completed_today - $in_progress,
            'total_est_minutes' => $total_est_mins,
            'done_est_minutes'  => $done_est_mins,
            'overdue_count'     => count($overdue),
        ],
        'velocity'          => [
            'avg_completion_rate' => $avg_completion_rate,
            'cumulative_adj'     => $cumulative_adj,
            'streak'             => $streak,
        ],
        'report_submitted'  => !empty($existing_report['submitted_at']),
        'report_score'      => (int) ($existing_report['day_score'] ?? 0),
    ]);
}

/**
 * AJAX: Submit end-of-day report — contrasts planned vs completed,
 *       calculates vision velocity adjustment
 */
add_action('wp_ajax_vmo_submit_daily_report', 'vmo_submit_daily_report');
function vmo_submit_daily_report() {
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if (!vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Invalid nonce'], 403);
    }

    global $wpdb;
    $user_id    = get_current_user_id();
    $today      = current_time('Y-m-d');
    $roadmap_id = sanitize_text_field(wp_unslash($_POST['roadmap_id'] ?? ''));
    $user_notes = sanitize_textarea_field(wp_unslash($_POST['notes'] ?? ''));
    $blockers   = sanitize_textarea_field(wp_unslash($_POST['blockers'] ?? ''));
    $wins       = sanitize_textarea_field(wp_unslash($_POST['wins'] ?? ''));

    if (empty($roadmap_id)) {
        wp_send_json_error(['message' => 'Missing roadmap_id']);
    }

    $activities_table = $wpdb->prefix . 'vmo_daily_activities';
    $reports_table    = $wpdb->prefix . 'vmo_daily_reports';
    $roadmaps_table   = $wpdb->prefix . 'vmo_business_roadmaps';

    // Get roadmap
    $roadmap = $wpdb->get_row($wpdb->prepare(
        "SELECT roadmap_id, total_duration_days, current_day_number, vision_target_date, roadmap_start_date
         FROM {$roadmaps_table} WHERE roadmap_id = %s AND user_id = %d AND is_active = 1 LIMIT 1",
        $roadmap_id, $user_id
    ));
    if (!$roadmap) {
        wp_send_json_error(['message' => 'Roadmap not found']);
    }

    $current_day = (int) $roadmap->current_day_number;
    $total_days  = (int) $roadmap->total_duration_days;

    // Get today's tasks
    $tasks = $wpdb->get_results($wpdb->prepare(
        "SELECT activity_id, status, estimated_duration_minutes FROM {$activities_table}
         WHERE user_id = %d AND activity_date = %s AND roadmap_id = %s",
        $user_id, $today, $roadmap_id
    ), ARRAY_A);

    $planned   = count($tasks);
    $completed = 0;
    $completed_ids   = [];
    $uncompleted_ids = [];
    foreach ($tasks as $t) {
        if ($t['status'] === 'completed') {
            $completed++;
            $completed_ids[] = $t['activity_id'];
        } else {
            $uncompleted_ids[] = $t['activity_id'];
        }
    }

    // Calculate velocity adjustment
    // If completion > 100% (user did extra tasks), days saved = positive
    // If completion < 100%, days added = negative
    $completion_rate = $planned > 0 ? ($completed / $planned) : 1.0;
    $day_adjustment  = 0;
    if ($planned > 0) {
        // Each percentage point above/below 100% adjusts vision estimate
        // At 150%, saving ~0.5 day. At 50%, losing ~0.5 day.
        $delta = $completion_rate - 1.0;
        $day_adjustment = (int) round($delta * -1); // negative = days saved, positive = days added
    }

    // 100% completion = 100 score; scale linearly with extra bonus for streaks
    $day_score = min(100, (int) round($completion_rate * 100));

    // Get previous cumulative adjustment
    $prev_cum = (int) $wpdb->get_var($wpdb->prepare(
        "SELECT cumulative_adjustment FROM {$reports_table}
         WHERE user_id = %d AND roadmap_id = %s AND report_date < %s
         ORDER BY report_date DESC LIMIT 1",
        $user_id, $roadmap_id, $today
    ));
    $new_cumulative = $prev_cum + $day_adjustment;

    // Streak
    $prev_streak = (int) $wpdb->get_var($wpdb->prepare(
        "SELECT streak_count FROM {$reports_table}
         WHERE user_id = %d AND roadmap_id = %s AND report_date < %s
         ORDER BY report_date DESC LIMIT 1",
        $user_id, $roadmap_id, $today
    ));
    $new_streak = ($completion_rate >= 1.0 && $planned > 0) ? ($prev_streak + 1) : 0;

    // Bonus: 7-day streak saves an extra day
    if ($new_streak > 0 && $new_streak % 7 === 0) {
        $day_adjustment--;
        $new_cumulative--;
    }

    // Progress delta
    $progress_delta = $total_days > 0 ? round(($completed / max(1, $total_days)) * 100, 2) : 0;

    // Upsert report
    $report_id = 'dr_' . $roadmap_id . '_d' . $current_day;
    $existing = $wpdb->get_var($wpdb->prepare(
        "SELECT id FROM {$reports_table} WHERE report_id = %s LIMIT 1", $report_id
    ));

    $report_data = [
        'report_id'                    => $report_id,
        'roadmap_id'                   => $roadmap_id,
        'user_id'                      => $user_id,
        'day_number'                   => $current_day,
        'report_date'                  => $today,
        'planned_activities_count'     => $planned,
        'completed_activities_count'   => $completed,
        'completed_activity_ids'       => wp_json_encode($completed_ids),
        'uncompleted_activity_ids'     => wp_json_encode($uncompleted_ids),
        'user_notes'                   => $user_notes,
        'blockers'                     => $blockers,
        'wins'                         => $wins,
        'progress_delta'               => $progress_delta,
        'estimated_completion_adjustment' => $day_adjustment,
        'cumulative_adjustment'        => $new_cumulative,
        'day_score'                    => $day_score,
        'streak_count'                 => $new_streak,
        'submitted_at'                 => current_time('mysql'),
    ];

    if ($existing) {
        $wpdb->update($reports_table, $report_data, ['id' => $existing]);
    } else {
        $wpdb->insert($reports_table, $report_data);
    }

    // Auto-generate actions from uncompleted tasks (Reports → Actions pipeline)
    $actions_table = $wpdb->prefix . 'vmo_actions';
    if (vmo_table_exists($actions_table) && !empty($uncompleted_ids)) {
        foreach ($uncompleted_ids as $act_id) {
            $task = $wpdb->get_row($wpdb->prepare(
                "SELECT title, description FROM {$activities_table} WHERE activity_id = %s LIMIT 1",
                $act_id
            ));
            if ($task) {
                // Check if action already exists for this source
                $exists = $wpdb->get_var($wpdb->prepare(
                    "SELECT id FROM {$actions_table} WHERE user_id = %d AND source = 'strategic' AND source_id = %s AND status != 'completed' LIMIT 1",
                    $user_id, $act_id
                ));
                if (!$exists) {
                    $wpdb->insert($actions_table, [
                        'user_id'     => $user_id,
                        'title'       => wp_kses_post($task->title ?: $task->description ?: 'Uncompleted task'),
                        'description' => 'Carried over from Day ' . $current_day . ' — not completed',
                        'source'      => 'strategic',
                        'source_id'   => $act_id,
                        'priority'    => 'high',
                        'category'    => 'operations',
                        'status'      => 'pending',
                        'due_date'    => date('Y-m-d', strtotime('+1 day')),
                        'created_at'  => current_time('mysql'),
                        'updated_at'  => current_time('mysql'),
                    ]);
                }
            }
        }
    }

    // Log activity
    $log_table = $wpdb->prefix . 'vmo_activity_log';
    if (vmo_table_exists($log_table)) {
        $wpdb->insert($log_table, [
            'user_id'       => $user_id,
            'action_type'   => 'task_completed',
            'description'   => "Daily report submitted: {$completed}/{$planned} tasks done (Day {$current_day})",
            'source_system' => 'today',
            'related_id'    => $report_id,
            'metadata'      => wp_json_encode(['score' => $day_score, 'streak' => $new_streak, 'actions_created' => count($uncompleted_ids)]),
            'created_at'    => current_time('mysql'),
        ]);
    }

    // Update roadmap's vision_target_date based on adjustment
    if ($day_adjustment !== 0) {
        $new_target = date('Y-m-d', strtotime($roadmap->vision_target_date . " {$day_adjustment} days"));
        $wpdb->update($roadmaps_table, ['vision_target_date' => $new_target], [
            'roadmap_id' => $roadmap_id,
            'user_id'    => $user_id,
        ]);
    }

    // ── Acceleration: pull forward future tasks when ahead of schedule ──
    $accelerated = 0;
    if ($completion_rate >= 1.0 && $planned > 0) {
        $tomorrow = date('Y-m-d', strtotime($today . ' +1 day'));
        $pull_limit = max(1, (int) floor($planned * 0.3)); // pull up to 30% of today's load
        $future_tasks = $wpdb->get_results($wpdb->prepare(
            "SELECT activity_id FROM {$activities_table}
             WHERE user_id = %d AND roadmap_id = %s AND activity_date = %s AND status = 'pending'
             ORDER BY priority ASC LIMIT %d",
            $user_id, $roadmap_id, $tomorrow, $pull_limit
        ), ARRAY_A);
        foreach ($future_tasks as $ft) {
            $wpdb->update($activities_table, [
                'activity_date' => $today,
                'updated_at'    => current_time('mysql'),
            ], ['activity_id' => $ft['activity_id'], 'user_id' => $user_id]);
            $accelerated++;
        }
    }

    // Build human-friendly assessment
    $assessment = '';
    if ($completion_rate >= 1.0) {
        $assessment = 'Outstanding day! You completed all planned tasks';
        if ($new_streak >= 3) $assessment .= " ({$new_streak}-day streak!)";
        if ($day_adjustment < 0) $assessment .= '. You are ' . abs($day_adjustment) . ' day(s) closer to achieving your business vision.';
        else $assessment .= '.';
        if ($accelerated > 0) $assessment .= " {$accelerated} extra task(s) pulled forward — keep the momentum!";
    } elseif ($completion_rate >= 0.8) {
        $assessment = "Good progress — {$completed}/{$planned} tasks done ({$day_score}%). ";
        $assessment .= 'Keep pushing to maintain momentum.';
    } elseif ($completion_rate >= 0.5) {
        $assessment = "Moderate progress — {$completed}/{$planned} tasks done ({$day_score}%). ";
        if ($day_adjustment > 0) $assessment .= "This pushes your vision date out by {$day_adjustment} day(s). Try to catch up tomorrow.";
    } else {
        $assessment = "Low completion today — {$completed}/{$planned} tasks done ({$day_score}%). ";
        if ($day_adjustment > 0) $assessment .= "Your business vision is now {$day_adjustment} day(s) further away. Consider blocking dedicated time tomorrow.";
    }

    wp_send_json_success([
        'report_id'       => $report_id,
        'day_score'       => $day_score,
        'completion_rate'  => round($completion_rate * 100),
        'planned'         => $planned,
        'completed'       => $completed,
        'day_adjustment'  => $day_adjustment,
        'cumulative_adj'  => $new_cumulative,
        'streak'          => $new_streak,
        'assessment'      => $assessment,
        'new_vision_date' => $day_adjustment !== 0 ? $new_target : $roadmap->vision_target_date,
    ]);
}

/**
 * AJAX: Get historical daily reports for the user
 */
add_action('wp_ajax_vmo_get_daily_reports_history', 'vmo_get_daily_reports_history');
function vmo_get_daily_reports_history() {
    if (!current_user_can('read')) wp_send_json_error(['message' => 'Unauthorized'], 403);
    if (!vmo_verify_admin_security()) wp_send_json_error(['message' => 'Invalid nonce'], 403);

    global $wpdb;
    $user_id    = get_current_user_id();
    $roadmap_id = sanitize_text_field(wp_unslash($_POST['roadmap_id'] ?? ''));
    $limit      = min(30, max(1, (int) ($_POST['limit'] ?? 14)));

    if (empty($roadmap_id)) {
        wp_send_json_error(['message' => 'Missing roadmap_id']);
    }

    $reports_table = $wpdb->prefix . 'vmo_daily_reports';
    if (!vmo_table_exists($reports_table)) {
        wp_send_json_success(['reports' => []]);
    }

    $rows = $wpdb->get_results($wpdb->prepare(
        "SELECT report_date, day_number, planned_activities_count, completed_activities_count,
                day_score, streak_count, estimated_completion_adjustment, cumulative_adjustment,
                user_notes, wins, blockers, submitted_at
         FROM {$reports_table}
         WHERE user_id = %d AND roadmap_id = %s
         ORDER BY report_date DESC LIMIT %d",
        $user_id, $roadmap_id, $limit
    ), ARRAY_A);

    wp_send_json_success(['reports' => $rows ?: []]);
}

// SEO Analytics AJAX handlers
add_action('wp_ajax_vmo_get_seo_data', 'vmo_get_seo_data');
add_action('wp_ajax_vmo_get_seo_recommendations', 'vmo_get_seo_recommendations');

// Strategic Dashboard SEO wiring (integrations + approvals)
add_action('wp_ajax_vmo_get_seo_overview', 'vmo_get_seo_overview');
add_action('wp_ajax_vmo_get_seo_integrations', 'vmo_get_seo_integrations');
add_action('wp_ajax_vmo_save_seo_integrations', 'vmo_save_seo_integrations');
add_action('wp_ajax_vmo_set_seo_auto_execute', 'vmo_set_seo_auto_execute');
add_action('wp_ajax_vmo_refresh_seo_data', 'vmo_refresh_seo_data');
add_action('wp_ajax_vmo_get_seo_reports', 'vmo_get_seo_reports');
add_action('wp_ajax_vmo_regenerate_seo_recommendations', 'vmo_regenerate_seo_recommendations');
add_action('wp_ajax_vmo_update_seo_recommendation', 'vmo_update_seo_recommendation');
add_action('wp_ajax_vmo_approve_seo_recommendation', 'vmo_approve_seo_recommendation');
add_action('wp_ajax_vmo_execute_seo_recommendation', 'vmo_execute_seo_recommendation');
add_action('wp_ajax_vmo_seo_get_growth_modules', 'vmo_seo_get_growth_modules');
add_action('wp_ajax_vmo_seo_get_authority_tasks', 'vmo_seo_get_authority_tasks');
add_action('wp_ajax_vmo_seo_update_authority_task_status', 'vmo_seo_update_authority_task_status');
add_action('wp_ajax_vmo_seo_update_local_visibility_status', 'vmo_seo_update_local_visibility_status');
add_action('wp_ajax_vmo_seo_schedule_review_request', 'vmo_seo_schedule_review_request');
add_action('wp_ajax_vmo_seo_mark_review_received', 'vmo_seo_mark_review_received');
add_action('wp_ajax_vmo_seo_generate_review_response_draft', 'vmo_seo_generate_review_response_draft');
add_action('wp_ajax_vmo_seo_generate_llms_txt', 'vmo_seo_generate_llms_txt_ajax');
add_action('wp_ajax_vmo_seo_execute_ai_search_recommendation', 'vmo_seo_execute_ai_search_recommendation');
add_action('wp_ajax_vmo_gsc_save_credentials', 'vmo_gsc_save_credentials');
add_action('wp_ajax_vmo_gsc_start_oauth', 'vmo_gsc_start_oauth');
add_action('wp_ajax_vmo_gsc_handle_oauth_callback', 'vmo_gsc_handle_oauth_callback');
add_action('wp_ajax_nopriv_vmo_gsc_handle_oauth_callback', 'vmo_gsc_handle_oauth_callback');
add_action('wp_ajax_vmo_gsc_fetch_sites', 'vmo_gsc_fetch_sites');
add_action('init', 'vmo_seo_register_llms_txt_route');
add_action('template_redirect', 'vmo_seo_serve_llms_txt');
add_action('vmo_contact_created', 'vmo_seo_record_contact_attribution', 10, 3);
add_action('vmo_contact_updated', 'vmo_seo_record_contact_attribution', 10, 3);
add_action('vmo_checkout_completed', 'vmo_seo_record_checkout_attribution', 10, 2);
add_action('vmo_sms_received', 'vmo_seo_record_sms_attribution', 10, 4);

// Strategic Dashboard CRM wiring (contacts list + tags + import)
add_action('wp_ajax_vmo_get_contacts', 'vmo_get_contacts');
add_action('wp_ajax_vmo_list_tags', 'vmo_list_tags');
add_action('wp_ajax_vmo_save_crm_contact', 'vmo_save_crm_contact');
add_action('wp_ajax_vmo_mark_as_customer', 'vmo_mark_as_customer');
add_action('wp_ajax_vmo_get_crm_contact_details', 'vmo_get_crm_contact_details');
add_action('wp_ajax_vmo_import_contacts', 'vmo_import_contacts');
add_action('wp_ajax_vmo_resolve_contact_duplicate', 'vmo_resolve_contact_duplicate');

// Contact Communications History AJAX handlers
add_action('wp_ajax_vmo_get_contact_communications', 'vmo_get_contact_communications');
add_action('wp_ajax_vmo_get_communication_detail', 'vmo_get_communication_detail');
add_action('wp_ajax_vmo_generate_contact_recommendations', 'vmo_generate_contact_recommendations');

// Quarterly Business Insights AJAX handlers
add_action('wp_ajax_vmo_generate_quarterly_insights', 'vmo_generate_quarterly_insights');
add_action('wp_ajax_vmo_get_quarterly_history', 'vmo_get_quarterly_history');
add_action('wp_ajax_vmo_get_quarterly_report', 'vmo_get_quarterly_report');

// Strategic Dashboard Payments tax settings
add_action('wp_ajax_vmo_get_tax_settings', 'vmo_get_tax_settings');
add_action('wp_ajax_vmo_save_tax_settings', 'vmo_save_tax_settings');

// Email Marketing AJAX handlers
add_action('wp_ajax_vmo_get_email_metrics', 'vmo_get_email_metrics');
add_action('wp_ajax_vmo_get_email_campaigns', 'vmo_get_email_campaigns');
;

// Promotion Command Center AJAX handlers
add_action('wp_ajax_vmo_get_promotion_settings', 'vmo_get_promotion_settings');
add_action('wp_ajax_vmo_save_promotion_settings', 'vmo_save_promotion_settings');
add_action('wp_ajax_vmo_upload_promotion_asset', 'vmo_upload_promotion_asset');
add_action('wp_ajax_vmo_generate_promotion_strategy', 'vmo_generate_promotion_strategy');
add_action('wp_ajax_vmo_generate_promotion_posts', 'vmo_generate_promotion_posts');
add_action('wp_ajax_vmo_refine_promotion_posts', 'vmo_refine_promotion_posts');
add_action('wp_ajax_vmo_generate_promotion_engagement', 'vmo_generate_promotion_engagement');
add_action('wp_ajax_vmo_get_promotion_provider_capabilities', 'vmo_get_promotion_provider_capabilities');
add_action('wp_ajax_vmo_start_promotion_provider_connection', 'vmo_start_promotion_provider_connection');
add_action('wp_ajax_vmo_refresh_promotion_provider_auth', 'vmo_refresh_promotion_provider_auth');
add_action('wp_ajax_vmo_get_promotion_provider_accounts', 'vmo_get_promotion_provider_accounts');
add_action('wp_ajax_vmo_get_promotion_provider_ad_accounts', 'vmo_get_promotion_provider_ad_accounts');
add_action('wp_ajax_vmo_create_promotion_ad_draft', 'vmo_create_promotion_ad_draft');
add_action('wp_ajax_vmo_run_promotion_jobs', 'vmo_run_promotion_jobs');
add_action('wp_ajax_vmo_run_promotion_ad_jobs', 'vmo_run_promotion_ad_jobs');

// Email Credentials Management (User-Provided IMAP/SMTP)
add_action('wp_ajax_vmo_save_email_credentials', 'vmo_save_email_credentials');
add_action('wp_ajax_vmo_test_and_save_email_credentials', 'vmo_test_and_save_email_credentials');
add_action('wp_ajax_vmo_verify_email_credentials', 'vmo_verify_email_credentials');
add_action('wp_ajax_vmo_get_email_accounts', 'vmo_get_email_accounts');
add_action('wp_ajax_vmo_delete_email_account', 'vmo_delete_email_account');

// Bulk Email Provider Integration (Brevo, MailerLite)
add_action('wp_ajax_vmo_save_bulk_email_provider', 'vmo_save_bulk_email_provider');
add_action('wp_ajax_vmo_verify_bulk_email_provider', 'vmo_verify_bulk_email_provider');

// Payment/Revenue AJAX handlers
add_action('wp_ajax_vmo_get_payment_data', 'vmo_get_payment_data');
add_action('wp_ajax_vmo_get_revenue_analytics', 'vmo_get_revenue_analytics');

// CRM AJAX handlers
add_action('wp_ajax_vmo_get_crm_data', 'vmo_get_crm_data');
add_action('wp_ajax_vmo_get_leads_data', 'vmo_get_leads_data');

// AI Recommendations AJAX handlers
add_action('wp_ajax_vmo_get_ai_recommendations', 'vmo_get_ai_recommendations');
add_action('wp_ajax_vmo_approve_ai_action', 'vmo_approve_ai_action');
add_action('wp_ajax_vmo_get_industry_insights', 'vmo_get_industry_insights');

// Detailed Analytics AJAX handlers
add_action('wp_ajax_vmo_get_detailed_analytics', 'vmo_get_detailed_analytics');
add_action('wp_ajax_vmo_get_geographical_data', 'vmo_get_geographical_data');

// Shipping & Fulfillment AJAX handlers
add_action('wp_ajax_vmo_get_active_orders', 'vmo_get_active_orders');
add_action('wp_ajax_vmo_approve_shipment', 'vmo_approve_shipment');
add_action('wp_ajax_vmo_get_shipping_settings', 'vmo_get_shipping_settings');
add_action('wp_ajax_vmo_save_shipping_settings', 'vmo_save_shipping_settings');
add_action('wp_ajax_vmo_test_shipping_integration', 'vmo_test_shipping_integration');
add_action('wp_ajax_vmo_get_shipping_analytics', 'vmo_get_shipping_analytics');
add_action('wp_ajax_vmo_get_shipping_ai_recommendations', 'vmo_get_shipping_ai_recommendations');
add_action('wp_ajax_vmo_generate_shipping_recommendations', 'vmo_generate_shipping_recommendations');
add_action('wp_ajax_vmo_approve_shipping_recommendation', 'vmo_approve_shipping_recommendation');
add_action('wp_ajax_vmo_apply_all_shipping_optimizations', 'vmo_apply_all_shipping_optimizations');
// Shipping utility endpoints used by the dashboard UI
add_action('wp_ajax_vmo_test_carrier_connection', 'vmo_test_carrier_connection');
add_action('wp_ajax_vmo_get_order_details', 'vmo_get_order_details');
add_action('wp_ajax_vmo_bulk_approve_shipments', 'vmo_bulk_approve_shipments');
add_action('wp_ajax_vmo_get_carrier_config', 'vmo_get_carrier_config');
add_action('wp_ajax_vmo_save_carrier_config', 'vmo_save_carrier_config');
add_action('wp_ajax_vmo_save_fulfillment_config', 'vmo_save_fulfillment_config');

// Strategic Dashboard KPI endpoint (was referenced by the UI but not registered)
add_action('wp_ajax_vmo_get_dashboard_kpis', 'vmo_get_dashboard_kpis');
function vmo_get_dashboard_kpis() {
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }

    // Require a valid nonce; return JSON error (avoid admin-ajax 400 noise).
    if (!vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Invalid nonce', 'status' => 'invalid_nonce'], 403);
    }

    $period = isset($_POST['period']) ? sanitize_text_field(wp_unslash($_POST['period'])) : '30d';
    $user_id = get_current_user_id();
    global $wpdb;
    $dashboard_context = vmo_dashboard_get_generation_context($user_id);
    $setup_snapshot = vmo_dashboard_build_setup_snapshot($dashboard_context);
    $seed_kpis = $dashboard_context['seed_kpis'];

    // Determine date window from period
    $days = 30;
    if ($period === '7d') $days = 7;
    elseif ($period === '90d') $days = 90;
    elseif ($period === '365d' || $period === '1y') $days = 365;
    $date_from = date('Y-m-d H:i:s', strtotime("-{$days} days"));

    // ── Contacts / Customers ──
    $contacts_table = $wpdb->prefix . 'vmo_contacts';
    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
    $contacts_exist = ($wpdb->get_var("SHOW TABLES LIKE '{$contacts_table}'") === $contacts_table);
    $total_customers = 0;
    $new_customers = 0;
    $total_leads = 0;
    if ($contacts_exist) {
        $contact_columns = function_exists('vmo_crm_contact_columns') ? vmo_crm_contact_columns($contacts_table) : [];
        if (in_array('user_id', $contact_columns, true)) {
            $customer_conditions = [];
            if (in_array('lifecycle_stage', $contact_columns, true)) $customer_conditions[] = "lifecycle_stage = 'customer'";
            if (in_array('primary_category', $contact_columns, true)) $customer_conditions[] = "primary_category = 'customer'";
            if (!empty($customer_conditions)) {
                $customer_where = '(' . implode(' OR ', $customer_conditions) . ')';
                $total_customers = (int) $wpdb->get_var($wpdb->prepare(
                    "SELECT COUNT(*) FROM {$contacts_table} WHERE user_id = %d AND {$customer_where}", $user_id
                ));
                if (in_array('created_at', $contact_columns, true)) {
                    $new_customers = (int) $wpdb->get_var($wpdb->prepare(
                        "SELECT COUNT(*) FROM {$contacts_table} WHERE user_id = %d AND created_at >= %s AND {$customer_where}", $user_id, $date_from
                    ));
                }
            }

            $lead_conditions = [];
            if (in_array('lifecycle_stage', $contact_columns, true)) $lead_conditions[] = "(lifecycle_stage IS NULL OR lifecycle_stage <> 'customer')";
            if (in_array('primary_category', $contact_columns, true)) $lead_conditions[] = "(primary_category IS NULL OR primary_category NOT IN ('customer', 'website_visitor'))";
            $lead_where = !empty($lead_conditions) ? ' AND ' . implode(' AND ', $lead_conditions) : '';
            $total_leads = (int) $wpdb->get_var($wpdb->prepare(
                "SELECT COUNT(*) FROM {$contacts_table} WHERE user_id = %d{$lead_where}", $user_id
            ));
        }
    }

    // ── Call metrics ──
    $call_sources = array(
        array(
            'table' => $wpdb->prefix . 'vmo_call_history',
            'timestamp_column' => 'created_at',
            'duration_column' => 'duration',
            'label' => 'call_history',
        ),
        array(
            'table' => $wpdb->prefix . 'vmo_phone_calls',
            'timestamp_column' => 'created_at',
            'duration_column' => 'duration',
            'label' => 'phone_calls',
        ),
        array(
            'table' => $wpdb->prefix . 'vmo_business_calls',
            'timestamp_column' => 'call_start_time',
            'duration_column' => 'call_duration',
            'label' => 'business_calls',
        ),
    );
    $total_calls = 0;
    $avg_duration = 0;
    $call_metrics_source = 'none';
    foreach ($call_sources as $source) {
        $table_name = $source['table'];
        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
        if ($wpdb->get_var("SHOW TABLES LIKE '{$table_name}'") !== $table_name) {
            continue;
        }

        $count = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$table_name} WHERE user_id = %d AND {$source['timestamp_column']} >= %s",
            $user_id,
            $date_from
        ));
        $duration = (float) $wpdb->get_var($wpdb->prepare(
            "SELECT COALESCE(AVG({$source['duration_column']}), 0) FROM {$table_name} WHERE user_id = %d AND {$source['timestamp_column']} >= %s",
            $user_id,
            $date_from
        ));

        if ($count > $total_calls) {
            $total_calls = $count;
            $avg_duration = $duration;
            $call_metrics_source = $source['label'];
        }
    }

    // ── Revenue (from Stripe orders) ──
    $orders_table = $wpdb->prefix . 'vmo_store_orders';
    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
    $orders_exist = ($wpdb->get_var("SHOW TABLES LIKE '{$orders_table}'") === $orders_table);
    $total_revenue = 0;
    $avg_transaction = 0;
    if ($orders_exist) {
        $total_revenue_cents = (float) $wpdb->get_var($wpdb->prepare(
            "SELECT COALESCE(SUM(amount_total), 0) FROM {$orders_table} WHERE user_id = %d AND status = 'paid' AND created_at >= %s", $user_id, $date_from
        ));
        $avg_transaction_cents = (float) $wpdb->get_var($wpdb->prepare(
            "SELECT COALESCE(AVG(amount_total), 0) FROM {$orders_table} WHERE user_id = %d AND status = 'paid' AND created_at >= %s", $user_id, $date_from
        ));
        $total_revenue = round($total_revenue_cents / 100, 2);
        $avg_transaction = round($avg_transaction_cents / 100, 2);
    }

    // ── Roadmap / Tasks ──
    $tasks_table = $wpdb->prefix . 'vmo_daily_activities';
    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
    $tasks_exist = ($wpdb->get_var("SHOW TABLES LIKE '{$tasks_table}'") === $tasks_table);
    $total_tasks = 0;
    $tasks_completed = 0;
    $overdue_count = 0;
    if ($tasks_exist) {
        $total_tasks = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$tasks_table} WHERE user_id = %d", $user_id
        ));
        $tasks_completed = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$tasks_table} WHERE user_id = %d AND status = 'completed'", $user_id
        ));
        $overdue_count = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$tasks_table} WHERE user_id = %d AND status != 'completed' AND scheduled_date < %s", $user_id, current_time('Y-m-d')
        ));
    }
    $task_completion_rate = $total_tasks > 0 ? round(($tasks_completed / $total_tasks) * 100) : 0;

    // ── Marketing / Visitors ──
    $interactions_table = $wpdb->prefix . 'vmo_contact_interactions';
    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
    $interactions_exist = ($wpdb->get_var("SHOW TABLES LIKE '{$interactions_table}'") === $interactions_table);
    $total_visitors  = 0;
    $total_pageviews = 0;
    $bounce_rate     = 0;
    if ($interactions_exist) {
        $total_visitors = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(DISTINCT COALESCE(contact_id, id)) FROM {$interactions_table} WHERE user_id = %d AND interaction_type = 'website_visit' AND created_at >= %s",
            $user_id, $date_from
        ));
        $total_pageviews = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$interactions_table} WHERE user_id = %d AND interaction_type IN ('website_visit','page_view') AND created_at >= %s",
            $user_id, $date_from
        ));
        // Bounce = single-page sessions. Approximate: visitors with exactly 1 pageview / total visitors
        if ($total_visitors > 0) {
            $single_page_visitors = (int) $wpdb->get_var($wpdb->prepare(
                "SELECT COUNT(*) FROM (
                    SELECT COALESCE(contact_id, id) as vid, COUNT(*) as pv
                    FROM {$interactions_table}
                    WHERE user_id = %d AND interaction_type IN ('website_visit','page_view') AND created_at >= %s
                    GROUP BY vid HAVING pv = 1
                ) bounce_sub",
                $user_id, $date_from
            ));
            $bounce_rate = round(($single_page_visitors / max(1, $total_visitors)) * 100, 1);
        }
    }

    // ── Goals (from roadmaps) ──
    $roadmaps_table = $wpdb->prefix . 'vmo_business_roadmaps';
    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
    $roadmaps_exist = ($wpdb->get_var("SHOW TABLES LIKE '{$roadmaps_table}'") === $roadmaps_table);
    $total_goals  = 0;
    $goals_completed = 0;
    $goals_avg_progress = 0;
    $short_term_count = 0;
    $long_term_count  = 0;
    if ($roadmaps_exist) {
        $roadmap_rows = $wpdb->get_results($wpdb->prepare(
            "SELECT roadmap_data, status, total_milestones, vision_target_date, roadmap_start_date FROM {$roadmaps_table} WHERE user_id = %d AND is_active = 1",
            $user_id
        ));
        $total_goals = count($roadmap_rows);
        foreach ($roadmap_rows as $rm) {
            if ($rm->status === 'completed') $goals_completed++;
            // short-term: target within 90 days; long-term: beyond
            $target = $rm->vision_target_date ? strtotime($rm->vision_target_date) : 0;
            if ($target > 0) {
                $diff_days = ($target - time()) / 86400;
                if ($diff_days <= 90) { $short_term_count++; } else { $long_term_count++; }
            } else {
                $long_term_count++;
            }
        }
        // Average progress from progress_tracking
        $progress_table = $wpdb->prefix . 'vmo_progress_tracking';
        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
        if ($wpdb->get_var("SHOW TABLES LIKE '{$progress_table}'") === $progress_table) {
            $avg_prog = (float) $wpdb->get_var($wpdb->prepare(
                "SELECT AVG(progress_percentage) FROM {$progress_table} WHERE user_id = %d", $user_id
            ));
            $goals_avg_progress = round($avg_prog, 1);
        }
    }
    $goals_completion_rate = $total_goals > 0 ? round(($goals_completed / $total_goals) * 100) : 0;

    // ── Market intelligence ──
    $phase_outputs_table = $wpdb->prefix . 'vmo_phase_outputs';
    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
    $phase_outputs_exist = ($wpdb->get_var("SHOW TABLES LIKE '{$phase_outputs_table}'") === $phase_outputs_table);
    $total_scans        = 0;
    $active_competitors = 0;
    $total_threats      = 0;
    $total_opportunities = 0;
    if ($phase_outputs_exist) {
        $total_scans = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$phase_outputs_table} WHERE user_id = %d AND output_type = 'market_research' AND is_active = 1",
            $user_id
        ));
        // Parse latest market research for competitor/threat/opportunity counts
        $latest_market = $wpdb->get_var($wpdb->prepare(
            "SELECT output_json FROM {$phase_outputs_table} WHERE user_id = %d AND output_type = 'market_research' AND is_active = 1 ORDER BY created_at DESC LIMIT 1",
            $user_id
        ));
        if ($latest_market) {
            $market_data = json_decode($latest_market, true);
            if (is_array($market_data)) {
                $active_competitors  = count($market_data['competitors'] ?? []);
                $total_threats       = count($market_data['threats'] ?? []);
                $total_opportunities = count($market_data['opportunities'] ?? []);
            }
        }
    }

    $seed_total_leads = (int) vmo_dashboard_seed_metric_value($seed_kpis, 'total_leads', 0);
    if ($total_leads <= 0 && $seed_total_leads > 0) {
        $total_leads = $seed_total_leads;
    }

    $seed_total_calls = (int) vmo_dashboard_seed_metric_value($seed_kpis, 'phone_calls_total', 0);
    if ($total_calls <= 0 && $seed_total_calls > 0) {
        $total_calls = $seed_total_calls;
    }

    $seed_visitors = (int) vmo_dashboard_seed_metric_value($seed_kpis, 'website_visitors', 0);
    if ($total_visitors <= 0 && $seed_visitors > 0) {
        $total_visitors = $seed_visitors;
    }

    $seed_pageviews = (int) vmo_dashboard_seed_metric_value($seed_kpis, 'page_views', 0);
    if ($total_pageviews <= 0 && $seed_pageviews > 0) {
        $total_pageviews = $seed_pageviews;
    }

    if ($total_goals <= 0 && (int) ($setup_snapshot['setup']['total'] ?? 0) > 0) {
        $total_goals = (int) $setup_snapshot['setup']['total'];
        $goals_completed = (int) $setup_snapshot['setup']['completed'];
        $goals_avg_progress = (float) ($setup_snapshot['setup']['progress'] ?? 0);
        $short_term_count = (int) ((!empty($setup_snapshot['website']['draft_assembled']) || !empty($setup_snapshot['website']['draft_ready'])) ? 1 : 0);
        $long_term_count = (int) (($setup_snapshot['systems']['payments']['ready'] ?? false) || ($setup_snapshot['systems']['contact_management']['ready'] ?? false));
    }
    $goals_completion_rate = max((int) $goals_completion_rate, (int) ($setup_snapshot['setup']['progress'] ?? 0));

    if ($total_tasks <= 0 && !empty($setup_snapshot['tasks'])) {
        $total_tasks = count($setup_snapshot['tasks']);
        $overdue_count = max($overdue_count, (int) ($setup_snapshot['website']['qa_pending'] ?? 0));
    }
    $task_completion_rate = max((int) $task_completion_rate, min((int) ($setup_snapshot['setup']['progress'] ?? 0), 100));

    // ── Health Score ──
    $health = 50;
    if ($total_customers > 0) $health += 10;
    if ($total_calls > 0) $health += 5;
    if ($total_revenue > 0) $health += 15;
    if ($task_completion_rate > 50) $health += 10;
    if ($new_customers > 0) $health += 5;
    if ($total_visitors > 0) $health += 5;
    if ($total_goals > 0 && $goals_avg_progress > 25) $health += 5;
    if ($total_scans > 0) $health += 5;
    if ((int) ($setup_snapshot['setup']['progress'] ?? 0) > 0) {
        $health = max($health, min(100, 40 + (int) round(((int) $setup_snapshot['setup']['progress']) * 0.45)));
    }
    $health = min(100, $health);

    $conversion_rate = $total_leads > 0 ? round(($total_customers / max(1, $total_leads)) * 100, 1) : (float) vmo_dashboard_seed_metric_value($seed_kpis, 'conversion_rate', 0);
    $setup_progress_value = (int) ($setup_snapshot['setup']['progress'] ?? 0);
    $next_milestone = (string) ($setup_snapshot['setup']['next_milestone'] ?? 'Monitor live KPIs and manage operations');

    $kpis = [
        'overall' => [
            'health_score' => $health,
            'status' => $health >= 70 ? 'good' : ($health >= 40 ? 'fair' : 'needs_attention'),
        ],
        'revenue' => [
            'total_revenue' => round($total_revenue, 2),
            'net_revenue' => round($total_revenue * 0.971, 2), // ~2.9% Stripe fees
            'processor_fees' => round($total_revenue * 0.029, 2),
            'growth_rate' => 0,
            'avg_transaction_value' => round($avg_transaction, 2),
        ],
        'customers' => [
            'total_customers' => $total_customers,
            'new_customers' => $new_customers,
            'avg_lifetime_value' => $total_customers > 0 ? round($total_revenue / $total_customers, 2) : 0,
            'acquisition_rate' => $total_customers > 0 ? round(($new_customers / max(1, $total_customers)) * 100) : 0,
        ],
        'calls' => [
            'total_calls' => $total_calls,
            'avg_duration' => round($avg_duration),
            'source' => $call_metrics_source,
            'avg_sentiment_score' => 0,
            'avg_conversion_probability' => 0,
        ],
        'marketing' => [
            'total_visitors' => $total_visitors,
            'total_pageviews' => $total_pageviews,
            'bounce_rate' => $bounce_rate,
            'total_leads' => $total_leads,
            'conversion_rate' => $conversion_rate,
        ],
        'goals' => [
            'total_goals' => $total_goals,
            'completion_rate' => $goals_completion_rate,
            'avg_progress' => $goals_avg_progress,
            'short_term_count' => $short_term_count,
            'long_term_count' => $long_term_count,
        ],
        'tasks' => [
            'total_tasks' => $total_tasks,
            'completion_rate' => $task_completion_rate,
            'overdue_count' => $overdue_count,
            'immediate_count' => count($setup_snapshot['tasks'] ?? array()),
        ],
        'market' => [
            'total_scans' => $total_scans,
            'active_competitors' => $active_competitors,
            'total_threats' => $total_threats,
            'total_opportunities' => $total_opportunities,
        ],
        'website' => [
            'status' => (string) ($setup_snapshot['website']['status'] ?? 'pending'),
            'pages_count' => (int) ($setup_snapshot['website']['pages_count'] ?? 0),
            'publish_ready' => !empty($setup_snapshot['website']['publish_ready']) ? 1 : 0,
            'qa_pending_count' => (int) ($setup_snapshot['website']['qa_pending'] ?? 0),
        ],
        'setup' => [
            'setup_progress' => $setup_progress_value,
            'next_milestone' => $next_milestone,
            'payment_system_ready' => !empty($setup_snapshot['systems']['payments']['ready']) ? 1 : 0,
            'phone_system_ready' => !empty($setup_snapshot['systems']['phone']['ready']) ? 1 : 0,
            'email_marketing_ready' => !empty($setup_snapshot['systems']['email_marketing']['ready']) ? 1 : 0,
            'seo_system_ready' => !empty($setup_snapshot['systems']['seo']['ready']) ? 1 : 0,
            'contact_management_ready' => !empty($setup_snapshot['systems']['contact_management']['ready']) ? 1 : 0,
        ],
        'business' => [
            'business_name' => (string) ($setup_snapshot['business']['name'] ?? ''),
            'business_type' => (string) ($setup_snapshot['business']['type'] ?? ''),
            'industry' => (string) ($setup_snapshot['business']['industry'] ?? ''),
        ],
        'recommendations' => [
            'next_milestone' => $next_milestone,
            'active_modules' => (int) ($setup_snapshot['modules']['active'] ?? 0),
            'in_progress_modules' => (int) ($setup_snapshot['modules']['in_progress'] ?? 0),
            'critical_alerts' => max((int) $overdue_count, (int) ($setup_snapshot['website']['qa_pending'] ?? 0)),
        ],
    ];

    wp_send_json_success([
        'period' => $period,
        'kpis' => $kpis,
        'dashboard_modules' => $dashboard_context['dashboard_modules'],
        'kpi_model' => $dashboard_context['kpi_model'],
        'last_update' => (string) ($dashboard_context['operating_state']['generated_at'] ?? current_time('mysql')),
    ]);
}

// SEO task list endpoint (used by Strategic Dashboard loadTasks())
add_action('wp_ajax_vmo_list_seo_tasks', 'vmo_list_seo_tasks');
function vmo_list_seo_tasks() {
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if (!vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Invalid nonce', 'status' => 'invalid_nonce'], 403);
    }

    global $wpdb;
    $user_id = get_current_user_id();
    $table = $wpdb->prefix . 'vmo_approval_queue';
    $has_table = vmo_table_exists($table);
    if (!$has_table) {
        wp_send_json_success(['tasks' => []]);
    }

    $rows = $wpdb->get_results(
        $wpdb->prepare(
            "SELECT id, title, status, created_at, updated_at FROM {$table} WHERE user_id=%d AND type='seo' ORDER BY updated_at DESC LIMIT 50",
            $user_id
        ),
        ARRAY_A
    );
    wp_send_json_success([
        'tasks' => is_array($rows) ? $rows : [],
    ]);
}

if (!defined('VMO_TOKENS_SEO_RECOMMENDATION_REGEN')) define('VMO_TOKENS_SEO_RECOMMENDATION_REGEN', 900);
if (!defined('VMO_TOKENS_SEO_CONTENT_GENERATION')) define('VMO_TOKENS_SEO_CONTENT_GENERATION', 1800);
if (!defined('VMO_TOKENS_SEO_AUTHORITY_SCAN')) define('VMO_TOKENS_SEO_AUTHORITY_SCAN', 700);
if (!defined('VMO_TOKENS_SEO_AI_SEARCH_AUDIT')) define('VMO_TOKENS_SEO_AI_SEARCH_AUDIT', 800);
if (!defined('VMO_TOKENS_SEO_TRANSCRIPT_MINING')) define('VMO_TOKENS_SEO_TRANSCRIPT_MINING', 900);
if (!defined('VMO_TOKENS_SEO_COMPETITOR_GAP_SCAN')) define('VMO_TOKENS_SEO_COMPETITOR_GAP_SCAN', 1200);
if (!defined('VMO_TOKENS_SEO_REVIEW_MESSAGE')) define('VMO_TOKENS_SEO_REVIEW_MESSAGE', 250);
if (!defined('VMO_TOKENS_SEO_AI_QUALITY_REVIEW')) define('VMO_TOKENS_SEO_AI_QUALITY_REVIEW', 700);

function vmo_seo_ajax_guard() {
    if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['nonce'] ?? $_REQUEST['security'] ?? '')), 'vmo_nonce')) {
        wp_send_json_error(['message' => 'Security check failed'], 403);
    }
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if (!vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Invalid nonce', 'status' => 'invalid_nonce'], 403);
    }
}

function vmo_seo_charge_tokens($user_id, $constant_name, $operation, $description) {
    // Do not deduct for fallback/template output.
    $cost = defined($constant_name) ? (int) constant($constant_name) : 0;
    if ($cost <= 0 || !function_exists('vmo_deduct_tokens')) return true;
    return (bool) vmo_deduct_tokens($user_id, $cost, $operation, $description);
}

function vmo_seo_has_token_balance($user_id, $constant_name) {
    $cost = defined($constant_name) ? (int) constant($constant_name) : 0;
    if ($cost <= 0) return true;
    $balance = (int) get_user_meta($user_id, 'vmo_token_balance', true);
    return $balance >= $cost;
}

function vmo_seo_table_columns($table) {
    global $wpdb;
    if (!vmo_table_exists($table)) return [];
    $columns = $wpdb->get_col("SHOW COLUMNS FROM {$table}", 0);
    return is_array($columns) ? array_map('strval', $columns) : [];
}

function vmo_seo_insert_table_row($table, array $row) {
    global $wpdb;
    if (!vmo_table_exists($table)) return false;
    $columns = vmo_seo_table_columns($table);
    if (!empty($columns)) $row = array_intersect_key($row, array_flip($columns));
    return !empty($row) ? (bool) $wpdb->insert($table, $row) : false;
}

function vmo_seo_ensure_growth_tables() {
    global $wpdb;
    require_once ABSPATH . 'wp-admin/includes/upgrade.php';
    $charset_collate = $wpdb->get_charset_collate();
    $authority_table = $wpdb->prefix . 'vmo_seo_authority_tasks';
    $attribution_table = $wpdb->prefix . 'vmo_attribution_events';
    dbDelta("CREATE TABLE {$authority_table} (
        id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
        user_id BIGINT UNSIGNED NOT NULL,
        title VARCHAR(255) NOT NULL,
        type VARCHAR(60) DEFAULT 'citation',
        suggested_link TEXT NULL,
        why_it_matters TEXT NULL,
        steps_json LONGTEXT NULL,
        impact VARCHAR(20) DEFAULT 'medium',
        difficulty VARCHAR(20) DEFAULT 'moderate',
        estimated_time VARCHAR(80) DEFAULT NULL,
        status VARCHAR(30) DEFAULT 'not_started',
        source VARCHAR(80) DEFAULT 'vmo_seo',
        source_type VARCHAR(80) DEFAULT 'fallback_search_url',
        confidence VARCHAR(20) DEFAULT 'medium',
        related_competitor VARCHAR(255) DEFAULT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        completed_at DATETIME DEFAULT NULL,
        PRIMARY KEY  (id),
        KEY idx_user_status (user_id, status),
        KEY idx_type (type)
    ) {$charset_collate};");
    dbDelta("CREATE TABLE {$attribution_table} (
        id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
        user_id BIGINT UNSIGNED NOT NULL,
        visitor_id VARCHAR(120) DEFAULT NULL,
        contact_id VARCHAR(120) DEFAULT NULL,
        session_id VARCHAR(120) DEFAULT NULL,
        event_type VARCHAR(80) NOT NULL,
        event_subtype VARCHAR(120) DEFAULT NULL,
        source VARCHAR(80) DEFAULT NULL,
        medium VARCHAR(80) DEFAULT NULL,
        campaign VARCHAR(160) DEFAULT NULL,
        term VARCHAR(255) DEFAULT NULL,
        content VARCHAR(255) DEFAULT NULL,
        landing_page TEXT NULL,
        referrer TEXT NULL,
        gclid VARCHAR(255) DEFAULT NULL,
        utm_json LONGTEXT NULL,
        seo_keyword VARCHAR(255) DEFAULT NULL,
        seo_page_id BIGINT UNSIGNED DEFAULT NULL,
        related_call_id BIGINT UNSIGNED DEFAULT NULL,
        related_form_id BIGINT UNSIGNED DEFAULT NULL,
        related_order_id BIGINT UNSIGNED DEFAULT NULL,
        related_appointment_id BIGINT UNSIGNED DEFAULT NULL,
        conversion_value DECIMAL(12,2) DEFAULT 0,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY  (id),
        KEY idx_user_created (user_id, created_at),
        KEY idx_source_medium (source, medium),
        KEY idx_event_type (event_type)
    ) {$charset_collate};");
    $contacts_table = $wpdb->prefix . 'vmo_contacts';
    if (vmo_table_exists($contacts_table)) {
        $contact_columns = vmo_seo_table_columns($contacts_table);
        $touch_columns = [
            'first_touch_source' => "ALTER TABLE {$contacts_table} ADD first_touch_source VARCHAR(80) DEFAULT NULL",
            'first_touch_medium' => "ALTER TABLE {$contacts_table} ADD first_touch_medium VARCHAR(80) DEFAULT NULL",
            'first_touch_landing_page' => "ALTER TABLE {$contacts_table} ADD first_touch_landing_page TEXT NULL",
            'first_touch_at' => "ALTER TABLE {$contacts_table} ADD first_touch_at DATETIME DEFAULT NULL",
            'last_touch_source' => "ALTER TABLE {$contacts_table} ADD last_touch_source VARCHAR(80) DEFAULT NULL",
            'last_touch_medium' => "ALTER TABLE {$contacts_table} ADD last_touch_medium VARCHAR(80) DEFAULT NULL",
            'last_touch_landing_page' => "ALTER TABLE {$contacts_table} ADD last_touch_landing_page TEXT NULL",
            'last_touch_at' => "ALTER TABLE {$contacts_table} ADD last_touch_at DATETIME DEFAULT NULL",
        ];
        foreach ($touch_columns as $column => $sql) {
            if (!in_array($column, $contact_columns, true)) $wpdb->query($sql);
        }
    }
}

function vmo_seo_profile_value(array $profile, array $keys, $default = '') {
    foreach ($keys as $key) {
        if (!empty($profile[$key])) return is_array($profile[$key]) ? implode(', ', array_filter(array_map('strval', $profile[$key]))) : (string) $profile[$key];
    }
    return $default;
}

function vmo_seo_business_nap($user_id, array $profile = []) {
    if (empty($profile) && function_exists('vmo_get_consultation_profile')) $profile = vmo_get_consultation_profile($user_id);
    return [
        'business_name' => sanitize_text_field(vmo_seo_profile_value($profile, ['business_name', 'company_name'], get_bloginfo('name'))),
        'address' => sanitize_text_field(vmo_seo_profile_value($profile, ['address', 'business_address', 'location'], '')),
        'phone' => sanitize_text_field(vmo_seo_profile_value($profile, ['phone', 'business_phone', 'phone_number'], '')),
        'website' => esc_url_raw(home_url('/')),
        'description' => sanitize_textarea_field(vmo_seo_profile_value($profile, ['business_description', 'description', 'summary'], '')),
    ];
}

function vmo_seo_create_action_task($user_id, $title, $description, $category = 'marketing', $priority = 'normal', array $meta = []) {
    global $wpdb;
    $table = $wpdb->prefix . 'vmo_actions';
    return vmo_seo_insert_table_row($table, [
        'user_id' => $user_id,
        'title' => sanitize_text_field($title),
        'description' => wp_json_encode(array_merge(['description' => $description], $meta)),
        'source' => 'seo',
        'source_id' => isset($meta['source_id']) ? (int) $meta['source_id'] : 0,
        'priority' => in_array($priority, ['urgent', 'high', 'normal', 'low'], true) ? $priority : 'normal',
        'category' => sanitize_key($category),
        'status' => 'pending',
        'due_date' => date('Y-m-d', strtotime('+7 days')),
        'created_at' => current_time('mysql'),
        'updated_at' => current_time('mysql'),
    ]);
}

function vmo_seo_discover_url_from_search($query, array $blocked_hosts = []) {
    $query = trim((string) $query);
    if ($query === '') return '';
    $blocked_hosts = array_merge(['google.com', 'bing.com', 'duckduckgo.com', 'facebook.com', 'instagram.com', 'x.com', 'twitter.com'], $blocked_hosts);
    $response = wp_remote_get('https://duckduckgo.com/html/?q=' . rawurlencode($query), ['timeout' => 8, 'user-agent' => 'VMO SEO Opportunity Discovery/1.0; ' . home_url('/')]);
    if (is_wp_error($response)) return '';
    $html = (string) wp_remote_retrieve_body($response);
    if ($html === '') return '';
    preg_match_all('/<a[^>]+href=["\']([^"\']+)["\'][^>]*>/i', $html, $matches);
    foreach ((array) ($matches[1] ?? []) as $url) {
        $url = html_entity_decode((string) $url, ENT_QUOTES, 'UTF-8');
        if (strpos($url, 'uddg=') !== false) {
            $parts = wp_parse_url($url);
            parse_str($parts['query'] ?? '', $query_args);
            if (!empty($query_args['uddg'])) $url = rawurldecode((string) $query_args['uddg']);
        }
        $url = esc_url_raw($url);
        if (!preg_match('/^https?:\/\//i', $url)) continue;
        $host = strtolower((string) wp_parse_url($url, PHP_URL_HOST));
        if ($host === '') continue;
        $blocked = false;
        foreach ($blocked_hosts as $blocked_host) {
            if ($blocked_host !== '' && strpos($host, strtolower($blocked_host)) !== false) { $blocked = true; break; }
        }
        if (!$blocked) return $url;
    }
    return '';
}

function vmo_seo_discover_specific_authority_opportunities($user_id, $profile) {
    $cached = get_user_meta($user_id, 'vmo_seo_last_authority_discovery', true);
    if (is_array($cached) && !empty($cached['generated_at']) && strtotime((string) $cached['generated_at']) > strtotime('-7 days')) return (array) ($cached['items'] ?? []);
    if (!vmo_seo_has_token_balance($user_id, 'VMO_TOKENS_SEO_AUTHORITY_SCAN')) return [];
    $profile = is_array($profile) ? $profile : [];
    $business_name = vmo_seo_profile_value($profile, ['business_name', 'company_name'], get_bloginfo('name'));
    $location = vmo_seo_profile_value($profile, ['city', 'location', 'service_area'], 'local area');
    $industry = vmo_seo_profile_value($profile, ['industry', 'business_type'], 'business');
    $searches = [
        'citation' => ['title' => 'Join ' . $location . ' Chamber of Commerce', 'query' => $location . ' chamber of commerce join membership', 'fallback' => 'https://www.google.com/search?q=' . rawurlencode($location . ' chamber of commerce join')],
        'directory' => ['title' => 'Claim a local business directory listing', 'query' => $location . ' business directory submit listing ' . $industry, 'fallback' => 'https://www.google.com/search?q=' . rawurlencode($location . ' business directory ' . $industry)],
        'PR' => ['title' => 'Pitch a local publication feature', 'query' => $location . ' local newspaper submit business news', 'fallback' => 'https://www.google.com/search?q=' . rawurlencode($location . ' local newspaper business feature')],
        'podcast' => ['title' => 'Pitch a podcast/interview opportunity for ' . $business_name, 'query' => $location . ' ' . $industry . ' podcast interview guest', 'fallback' => 'https://www.google.com/search?q=' . rawurlencode($location . ' podcast ' . $industry . ' interview')],
        'backlink' => ['title' => 'Create an industry association profile', 'query' => $industry . ' professional association member directory ' . $location, 'fallback' => 'https://www.google.com/search?q=' . rawurlencode($industry . ' professional association directory ' . $location)],
        'partnership' => ['title' => 'Identify partner backlink opportunities', 'query' => $location . ' ' . $industry . ' partners vendors suppliers directory', 'fallback' => 'https://www.google.com/search?q=' . rawurlencode($location . ' ' . $industry . ' partners vendors suppliers')],
        'review_platform' => ['title' => 'Configure relevant review platform profiles', 'query' => $industry . ' review platform profile ' . $location, 'fallback' => 'https://www.google.com/search?q=' . rawurlencode($industry . ' review platforms ' . $location)],
    ];
    $items = [];
    foreach ($searches as $type => $search) {
        $url = vmo_seo_discover_url_from_search($search['query']);
        $specific = $url !== '';
        $items[] = [
            'title' => $search['title'],
            'type' => $type,
            'suggested_link' => $specific ? $url : $search['fallback'],
            'source_type' => $specific ? 'specific_discovered_url' : 'fallback_search_url',
            'confidence' => $specific ? 'high' : 'medium',
            'why_it_matters' => 'This off-site opportunity can strengthen local trust, entity authority, referral traffic, and search visibility.',
            'impact' => in_array($type, ['citation', 'PR', 'review_platform'], true) ? 'high' : 'medium',
            'difficulty' => $specific ? 'moderate' : 'research_needed',
            'estimated_time' => $specific ? '15-45 minutes' : '30-60 minutes',
        ];
    }
    update_user_meta($user_id, 'vmo_seo_last_authority_discovery', ['generated_at' => current_time('mysql'), 'items' => $items]);
    if (count(array_filter($items, static fn($item) => ($item['source_type'] ?? '') === 'specific_discovered_url')) > 0) {
        vmo_seo_charge_tokens($user_id, 'VMO_TOKENS_SEO_AUTHORITY_SCAN', 'seo_authority', 'Discovered specific authority growth opportunities');
    }
    return $items;
}

function vmo_seo_generate_authority_recommendations($user_id, $profile, $competitor_data = []) {
    $profile = is_array($profile) ? $profile : [];
    $business_name = vmo_seo_profile_value($profile, ['business_name', 'company_name'], get_bloginfo('name'));
    $location = vmo_seo_profile_value($profile, ['city', 'location', 'service_area'], 'local area');
    $industry = vmo_seo_profile_value($profile, ['industry', 'business_type'], 'business');
    $encoded_location = rawurlencode($location . ' ' . $industry);
    $base_steps = [
        'Open the suggested external page or search result.',
        'Create or claim the business profile using the exact VMO business name, address, phone, website, and description.',
        'Upload the logo, service/product images, and business photos when available.',
        'Add the website URL and service/category details.',
        'Return to VMO and mark the task complete once submitted or published.'
    ];
    $items = vmo_seo_discover_specific_authority_opportunities($user_id, $profile);
    if (empty($items)) $items = [
        ['title' => 'Join the ' . $location . ' Chamber of Commerce', 'type' => 'citation', 'suggested_link' => 'https://www.google.com/search?q=' . rawurlencode($location . ' chamber of commerce join'), 'why_it_matters' => 'Local chambers can create trusted local citation and authority signals.', 'impact' => 'high', 'difficulty' => 'moderate', 'estimated_time' => '15-30 minutes'],
        ['title' => 'Claim top local business directory listings', 'type' => 'directory', 'suggested_link' => 'https://www.google.com/search?q=' . rawurlencode($location . ' business directory ' . $industry), 'why_it_matters' => 'Consistent citations reinforce local relevance and trust.', 'impact' => 'medium', 'difficulty' => 'easy', 'estimated_time' => '30-45 minutes'],
        ['title' => 'Pitch a local newspaper or community feature', 'type' => 'PR', 'suggested_link' => 'https://www.google.com/search?q=' . rawurlencode($location . ' local newspaper business feature'), 'why_it_matters' => 'Local press mentions can increase brand authority and referral traffic.', 'impact' => 'high', 'difficulty' => 'hard', 'estimated_time' => '45-60 minutes'],
        ['title' => 'Find podcast or interview opportunities for ' . $business_name, 'type' => 'podcast', 'suggested_link' => 'https://www.google.com/search?q=' . rawurlencode($location . ' podcast ' . $industry . ' interview'), 'why_it_matters' => 'Interviews build entity authority, backlinks, and reusable content.', 'impact' => 'medium', 'difficulty' => 'moderate', 'estimated_time' => '30-60 minutes'],
        ['title' => 'Create professional association profiles', 'type' => 'backlink', 'suggested_link' => 'https://www.google.com/search?q=' . rawurlencode($industry . ' professional association directory ' . $location), 'why_it_matters' => 'Industry directories can validate expertise and improve topical authority.', 'impact' => 'medium', 'difficulty' => 'moderate', 'estimated_time' => '30-45 minutes'],
        ['title' => 'Identify partner backlink opportunities', 'type' => 'partnership', 'suggested_link' => 'https://www.google.com/search?q=' . $encoded_location . rawurlencode(' partners vendors suppliers'), 'why_it_matters' => 'Supplier, partner, and vendor pages often provide relevant white-hat backlink opportunities.', 'impact' => 'medium', 'difficulty' => 'moderate', 'estimated_time' => '30-60 minutes'],
        ['title' => 'Prepare a local press release opportunity', 'type' => 'PR', 'suggested_link' => 'https://www.google.com/search?q=' . rawurlencode($location . ' press release distribution local business'), 'why_it_matters' => 'Press release placement can support launches, new services, and credibility.', 'impact' => 'medium', 'difficulty' => 'moderate', 'estimated_time' => '45-60 minutes'],
        ['title' => 'Configure relevant review platform profiles', 'type' => 'review_platform', 'suggested_link' => 'https://www.google.com/search?q=' . rawurlencode($industry . ' review platforms ' . $location), 'why_it_matters' => 'Review platforms support local trust, conversion, and reputation signals.', 'impact' => 'high', 'difficulty' => 'easy', 'estimated_time' => '15-30 minutes'],
    ];
    foreach ($items as &$item) {
        $item['requires_user_action'] = true;
        $item['external_execution'] = true;
        $item['steps'] = $base_steps;
        $item['status'] = 'not_started';
        $item['source'] = 'vmo_authority_growth';
        $item['source_type'] = $item['source_type'] ?? 'fallback_search_url';
        $item['confidence'] = $item['confidence'] ?? 'medium';
        $item['related_competitor'] = '';
    }
    return $items;
}

function vmo_seo_save_authority_tasks($user_id, $items) {
    global $wpdb;
    vmo_seo_ensure_growth_tables();
    $table = $wpdb->prefix . 'vmo_seo_authority_tasks';
    foreach ((array) $items as $item) {
        $title = sanitize_text_field((string) ($item['title'] ?? 'Authority task'));
        $type = sanitize_key((string) ($item['type'] ?? 'citation'));
        $exists = $wpdb->get_var($wpdb->prepare("SELECT id FROM {$table} WHERE user_id=%d AND title=%s AND type=%s LIMIT 1", $user_id, $title, $type));
        if ($exists) continue;
        $wpdb->insert($table, [
            'user_id' => $user_id,
            'title' => $title,
            'type' => $type,
            'suggested_link' => esc_url_raw((string) ($item['suggested_link'] ?? '')),
            'why_it_matters' => sanitize_textarea_field((string) ($item['why_it_matters'] ?? '')),
            'steps_json' => wp_json_encode((array) ($item['steps'] ?? [])),
            'impact' => sanitize_key((string) ($item['estimated_impact'] ?? $item['impact'] ?? 'medium')),
            'difficulty' => sanitize_key((string) ($item['difficulty'] ?? 'moderate')),
            'estimated_time' => sanitize_text_field((string) ($item['estimated_time'] ?? '')),
            'status' => sanitize_key((string) ($item['status'] ?? 'not_started')),
            'source' => sanitize_key((string) ($item['source'] ?? 'vmo_authority_growth')),
            'source_type' => sanitize_key((string) ($item['source_type'] ?? 'fallback_search_url')),
            'confidence' => sanitize_key((string) ($item['confidence'] ?? 'medium')),
            'related_competitor' => sanitize_text_field((string) ($item['related_competitor'] ?? '')),
            'created_at' => current_time('mysql'),
            'updated_at' => current_time('mysql'),
        ]);
    }
}

function vmo_seo_get_authority_task_rows($user_id) {
    global $wpdb;
    vmo_seo_ensure_growth_tables();
    $table = $wpdb->prefix . 'vmo_seo_authority_tasks';
    $rows = $wpdb->get_results($wpdb->prepare("SELECT * FROM {$table} WHERE user_id=%d ORDER BY FIELD(status,'not_started','in_progress','completed','skipped'), impact DESC, created_at DESC LIMIT 50", $user_id), ARRAY_A);
    if (empty($rows)) {
        $profile = function_exists('vmo_get_consultation_profile') ? vmo_get_consultation_profile($user_id) : [];
        vmo_seo_save_authority_tasks($user_id, vmo_seo_generate_authority_recommendations($user_id, $profile));
        $rows = $wpdb->get_results($wpdb->prepare("SELECT * FROM {$table} WHERE user_id=%d ORDER BY created_at DESC LIMIT 50", $user_id), ARRAY_A);
    }
    foreach ($rows as &$row) {
        $row['steps'] = json_decode((string) ($row['steps_json'] ?? '[]'), true) ?: [];
        $row['requires_user_action'] = true;
        $row['external_execution'] = true;
        $row['source_type'] = $row['source_type'] ?? 'fallback_search_url';
        $row['confidence'] = $row['confidence'] ?? 'medium';
    }
    return is_array($rows) ? $rows : [];
}

function vmo_seo_get_authority_tasks() {
    vmo_seo_ajax_guard();
    wp_send_json_success(['items' => vmo_seo_get_authority_task_rows(get_current_user_id()), 'nap' => vmo_seo_business_nap(get_current_user_id())]);
}

function vmo_seo_update_authority_task_status() {
    vmo_seo_ajax_guard();
    global $wpdb;
    vmo_seo_ensure_growth_tables();
    $user_id = get_current_user_id();
    $id = absint($_POST['id'] ?? 0);
    $status = sanitize_key((string) ($_POST['status'] ?? ''));
    if ($id <= 0 || !in_array($status, ['not_started', 'in_progress', 'completed', 'skipped'], true)) wp_send_json_error(['message' => 'Invalid task status']);
    $table = $wpdb->prefix . 'vmo_seo_authority_tasks';
    $wpdb->update($table, ['status' => $status, 'updated_at' => current_time('mysql'), 'completed_at' => $status === 'completed' ? current_time('mysql') : null], ['id' => $id, 'user_id' => $user_id]);
    if ($status === 'completed') vmo_seo_create_action_task($user_id, 'Authority task completed', 'User confirmed completion of off-site SEO authority task.', 'marketing', 'normal', ['task_type' => 'seo_authority_task', 'source_id' => $id]);
    wp_send_json_success(['updated' => true, 'items' => vmo_seo_get_authority_task_rows($user_id)]);
}

function vmo_seo_get_local_visibility_checklist($user_id) {
    $profile = function_exists('vmo_get_consultation_profile') ? vmo_get_consultation_profile($user_id) : [];
    $nap = vmo_seo_business_nap($user_id, (array) $profile);
    $items = get_user_meta($user_id, 'vmo_seo_local_visibility_checklist', true);
    if (!is_array($items) || empty($items)) {
        $labels = ['Create or claim Google Business Profile', 'Confirm business name', 'Confirm business address or service area', 'Confirm phone number', 'Confirm website URL', 'Select primary category', 'Select secondary categories', 'Add business description', 'Add opening hours', 'Add service areas', 'Add products/services', 'Upload logo and business images', 'Add appointment link', 'Add UTM-tagged website link', 'Enable messaging if applicable', 'Create first Google post', 'Request first reviews'];
        $items = [];
        foreach ($labels as $index => $label) $items[] = ['id' => 'local_' . ($index + 1), 'title' => $label, 'status' => 'not_started'];
        update_user_meta($user_id, 'vmo_seo_local_visibility_checklist', $items);
    }
    $category = vmo_seo_profile_value((array) $profile, ['industry', 'business_type'], 'Local business');
    $service_areas = vmo_seo_value_as_list($profile['service_areas'] ?? $profile['location'] ?? []);
    $services = vmo_seo_value_as_list($profile['services'] ?? $profile['products_services'] ?? $profile['products'] ?? []);
    return [
        'items' => $items,
        'helpers' => [
            'business_description' => $nap['description'] ?: $nap['business_name'] . ' provides ' . $category . ' services with clear communication, reliable support, and customer-focused next steps.',
            'suggested_categories' => array_values(array_filter([$category, 'Service establishment', 'Professional services'])),
            'service_areas' => $service_areas,
            'products_services' => $services,
            'review_request_message' => 'Thanks for choosing ' . $nap['business_name'] . '. If the experience was helpful, would you share a quick review? It helps local customers find us and know what to expect.',
            'google_post' => $nap['business_name'] . ' is accepting new customers. Visit our website to learn about services, availability, and next steps.',
            'utm_website_url' => add_query_arg(['utm_source' => 'google', 'utm_medium' => 'organic', 'utm_campaign' => 'google_business_profile'], home_url('/')),
            'deferred_next_version' => ['Direct Google Business Profile API connection', 'Direct Google Ads setup', 'Direct map/listing automation', 'Promotion campaign sync', 'GBP performance pull'],
        ],
        'nap' => $nap,
    ];
}

function vmo_seo_update_local_visibility_status() {
    vmo_seo_ajax_guard();
    $user_id = get_current_user_id();
    $item_id = sanitize_key((string) ($_POST['item_id'] ?? ''));
    $status = sanitize_key((string) ($_POST['status'] ?? ''));
    if ($item_id === '' || !in_array($status, ['not_started', 'in_progress', 'completed', 'skipped'], true)) wp_send_json_error(['message' => 'Invalid checklist status']);
    $payload = vmo_seo_get_local_visibility_checklist($user_id);
    $items = $payload['items'];
    foreach ($items as &$item) if (($item['id'] ?? '') === $item_id) $item['status'] = $status;
    update_user_meta($user_id, 'vmo_seo_local_visibility_checklist', $items);
    if ($status === 'completed') vmo_seo_create_action_task($user_id, 'Local visibility checklist item completed', 'User confirmed completion: ' . $item_id, 'marketing', 'normal', ['task_type' => 'seo_local_visibility_task']);
    wp_send_json_success(vmo_seo_get_local_visibility_checklist($user_id));
}

function vmo_seo_identify_review_candidates($user_id) {
    global $wpdb;
    $contacts = [];
    $table = $wpdb->prefix . 'vmo_contacts';
    if (vmo_table_exists($table)) {
        $columns = vmo_seo_table_columns($table);
        $select = [];
        foreach (['contact_id', 'full_name', 'email', 'phone', 'lifecycle_stage', 'primary_category', 'lead_score', 'updated_at', 'created_at'] as $column) {
            if (in_array($column, $columns, true)) $select[] = $column;
        }
        if (empty($select)) $select = ['id'];
        $conditions = ['user_id=%d'];
        $relationship_conditions = [];
        if (in_array('lifecycle_stage', $columns, true)) $relationship_conditions[] = "lifecycle_stage='customer'";
        if (in_array('primary_category', $columns, true)) $relationship_conditions[] = "primary_category='customer'";
        if (in_array('lead_score', $columns, true)) $relationship_conditions[] = 'lead_score >= 80';
        if (!empty($relationship_conditions)) $conditions[] = '(' . implode(' OR ', $relationship_conditions) . ')';
        $order = in_array('lead_score', $columns, true) ? 'lead_score DESC' : (in_array('updated_at', $columns, true) ? 'updated_at DESC' : '1 DESC');
        $contacts = $wpdb->get_results($wpdb->prepare("SELECT " . implode(',', $select) . " FROM {$table} WHERE " . implode(' AND ', $conditions) . " ORDER BY {$order} LIMIT 12", $user_id), ARRAY_A) ?: [];
    }
    return array_map(static function($contact) {
        $channel = !empty($contact['email']) ? 'email' : (!empty($contact['phone']) ? 'SMS' : 'CRM task');
        return [
            'contact_id' => (string) ($contact['contact_id'] ?? ''),
            'customer_name' => (string) ($contact['full_name'] ?? 'Customer'),
            'recommended_channel' => $channel,
            'reason' => 'Customer or high-score contact with likely positive relationship signals.',
            'status' => 'draft',
            'platform' => 'Google',
            'follow_up_date' => date('Y-m-d', strtotime('+3 days')),
        ];
    }, $contacts);
}

function vmo_seo_generate_review_request_message($user_id, $contact_id, $channel = 'email') {
    global $wpdb;
    $business_name = get_bloginfo('name');
    $customer_name = 'there';
    $table = $wpdb->prefix . 'vmo_contacts';
    if (vmo_table_exists($table) && $contact_id !== '') {
        $row = $wpdb->get_row($wpdb->prepare("SELECT full_name FROM {$table} WHERE user_id=%d AND contact_id=%s LIMIT 1", $user_id, $contact_id), ARRAY_A);
        if (!empty($row['full_name'])) $customer_name = $row['full_name'];
    }
    return "Hi {$customer_name}, thanks again for choosing {$business_name}. If your experience was helpful, would you share a quick review? It helps local customers find us and know what to expect. We appreciate it.";
}

function vmo_seo_get_contact_row($user_id, $contact_id) {
    global $wpdb;
    $table = $wpdb->prefix . 'vmo_contacts';
    if (!vmo_table_exists($table) || $contact_id === '') return [];
    $row = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table} WHERE user_id=%d AND contact_id=%s LIMIT 1", $user_id, $contact_id), ARRAY_A);
    return is_array($row) ? $row : [];
}

function vmo_seo_update_review_request_history($user_id, $request_id, array $updates) {
    $history = get_user_meta($user_id, 'vmo_seo_review_requests', true);
    $history = is_array($history) ? $history : [];
    foreach ($history as &$request) {
        if ((string) ($request['request_id'] ?? '') === (string) $request_id) {
            $request = array_merge($request, $updates, ['updated_at' => current_time('mysql')]);
            break;
        }
    }
    update_user_meta($user_id, 'vmo_seo_review_requests', array_slice($history, -100));
    return $history;
}

function vmo_seo_send_review_request_email($user_id, array $contact, $message, $platform = 'Google') {
    $email = sanitize_email((string) ($contact['email'] ?? ''));
    if ($email === '') return ['sent' => false, 'status' => 'draft', 'message' => 'No email address available.'];
    $subject = 'Could you share a quick review?';
    $html = '<p>' . nl2br(esc_html($message)) . '</p>';
    $auto_send = (bool) get_user_meta($user_id, 'vmo_seo_review_auto_send_email', true);
    if (!$auto_send) return ['sent' => false, 'status' => 'draft', 'message' => 'Email draft created; auto-send is disabled.', 'to' => $email, 'subject' => $subject, 'body' => $html];
    if (class_exists('VMO_Email_Router')) {
        $result = VMO_Email_Router::get_instance()->send_email($email, $subject, $html, 'transactional', ['headers' => ['Content-Type: text/html; charset=UTF-8'], 'skip_storage' => false]);
        return ['sent' => !empty($result['success']), 'status' => !empty($result['success']) ? 'sent' : 'draft', 'message' => (string) ($result['message'] ?? 'Email routed.'), 'to' => $email];
    }
    $sent = wp_mail($email, $subject, $html, ['Content-Type: text/html; charset=UTF-8']);
    return ['sent' => (bool) $sent, 'status' => $sent ? 'sent' : 'draft', 'message' => $sent ? 'Email sent.' : 'Email send failed; draft retained.', 'to' => $email];
}

function vmo_seo_send_review_request_sms($user_id, array $contact, $message, $platform = 'Google') {
    $phone = sanitize_text_field((string) ($contact['phone'] ?? ''));
    if ($phone === '') return ['sent' => false, 'status' => 'draft', 'message' => 'No phone number available.'];
    $sms_allowed = !empty($contact['sms_opt_in']) || !empty($contact['marketing_sms_opt_in']) || (bool) get_user_meta($user_id, 'vmo_seo_review_sms_permission_confirmed', true);
    $auto_send = (bool) get_user_meta($user_id, 'vmo_seo_review_auto_send_sms', true);
    if (!$sms_allowed || !$auto_send) return ['sent' => false, 'status' => 'draft', 'message' => 'SMS draft created; permission or auto-send is not enabled.', 'to' => $phone, 'body' => $message];
    $client = null;
    if (class_exists('VMO_Twilio_Enhanced_Client')) {
        $client = method_exists('VMO_Twilio_Enhanced_Client', 'get_instance_for_user') ? VMO_Twilio_Enhanced_Client::get_instance_for_user($user_id) : null;
        if (!$client && method_exists('VMO_Twilio_Enhanced_Client', 'get_instance')) $client = VMO_Twilio_Enhanced_Client::get_instance();
    }
    if (!$client || !method_exists($client, 'send_sms')) return ['sent' => false, 'status' => 'draft', 'message' => 'SMS client unavailable; CRM task created.', 'to' => $phone, 'body' => $message];
    $result = $client->send_sms($phone, $message, ['track_status' => true, 'source' => 'seo_review_growth']);
    if (is_wp_error($result)) return ['sent' => false, 'status' => 'draft', 'message' => $result->get_error_message(), 'to' => $phone, 'body' => $message];
    return ['sent' => true, 'status' => 'sent', 'message' => 'SMS sent.', 'to' => $phone, 'sid' => (string) ($result['sid'] ?? $result['message_sid'] ?? '')];
}

function vmo_seo_create_review_followup_sequence($user_id, array $request) {
    $sequence = get_user_meta($user_id, 'vmo_seo_review_followup_sequence', true);
    $sequence = is_array($sequence) ? $sequence : [];
    $request_id = (string) ($request['request_id'] ?? wp_generate_uuid4());
    $sequence[$request_id] = [
        'request_id' => $request_id,
        'contact_id' => (string) ($request['contact_id'] ?? ''),
        'platform' => (string) ($request['platform'] ?? 'Google'),
        'steps' => [
            ['delay_days' => 3, 'channel' => (string) ($request['recommended_channel'] ?? 'email'), 'status' => 'scheduled', 'purpose' => 'polite_review_reminder'],
            ['delay_days' => 10, 'channel' => 'crm_task', 'status' => 'scheduled', 'purpose' => 'confirm_review_received_or_skip'],
        ],
        'created_at' => current_time('mysql'),
    ];
    update_user_meta($user_id, 'vmo_seo_review_followup_sequence', $sequence);
    return $sequence[$request_id];
}

function vmo_seo_log_review_request($user_id, array $request) {
    $history = get_user_meta($user_id, 'vmo_seo_review_requests', true);
    if (!is_array($history)) $history = [];
    if (empty($request['request_id'])) $request['request_id'] = function_exists('wp_generate_uuid4') ? wp_generate_uuid4() : md5(wp_json_encode($request) . microtime(true));
    $request['created_at'] = current_time('mysql');
    $history[] = $request;
    update_user_meta($user_id, 'vmo_seo_review_requests', array_slice($history, -100));
    return $request;
}

function vmo_seo_schedule_review_request($user_id = null, $contact_id = null, $channel = null) {
    $is_ajax = $user_id === null;
    if ($is_ajax) vmo_seo_ajax_guard();
    $user_id = $user_id ?: get_current_user_id();
    $contact_id = $contact_id ?: sanitize_text_field(wp_unslash($_POST['contact_id'] ?? ''));
    $channel = $channel ?: sanitize_key((string) ($_POST['channel'] ?? 'email'));
    $platform = sanitize_text_field(wp_unslash($_POST['platform'] ?? 'Google'));
    $message = vmo_seo_generate_review_request_message($user_id, $contact_id, $channel);
    $contact = vmo_seo_get_contact_row($user_id, $contact_id);
    $send_result = ['sent' => false, 'status' => 'draft', 'message' => 'No direct channel available; CRM task created.'];
    if ($channel === 'sms' || $channel === 'SMS') $send_result = vmo_seo_send_review_request_sms($user_id, $contact, $message, $platform);
    elseif ($channel === 'email') $send_result = vmo_seo_send_review_request_email($user_id, $contact, $message, $platform);
    $status = !empty($send_result['sent']) ? 'sent' : ($send_result['status'] ?? 'draft');
    $request = vmo_seo_log_review_request($user_id, ['contact_id' => $contact_id, 'recommended_channel' => $channel, 'message' => $message, 'status' => $status, 'platform' => $platform, 'follow_up_date' => date('Y-m-d', strtotime('+3 days')), 'send_result' => $send_result]);
    vmo_seo_create_review_followup_sequence($user_id, $request);
    vmo_seo_create_action_task($user_id, $status === 'sent' ? 'Review request sent' : 'Send review request draft', $message, 'marketing', 'normal', ['task_type' => 'seo_review_task', 'contact_id' => $contact_id, 'request_id' => $request['request_id'] ?? '']);
    vmo_record_attribution_event($user_id, 'review_request', ['contact_id' => $contact_id, 'source' => 'seo_review_growth', 'medium' => $channel, 'event_subtype' => $status, 'content' => $platform]);
    if ($is_ajax) wp_send_json_success(['request' => $request, 'status' => vmo_seo_get_review_campaign_status($user_id)]);
    return $request;
}

function vmo_seo_get_review_campaign_status($user_id) {
    $history = get_user_meta($user_id, 'vmo_seo_review_requests', true);
    $history = is_array($history) ? $history : [];
    return ['requests_sent' => count(array_filter($history, static fn($r) => in_array(($r['status'] ?? ''), ['scheduled', 'sent', 'completed'], true))), 'confirmed_reviews' => count(array_filter($history, static fn($r) => ($r['status'] ?? '') === 'completed')), 'platforms_configured' => array_values(array_unique(array_filter(array_map(static fn($r) => (string) ($r['platform'] ?? ''), $history)))), 'history' => array_slice($history, -20)];
}

function vmo_seo_generate_review_response_draft($user_id = null, $review_text = '', $platform = 'Google') {
    $is_ajax = $user_id === null;
    if ($is_ajax) vmo_seo_ajax_guard();
    $user_id = $user_id ?: get_current_user_id();
    $review_text = $review_text !== '' ? $review_text : sanitize_textarea_field(wp_unslash($_POST['review_text'] ?? ''));
    $platform = sanitize_text_field($platform ?: ($_POST['platform'] ?? 'Google'));
    $business_name = get_bloginfo('name');
    $draft = "Thank you for taking the time to share your experience with {$business_name}. We appreciate your feedback and are glad we could help. Your review helps other local customers understand what to expect when they contact us.";
    $api_key = (string) get_option('vmo_openai_api_key', '');
    if ($api_key !== '' && $review_text !== '' && vmo_seo_has_token_balance($user_id, 'VMO_TOKENS_SEO_REVIEW_MESSAGE')) {
        $model = function_exists('vmo_get_model') ? vmo_get_model('mini') : 'gpt-5.5-mini';
        $prompt = "Draft a concise, warm, professional public review response for {$business_name} on {$platform}. Do not claim facts not present. Review text:\n" . substr($review_text, 0, 2000);
        $body = array_merge(['model' => $model, 'messages' => [['role' => 'system', 'content' => 'You write safe, customer-friendly public review responses.'], ['role' => 'user', 'content' => $prompt]], 'temperature' => 0.35], function_exists('vmo_get_token_param') ? vmo_get_token_param($model, 450) : ['max_completion_tokens' => 450]);
        $response = wp_remote_post('https://api.openai.com/v1/chat/completions', ['headers' => ['Authorization' => 'Bearer ' . $api_key, 'Content-Type' => 'application/json'], 'body' => wp_json_encode($body), 'timeout' => 25]);
        if (!is_wp_error($response)) {
            $raw = json_decode((string) wp_remote_retrieve_body($response), true);
            $ai_draft = trim(wp_strip_all_tags((string) ($raw['choices'][0]['message']['content'] ?? '')));
            if ($ai_draft !== '') {
                $draft = $ai_draft;
                vmo_seo_charge_tokens($user_id, 'VMO_TOKENS_SEO_REVIEW_MESSAGE', 'seo_review_growth', 'Generated AI review response draft');
            }
        }
    }
    if ($is_ajax) wp_send_json_success(['draft' => $draft, 'platform' => $platform]);
    return $draft;
}

function vmo_seo_mark_review_received() {
    vmo_seo_ajax_guard();
    $user_id = get_current_user_id();
    $request_id = sanitize_text_field(wp_unslash($_POST['request_id'] ?? ''));
    $contact_id = sanitize_text_field(wp_unslash($_POST['contact_id'] ?? ''));
    if ($request_id === '' && $contact_id === '') wp_send_json_error(['message' => 'Missing review request or contact ID.']);
    $history = get_user_meta($user_id, 'vmo_seo_review_requests', true);
    $history = is_array($history) ? $history : [];
    foreach ($history as &$request) {
        if (($request_id !== '' && (string) ($request['request_id'] ?? '') === $request_id) || ($contact_id !== '' && (string) ($request['contact_id'] ?? '') === $contact_id)) {
            $request['status'] = 'completed';
            $request['completed_at'] = current_time('mysql');
            break;
        }
    }
    update_user_meta($user_id, 'vmo_seo_review_requests', $history);
    vmo_record_attribution_event($user_id, 'review_received', ['contact_id' => $contact_id, 'source' => 'seo_review_growth', 'medium' => 'review_platform', 'event_subtype' => 'completed']);
    wp_send_json_success(['updated' => true, 'status' => vmo_seo_get_review_campaign_status($user_id)]);
}

function vmo_seo_review_growth_dashboard() {
    $user_id = get_current_user_id();
    return ['candidates' => vmo_seo_identify_review_candidates($user_id), 'campaign_status' => vmo_seo_get_review_campaign_status($user_id)];
}

function vmo_seo_ai_search_audit($user_id) {
    global $wpdb;
    $profile = function_exists('vmo_get_consultation_profile') ? vmo_get_consultation_profile($user_id) : [];
    $site_text = strtolower(wp_strip_all_tags(wp_json_encode($profile)) . ' ' . get_bloginfo('name') . ' ' . get_bloginfo('description'));
    $page_count = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_status='publish' AND post_type IN ('post','page')");
    $faq_count = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_status='publish' AND post_type IN ('post','page') AND (post_content LIKE '%<h2%FAQ%' OR post_content LIKE '%Frequently Asked Questions%')");
    $checks = [
        'business_identity' => !empty($profile['business_name']),
        'services_products' => !empty($profile['services']) || !empty($profile['products']) || !empty($profile['products_services']),
        'location_service_area' => !empty($profile['location']) || !empty($profile['service_areas']),
        'faq_sections' => $faq_count > 0,
        'crawlable_text' => $page_count > 0,
        'entity_schema' => true,
        'case_studies_reviews' => strpos($site_text, 'testimonial') !== false || strpos($site_text, 'review') !== false,
        'llms_txt' => (bool) get_user_meta($user_id, 'vmo_seo_llms_txt', true),
        'answer_blocks' => $page_count >= 3,
        'comparison_or_glossary' => strpos($site_text, 'compare') !== false || strpos($site_text, 'glossary') !== false,
    ];
    $score = (int) round((count(array_filter($checks)) / max(1, count($checks))) * 100);
    return ['readiness_score' => $score, 'label' => 'Lightweight readiness score - not actual platform ranking', 'source' => 'non-AI checklist audit; executable drafts may use AI/content tokens', 'platforms' => ['ChatGPT', 'Perplexity', 'Gemini', 'Google AI Overviews', 'Copilot'], 'checks' => $checks, 'recommendations' => vmo_seo_ai_visibility_recommendations($user_id)];
}

function vmo_seo_generate_llms_txt($user_id) {
    $profile = function_exists('vmo_get_consultation_profile') ? vmo_get_consultation_profile($user_id) : [];
    $nap = vmo_seo_business_nap($user_id, (array) $profile);
    $pages = get_posts(['post_type' => ['page', 'post'], 'post_status' => 'publish', 'numberposts' => 12]);
    $lines = ['# ' . $nap['business_name'], 'Website: ' . home_url('/'), 'Description: ' . ($nap['description'] ?: get_bloginfo('description')), 'Contact: ' . trim($nap['phone'] . ' ' . $nap['website']), 'Services/products: ' . vmo_seo_profile_value((array) $profile, ['services', 'products', 'products_services'], ''), 'Locations: ' . vmo_seo_profile_value((array) $profile, ['location', 'service_areas'], ''), '', 'Important pages:'];
    foreach ($pages as $page) $lines[] = '- ' . get_the_title($page) . ': ' . get_permalink($page);
    $content = implode("\n", array_filter($lines, static fn($line) => $line !== null));
    update_user_meta($user_id, 'vmo_seo_llms_txt', $content);
    return $content;
}

function vmo_seo_register_llms_txt_route() {
    add_rewrite_rule('^llms\.txt$', 'index.php?vmo_llms_txt=1', 'top');
    add_filter('query_vars', static function($vars) { $vars[] = 'vmo_llms_txt'; return $vars; });
}

function vmo_seo_serve_llms_txt() {
    $path = trim((string) wp_parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH), '/');
    if ($path !== 'llms.txt' && !get_query_var('vmo_llms_txt')) return;
    $user_id = get_current_user_id();
    if ($user_id <= 0) {
        $admins = get_users(['role__in' => ['administrator'], 'number' => 1, 'fields' => 'ID']);
        $user_id = !empty($admins[0]) ? (int) $admins[0] : 0;
    }
    $content = $user_id > 0 ? get_user_meta($user_id, 'vmo_seo_llms_txt', true) : '';
    if (!is_string($content) || trim($content) === '') $content = $user_id > 0 ? vmo_seo_generate_llms_txt($user_id) : '# ' . get_bloginfo('name') . "\nWebsite: " . home_url('/');
    status_header(200);
    nocache_headers();
    header('Content-Type: text/plain; charset=utf-8');
    echo $content;
    exit;
}

function vmo_seo_generate_llms_txt_ajax() {
    vmo_seo_ajax_guard();
    wp_send_json_success(['llms_txt' => vmo_seo_generate_llms_txt(get_current_user_id()), 'url' => home_url('/llms.txt')]);
}

function vmo_seo_generate_answer_blocks($post_id, $keyword) {
    return '<section class="vmo-answer-block"><h2>Best Answer: ' . esc_html($keyword) . '</h2><p>' . esc_html(get_the_title($post_id)) . ' explains the key answer, when it matters, and the next step customers should take.</p></section>';
}

function vmo_seo_generate_entity_schema($user_id, $post_id) {
    $nap = vmo_seo_business_nap($user_id);
    return ['@context' => 'https://schema.org', '@type' => 'LocalBusiness', 'name' => $nap['business_name'], 'url' => home_url('/'), 'telephone' => $nap['phone'], 'address' => $nap['address'], 'mainEntityOfPage' => get_permalink($post_id)];
}

function vmo_seo_ai_visibility_recommendations($user_id) {
    $customer_language = get_user_meta($user_id, 'vmo_seo_customer_language', true);
    $questions = is_array($customer_language) ? array_slice((array) ($customer_language['top_questions'] ?? $customer_language['seo_faq_opportunities'] ?? []), 0, 8) : [];
    return [
        ['type' => 'faq_block', 'title' => 'Create an FAQ block from customer questions', 'impact' => 'high', 'executable' => true, 'questions' => $questions],
        ['type' => 'answer_block', 'title' => 'Add an answer block to the strongest service page', 'impact' => 'high', 'executable' => true],
        ['type' => 'comparison_page', 'title' => 'Create comparison pages for buyer questions', 'impact' => 'medium', 'executable' => true],
        ['type' => 'glossary_page', 'title' => 'Add glossary/definition content for entity clarity', 'impact' => 'medium', 'executable' => true],
        ['type' => 'llms_txt', 'title' => 'Generate llms.txt for AI crawler context', 'impact' => 'medium', 'executable' => true, 'url' => home_url('/llms.txt')],
        ['type' => 'entity_schema', 'title' => 'Add entity-rich LocalBusiness/Product/Service schema', 'impact' => 'high', 'executable' => true],
    ];
}

function vmo_seo_execute_ai_search_recommendation() {
    vmo_seo_ajax_guard();
    $user_id = get_current_user_id();
    $type = sanitize_key((string) ($_POST['type'] ?? ''));
    $profile = function_exists('vmo_get_consultation_profile') ? vmo_get_consultation_profile($user_id) : [];
    $business_name = vmo_seo_profile_value((array) $profile, ['business_name', 'company_name'], get_bloginfo('name'));
    $industry = vmo_seo_profile_value((array) $profile, ['industry', 'business_type'], 'service');
    $location = vmo_seo_profile_value((array) $profile, ['city', 'location', 'service_area'], '');
    if ($type === 'llms_txt') wp_send_json_success(['created' => true, 'url' => home_url('/llms.txt'), 'llms_txt' => vmo_seo_generate_llms_txt($user_id)]);
    $topic_map = [
        'faq_block' => 'FAQ answers for ' . $business_name . ' customers',
        'answer_block' => 'Best answer guide for ' . $industry . ($location ? ' in ' . $location : ''),
        'comparison_page' => $business_name . ' comparison guide for buyers',
        'glossary_page' => $industry . ' glossary for customer questions',
        'entity_schema' => 'Entity authority page for ' . $business_name,
    ];
    if (!isset($topic_map[$type])) wp_send_json_error(['message' => 'Unknown AI-search recommendation type.']);
    $result = vmo_seo_create_content_asset($user_id, $topic_map[$type], in_array($type, ['faq_block', 'answer_block', 'entity_schema'], true) ? 'page' : 'blog_post', ['post_status' => 'draft', 'source' => 'ai_search_readiness', 'auto_execute' => false]);
    vmo_seo_create_action_task($user_id, 'Review AI-search draft', 'Review and approve the draft created from AI Search / GEO recommendation: ' . $type, 'marketing', 'high', ['task_type' => 'seo_ai_search_action', 'recommendation_type' => $type, 'source_id' => (int) ($result['post_id'] ?? 0)]);
    wp_send_json_success($result);
}

function vmo_gsc_save_credentials($user_id = null, ?array $credentials = null) {
    $is_ajax = $user_id === null;
    if ($is_ajax) vmo_seo_ajax_guard();
    $user_id = $user_id ?: get_current_user_id();
    $existing = get_user_meta($user_id, 'vmo_gsc_credentials', true);
    $existing = is_array($existing) ? $existing : [];
    $incoming = $credentials ?: ['client_id' => sanitize_text_field(wp_unslash($_POST['client_id'] ?? '')), 'client_secret' => sanitize_text_field(wp_unslash($_POST['client_secret'] ?? '')), 'site_url' => esc_url_raw(wp_unslash($_POST['site_url'] ?? home_url('/'))), 'access_token' => sanitize_text_field(wp_unslash($_POST['access_token'] ?? '')), 'refresh_token' => sanitize_text_field(wp_unslash($_POST['refresh_token'] ?? ''))];
    $credentials = array_merge($existing, array_filter($incoming, static fn($value) => trim((string) $value) !== ''));
    update_user_meta($user_id, 'vmo_gsc_credentials', $credentials);
    if ($is_ajax) wp_send_json_success(['saved' => true, 'status' => vmo_gsc_get_latest_metrics($user_id)]);
    return true;
}

function vmo_gsc_oauth_redirect_uri() { return admin_url('admin-ajax.php?action=vmo_gsc_handle_oauth_callback'); }

function vmo_gsc_start_oauth($user_id = null) {
    $is_ajax = $user_id === null;
    if ($is_ajax) vmo_seo_ajax_guard();
    $user_id = $user_id ?: get_current_user_id();
    $creds = get_user_meta($user_id, 'vmo_gsc_credentials', true);
    $client_id = is_array($creds) ? ($creds['client_id'] ?? '') : '';
    $state_payload = wp_json_encode(['user_id' => $user_id, 'nonce' => wp_create_nonce('vmo_gsc_oauth_' . $user_id), 'ts' => time()]);
    $state = rtrim(strtr(base64_encode($state_payload), '+/', '-_'), '=');
    update_user_meta($user_id, 'vmo_gsc_oauth_state', $state);
    $url = $client_id ? add_query_arg(['client_id' => $client_id, 'redirect_uri' => vmo_gsc_oauth_redirect_uri(), 'response_type' => 'code', 'scope' => 'https://www.googleapis.com/auth/webmasters.readonly', 'access_type' => 'offline', 'prompt' => 'consent', 'state' => $state], 'https://accounts.google.com/o/oauth2/v2/auth') : '';
    if ($is_ajax) wp_send_json_success(['oauth_url' => $url, 'message' => $url ? 'Open this URL to connect Search Console.' : 'Save a Google OAuth client ID first.']);
    return $url;
}

function vmo_gsc_user_from_state($state) {
    $state = sanitize_text_field((string) $state);
    if ($state === '') return get_current_user_id();
    $decoded = base64_decode(strtr($state, '-_', '+/'));
    $payload = json_decode((string) $decoded, true);
    if (!is_array($payload) || empty($payload['user_id']) || empty($payload['nonce'])) return get_current_user_id();
    $user_id = absint($payload['user_id']);
    $saved_state = get_user_meta($user_id, 'vmo_gsc_oauth_state', true);
    if (!hash_equals((string) $saved_state, $state)) return 0;
    if (!wp_verify_nonce((string) $payload['nonce'], 'vmo_gsc_oauth_' . $user_id)) return 0;
    return $user_id;
}

function vmo_gsc_exchange_code_for_tokens($user_id, $code) {
    $creds = get_user_meta($user_id, 'vmo_gsc_credentials', true);
    $creds = is_array($creds) ? $creds : [];
    if (empty($creds['client_id']) || empty($creds['client_secret']) || $code === '') return new WP_Error('missing_gsc_oauth_credentials', 'Save a Google OAuth client ID and secret first.');
    $response = wp_remote_post('https://oauth2.googleapis.com/token', [
        'timeout' => 20,
        'body' => [
            'code' => $code,
            'client_id' => $creds['client_id'],
            'client_secret' => $creds['client_secret'],
            'redirect_uri' => vmo_gsc_oauth_redirect_uri(),
            'grant_type' => 'authorization_code',
        ],
    ]);
    if (is_wp_error($response)) return $response;
    $body = json_decode((string) wp_remote_retrieve_body($response), true);
    if (!is_array($body) || empty($body['access_token'])) return new WP_Error('gsc_token_exchange_failed', (string) ($body['error_description'] ?? $body['error'] ?? 'Google token exchange failed.'));
    $creds['access_token'] = sanitize_text_field((string) $body['access_token']);
    if (!empty($body['refresh_token'])) $creds['refresh_token'] = sanitize_text_field((string) $body['refresh_token']);
    $creds['expires_at'] = time() + max(60, (int) ($body['expires_in'] ?? 3600)) - 60;
    update_user_meta($user_id, 'vmo_gsc_credentials', $creds);
    return $creds;
}

function vmo_gsc_refresh_access_token($user_id) {
    $creds = get_user_meta($user_id, 'vmo_gsc_credentials', true);
    $creds = is_array($creds) ? $creds : [];
    if (empty($creds['refresh_token']) || empty($creds['client_id']) || empty($creds['client_secret'])) return new WP_Error('missing_gsc_refresh_token', 'Search Console refresh token is missing.');
    $response = wp_remote_post('https://oauth2.googleapis.com/token', [
        'timeout' => 20,
        'body' => ['refresh_token' => $creds['refresh_token'], 'client_id' => $creds['client_id'], 'client_secret' => $creds['client_secret'], 'grant_type' => 'refresh_token'],
    ]);
    if (is_wp_error($response)) return $response;
    $body = json_decode((string) wp_remote_retrieve_body($response), true);
    if (!is_array($body) || empty($body['access_token'])) return new WP_Error('gsc_refresh_failed', (string) ($body['error_description'] ?? $body['error'] ?? 'Google token refresh failed.'));
    $creds['access_token'] = sanitize_text_field((string) $body['access_token']);
    $creds['expires_at'] = time() + max(60, (int) ($body['expires_in'] ?? 3600)) - 60;
    update_user_meta($user_id, 'vmo_gsc_credentials', $creds);
    return $creds;
}

function vmo_gsc_get_access_token($user_id) {
    $creds = get_user_meta($user_id, 'vmo_gsc_credentials', true);
    $creds = is_array($creds) ? $creds : [];
    if (!empty($creds['access_token']) && (int) ($creds['expires_at'] ?? 0) > time()) return (string) $creds['access_token'];
    $refreshed = vmo_gsc_refresh_access_token($user_id);
    if (is_wp_error($refreshed)) return $refreshed;
    return (string) ($refreshed['access_token'] ?? '');
}

function vmo_gsc_api_request($user_id, $method, $url, ?array $body = null) {
    $token = vmo_gsc_get_access_token($user_id);
    if (is_wp_error($token)) return $token;
    if ($token === '') return new WP_Error('gsc_not_connected', 'Connect Search Console for verified search data.');
    $args = ['method' => strtoupper($method), 'timeout' => 25, 'headers' => ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json']];
    if ($body !== null) $args['body'] = wp_json_encode($body);
    $response = wp_remote_request($url, $args);
    if (is_wp_error($response)) return $response;
    $code = (int) wp_remote_retrieve_response_code($response);
    $decoded = json_decode((string) wp_remote_retrieve_body($response), true);
    if ($code >= 400) return new WP_Error('gsc_api_error', (string) ($decoded['error']['message'] ?? 'Google Search Console API error.'));
    return is_array($decoded) ? $decoded : [];
}

function vmo_gsc_handle_oauth_callback($user_id = null) {
    $user_id = $user_id ?: vmo_gsc_user_from_state($_REQUEST['state'] ?? '');
    if ($user_id <= 0) wp_send_json_error(['connected' => false, 'message' => 'Invalid Search Console OAuth state.'], 403);
    $code = sanitize_text_field(wp_unslash($_REQUEST['code'] ?? ''));
    if ($code === '') wp_send_json_error(['connected' => false, 'message' => 'Missing OAuth code.']);
    update_user_meta($user_id, 'vmo_gsc_pending_oauth_code', $code);
    $tokens = vmo_gsc_exchange_code_for_tokens($user_id, $code);
    if (is_wp_error($tokens)) wp_send_json_error(['connected' => false, 'message' => $tokens->get_error_message(), 'code_saved' => true]);
    $sites = vmo_gsc_fetch_sites($user_id);
    vmo_gsc_refresh_metrics($user_id);
    wp_send_json_success(['connected' => true, 'message' => 'Search Console connected.', 'sites' => $sites]);
}

function vmo_gsc_fetch_sites($user_id = null) {
    $is_ajax = $user_id === null;
    if ($is_ajax) vmo_seo_ajax_guard();
    $user_id = $user_id ?: get_current_user_id();
    $result = vmo_gsc_api_request($user_id, 'GET', 'https://www.googleapis.com/webmasters/v3/sites');
    if (is_wp_error($result)) {
        if ($is_ajax) wp_send_json_error(['sites' => [], 'message' => $result->get_error_message()]);
        return [];
    }
    $sites = array_values((array) ($result['siteEntry'] ?? []));
    if (!empty($sites[0]['siteUrl'])) {
        $creds = get_user_meta($user_id, 'vmo_gsc_credentials', true);
        $creds = is_array($creds) ? $creds : [];
        if (empty($creds['site_url'])) { $creds['site_url'] = esc_url_raw((string) $sites[0]['siteUrl']); update_user_meta($user_id, 'vmo_gsc_credentials', $creds); }
    }
    update_user_meta($user_id, 'vmo_gsc_sites', $sites);
    if ($is_ajax) wp_send_json_success(['sites' => $sites, 'message' => empty($sites) ? 'No verified Search Console properties found for this Google account.' : 'Search Console sites fetched.']);
    return $sites;
}

function vmo_gsc_search_analytics_query($user_id, $site_url, $start_date, $end_date, array $dimensions, $row_limit = 25) {
    $endpoint = 'https://www.googleapis.com/webmasters/v3/sites/' . rawurlencode($site_url) . '/searchAnalytics/query';
    $body = ['startDate' => $start_date, 'endDate' => $end_date, 'dimensions' => $dimensions, 'rowLimit' => max(1, min(25000, (int) $row_limit))];
    return vmo_gsc_api_request($user_id, 'POST', $endpoint, $body);
}

function vmo_gsc_rows_to_named_items($result, $primary_key = 'value') {
    $items = [];
    foreach ((array) ($result['rows'] ?? []) as $row) {
        $keys = (array) ($row['keys'] ?? []);
        $items[] = [$primary_key => (string) ($keys[0] ?? ''), 'clicks' => (int) ($row['clicks'] ?? 0), 'impressions' => (int) ($row['impressions'] ?? 0), 'ctr' => round((float) ($row['ctr'] ?? 0), 4), 'position' => round((float) ($row['position'] ?? 0), 2)];
    }
    return $items;
}

function vmo_gsc_fetch_performance($site_url, $start_date, $end_date, $user_id = null) {
    $user_id = $user_id ?: get_current_user_id();
    $summary = vmo_gsc_search_analytics_query($user_id, $site_url, $start_date, $end_date, [], 1);
    if (is_wp_error($summary)) return ['source' => 'Google Search Console', 'connected' => false, 'message' => $summary->get_error_message(), 'clicks' => 0, 'impressions' => 0, 'ctr' => 0, 'average_position' => 0, 'queries' => [], 'pages' => [], 'countries' => [], 'devices' => [], 'date_trends' => []];
    $query_rows = vmo_gsc_search_analytics_query($user_id, $site_url, $start_date, $end_date, ['query'], 50);
    $page_rows = vmo_gsc_search_analytics_query($user_id, $site_url, $start_date, $end_date, ['page'], 50);
    $device_rows = vmo_gsc_search_analytics_query($user_id, $site_url, $start_date, $end_date, ['device'], 20);
    $country_rows = vmo_gsc_search_analytics_query($user_id, $site_url, $start_date, $end_date, ['country'], 20);
    $date_rows = vmo_gsc_search_analytics_query($user_id, $site_url, $start_date, $end_date, ['date'], 90);
    $summary_row = (array) (($summary['rows'][0] ?? []));
    $queries = !is_wp_error($query_rows) ? vmo_gsc_rows_to_named_items($query_rows, 'keyword') : [];
    $pages = !is_wp_error($page_rows) ? vmo_gsc_rows_to_named_items($page_rows, 'page') : [];
    return [
        'source' => 'Google Search Console',
        'connected' => true,
        'clicks' => (int) ($summary_row['clicks'] ?? 0),
        'impressions' => (int) ($summary_row['impressions'] ?? 0),
        'ctr' => round((float) ($summary_row['ctr'] ?? 0), 4),
        'average_position' => round((float) ($summary_row['position'] ?? 0), 2),
        'queries' => $queries,
        'pages' => $pages,
        'devices' => !is_wp_error($device_rows) ? vmo_gsc_rows_to_named_items($device_rows, 'device') : [],
        'countries' => !is_wp_error($country_rows) ? vmo_gsc_rows_to_named_items($country_rows, 'country') : [],
        'date_trends' => !is_wp_error($date_rows) ? vmo_gsc_rows_to_named_items($date_rows, 'date') : [],
        'top_pages' => array_slice($pages, 0, 10),
        'underperforming_pages' => array_values(array_filter($pages, static fn($page) => (($page['impressions'] ?? 0) >= 50 && ($page['ctr'] ?? 0) < 0.02) || (($page['position'] ?? 0) > 15))),
        'keyword_opportunities' => array_values(array_filter($queries, static fn($kw) => (($kw['impressions'] ?? 0) >= 50 && ($kw['position'] ?? 0) > 8 && ($kw['position'] ?? 0) <= 30))),
    ];
}

function vmo_gsc_fetch_index_coverage($site_url, $user_id = null) {
    $user_id = $user_id ?: get_current_user_id();
    $pages = vmo_gsc_search_analytics_query($user_id, $site_url, date('Y-m-d', strtotime('-90 days')), date('Y-m-d'), ['page'], 25000);
    if (is_wp_error($pages)) return ['source' => 'Google Search Console', 'connected' => false, 'indexed_pages' => null, 'message' => $pages->get_error_message()];
    return ['source' => 'Google Search Console', 'connected' => true, 'indexed_pages' => count((array) ($pages['rows'] ?? [])), 'message' => 'Indexed-page estimate from Search Console pages receiving search impressions.'];
}

function vmo_gsc_fetch_sitemaps($site_url, $user_id = null) {
    $user_id = $user_id ?: get_current_user_id();
    $result = vmo_gsc_api_request($user_id, 'GET', 'https://www.googleapis.com/webmasters/v3/sites/' . rawurlencode($site_url) . '/sitemaps');
    if (is_wp_error($result)) return ['source' => 'Google Search Console', 'connected' => false, 'sitemaps' => [], 'message' => $result->get_error_message()];
    return ['source' => 'Google Search Console', 'connected' => true, 'sitemaps' => array_values((array) ($result['sitemap'] ?? []))];
}

function vmo_gsc_refresh_metrics($user_id) {
    $creds = get_user_meta($user_id, 'vmo_gsc_credentials', true);
    $creds = is_array($creds) ? $creds : [];
    $site_url = (string) ($creds['site_url'] ?? home_url('/'));
    if ($site_url === '') return vmo_gsc_get_latest_metrics($user_id);
    $end = date('Y-m-d', strtotime('-1 day'));
    $start = date('Y-m-d', strtotime('-90 days'));
    $performance = vmo_gsc_fetch_performance($site_url, $start, $end, $user_id);
    $coverage = vmo_gsc_fetch_index_coverage($site_url, $user_id);
    $sitemaps = vmo_gsc_fetch_sitemaps($site_url, $user_id);
    $metrics = array_merge($performance, ['site_url' => $site_url, 'indexed_pages' => $coverage['indexed_pages'] ?? null, 'index_coverage' => $coverage, 'sitemaps' => $sitemaps['sitemaps'] ?? [], 'fetched_at' => current_time('mysql')]);
    vmo_gsc_store_metrics($user_id, $metrics);
    return $metrics;
}

function vmo_gsc_store_metrics($user_id, $metrics) { update_user_meta($user_id, 'vmo_gsc_latest_metrics', is_array($metrics) ? $metrics : []); }
function vmo_gsc_get_latest_metrics($user_id) { $metrics = get_user_meta($user_id, 'vmo_gsc_latest_metrics', true); return is_array($metrics) && !empty($metrics) ? $metrics : ['connected' => false, 'source' => 'Google Search Console', 'message' => 'Connect Search Console for verified search data.']; }

function vmo_detect_traffic_source($referrer, $utm_data = []) {
    $ref = strtolower((string) $referrer);
    $medium = strtolower((string) ($utm_data['utm_medium'] ?? $utm_data['medium'] ?? ''));
    $source = strtolower((string) ($utm_data['utm_source'] ?? $utm_data['source'] ?? ''));
    if (strpos($medium, 'cpc') !== false || strpos($medium, 'paid') !== false || !empty($utm_data['gclid'])) return 'paid_search';
    if (strpos($medium, 'email') !== false) return 'email';
    if (preg_match('/chatgpt|perplexity|copilot|gemini|ai\.google|bard/', $ref . ' ' . $source)) return 'ai_search';
    if (preg_match('/google|bing|yahoo|duckduckgo/', $ref . ' ' . $source)) return 'organic_search';
    if (preg_match('/facebook|instagram|tiktok|linkedin|youtube/', $ref . ' ' . $source)) return strpos($medium, 'paid') !== false ? 'paid_social' : 'social';
    if ($ref === '' && $source === '') return 'direct';
    return $ref !== '' ? 'referral' : 'unknown';
}

function vmo_record_attribution_event($user_id, $event_type, $payload = []) {
    global $wpdb;
    $user_id = (int) $user_id;
    if ($user_id <= 0 || trim((string) $event_type) === '') return false;
    vmo_seo_ensure_growth_tables();
    $payload = is_array($payload) ? $payload : [];
    $utm = is_array($payload['utm'] ?? null) ? $payload['utm'] : [];
    foreach (['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'gclid'] as $key) {
        if (isset($payload[$key]) && !isset($utm[$key])) $utm[$key] = $payload[$key];
    }
    $referrer = esc_url_raw((string) ($payload['referrer'] ?? $payload['referer'] ?? ''));
    $detected_source = vmo_detect_traffic_source($referrer, $utm);
    $source = sanitize_key((string) ($payload['source'] ?? $utm['utm_source'] ?? $detected_source));
    if (in_array($source, ['google', 'bing', 'yahoo', 'duckduckgo'], true)) $source = 'organic_search';
    if ($source === '') $source = $detected_source;
    $medium = sanitize_key((string) ($payload['medium'] ?? $utm['utm_medium'] ?? ($source === 'organic_search' ? 'organic' : '')));
    if ($medium === '') $medium = $source === 'direct' ? 'direct' : 'referral';
    $landing_page = esc_url_raw((string) ($payload['landing_page'] ?? $payload['page_url'] ?? $payload['url'] ?? ''));
    $contact_id = sanitize_text_field((string) ($payload['contact_id'] ?? ''));
    $row = [
        'user_id' => $user_id,
        'visitor_id' => sanitize_text_field((string) ($payload['visitor_id'] ?? $payload['fingerprint'] ?? '')),
        'contact_id' => $contact_id,
        'session_id' => sanitize_text_field((string) ($payload['session_id'] ?? '')),
        'event_type' => sanitize_key((string) $event_type),
        'event_subtype' => sanitize_key((string) ($payload['event_subtype'] ?? $payload['subtype'] ?? '')),
        'source' => $source,
        'medium' => $medium,
        'campaign' => sanitize_text_field((string) ($utm['utm_campaign'] ?? $payload['campaign'] ?? '')),
        'term' => sanitize_text_field((string) ($utm['utm_term'] ?? $payload['term'] ?? '')),
        'content' => sanitize_text_field((string) ($utm['utm_content'] ?? $payload['content'] ?? '')),
        'landing_page' => $landing_page,
        'referrer' => $referrer,
        'gclid' => sanitize_text_field((string) ($utm['gclid'] ?? $payload['gclid'] ?? '')),
        'utm_json' => wp_json_encode($utm),
        'seo_keyword' => sanitize_text_field((string) ($payload['seo_keyword'] ?? $utm['utm_term'] ?? '')),
        'seo_page_id' => absint($payload['seo_page_id'] ?? $payload['page_id'] ?? 0),
        'related_call_id' => absint($payload['related_call_id'] ?? $payload['call_id'] ?? 0),
        'related_form_id' => absint($payload['related_form_id'] ?? $payload['form_id'] ?? 0),
        'related_order_id' => absint($payload['related_order_id'] ?? $payload['order_id'] ?? 0),
        'related_appointment_id' => absint($payload['related_appointment_id'] ?? $payload['appointment_id'] ?? 0),
        'conversion_value' => isset($payload['conversion_value']) ? (float) $payload['conversion_value'] : (isset($payload['amount']) ? (float) $payload['amount'] : 0),
        'created_at' => current_time('mysql'),
    ];
    $ok = vmo_seo_insert_table_row($wpdb->prefix . 'vmo_attribution_events', $row);
    if ($contact_id !== '') vmo_seo_update_contact_touchpoints($user_id, $contact_id, $row);
    return $ok;
}

function vmo_seo_update_contact_touchpoints($user_id, $contact_id, array $event) {
    global $wpdb;
    $contacts_table = $wpdb->prefix . 'vmo_contacts';
    if (!vmo_table_exists($contacts_table)) return false;
    $columns = vmo_seo_table_columns($contacts_table);
    $contact = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$contacts_table} WHERE user_id=%d AND contact_id=%s LIMIT 1", $user_id, $contact_id), ARRAY_A);
    if (!$contact) return false;
    $update = ['last_touch_source' => $event['source'] ?? '', 'last_touch_medium' => $event['medium'] ?? '', 'last_touch_landing_page' => $event['landing_page'] ?? '', 'last_touch_at' => current_time('mysql')];
    if (empty($contact['first_touch_source'])) {
        $update['first_touch_source'] = $event['source'] ?? '';
        $update['first_touch_medium'] = $event['medium'] ?? '';
        $update['first_touch_landing_page'] = $event['landing_page'] ?? '';
        $update['first_touch_at'] = current_time('mysql');
    }
    $update = array_intersect_key($update, array_flip($columns));
    return !empty($update) ? (bool) $wpdb->update($contacts_table, $update, ['id' => (int) $contact['id'], 'user_id' => $user_id]) : false;
}

function vmo_seo_record_contact_attribution($user_id, $contact_id, $data) {
    $data = is_array($data) ? $data : [];
    $source = sanitize_key((string) ($data['source'] ?? 'crm_contact'));
    $event_type = in_array($source, ['website_form', 'form', 'lead_form'], true) ? 'form_submission' : 'crm_lead';
    $acquisition = $data['acquisition_details'] ?? [];
    if (is_string($acquisition)) $acquisition = json_decode($acquisition, true) ?: [];
    vmo_record_attribution_event($user_id, $event_type, array_merge((array) $acquisition, $data, ['contact_id' => $contact_id, 'source' => $source]));
}

function vmo_seo_record_checkout_attribution($order_id, $session_data) {
    global $wpdb;
    $orders = $wpdb->prefix . 'vmo_store_orders';
    $order = vmo_table_exists($orders) && $order_id ? $wpdb->get_row($wpdb->prepare("SELECT * FROM {$orders} WHERE id=%d", (int) $order_id), ARRAY_A) : [];
    $user_id = (int) ($order['user_id'] ?? $session_data['user_id'] ?? 0);
    if ($user_id <= 0) return;
    $metadata = json_decode((string) ($order['metadata'] ?? '{}'), true);
    if (!is_array($metadata)) $metadata = [];
    vmo_record_attribution_event($user_id, 'purchase', array_merge($metadata, (array) $session_data, ['related_order_id' => (int) $order_id, 'conversion_value' => isset($order['amount_total']) ? ((float) $order['amount_total'] / 100) : 0, 'contact_id' => (string) ($order['contact_id'] ?? '')]));
}

function vmo_seo_record_sms_attribution($user_id, $from, $body, $message_sid) {
    vmo_record_attribution_event((int) $user_id, 'sms_message', ['source' => 'sms', 'medium' => 'sms', 'visitor_id' => sanitize_text_field((string) $from), 'event_subtype' => 'inbound', 'content' => substr(sanitize_text_field((string) $body), 0, 240), 'session_id' => sanitize_text_field((string) $message_sid)]);
}

function vmo_seo_get_business_outcomes($user_id) {
    global $wpdb;
    vmo_seo_ensure_growth_tables();
    $table = $wpdb->prefix . 'vmo_attribution_events';
    $organic_where = "user_id=%d AND (source IN ('organic_search','ai_search') OR medium='organic')";
    $organic_visits = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$table} WHERE {$organic_where} AND event_type IN ('website_visit','landing_page_view')", $user_id));
    $organic_leads = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$table} WHERE {$organic_where} AND event_type IN ('form_submission','live_chat','email_signup')", $user_id));
    $organic_calls = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$table} WHERE {$organic_where} AND event_type='phone_call'", $user_id));
    $organic_appointments = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$table} WHERE {$organic_where} AND event_type='appointment_booking'", $user_id));
    $organic_sales = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$table} WHERE {$organic_where} AND event_type IN ('checkout','purchase','repeat_purchase')", $user_id));
    $organic_revenue = (float) $wpdb->get_var($wpdb->prepare("SELECT COALESCE(SUM(conversion_value),0) FROM {$table} WHERE {$organic_where} AND event_type IN ('purchase','repeat_purchase')", $user_id));
    $top_pages = $wpdb->get_results($wpdb->prepare("SELECT landing_page, COUNT(*) AS conversions, COALESCE(SUM(conversion_value),0) AS revenue FROM {$table} WHERE {$organic_where} AND event_type IN ('form_submission','phone_call','appointment_booking','purchase','repeat_purchase') GROUP BY landing_page ORDER BY conversions DESC LIMIT 5", $user_id), ARRAY_A) ?: [];
    $conversions = $organic_leads + $organic_calls + $organic_appointments + $organic_sales;
    return ['organic_visits' => $organic_visits, 'organic_leads' => $organic_leads, 'organic_calls' => $organic_calls, 'organic_appointments' => $organic_appointments, 'organic_sales' => $organic_sales, 'organic_revenue' => round($organic_revenue, 2), 'organic_conversion_rate' => $organic_visits > 0 ? round(($conversions / $organic_visits) * 100, 1) : 0, 'top_converting_seo_pages' => $top_pages, 'source' => 'VMO attribution events'];
}

function vmo_seo_collect_customer_language_texts($user_id, $days = 30) {
    global $wpdb;
    $since = date('Y-m-d H:i:s', strtotime('-' . max(1, (int) $days) . ' days'));
    $texts = [];
    $candidate_tables = [
        $wpdb->prefix . 'vmo_phone_calls' => ['transcript', 'summary', 'call_summary', 'notes', 'call_sentiment', 'intent'],
        $wpdb->prefix . 'vmo_contacts' => ['notes', 'acquisition_details', 'categorization_analysis', 'engagement_strategy'],
        $wpdb->prefix . 'vmo_contact_interactions' => ['subject', 'summary', 'full_details', 'ai_analysis', 'metadata'],
        $wpdb->prefix . 'vmo_email_auto_reply_queue' => ['trigger_value', 'action_data'],
        $wpdb->prefix . 'vmo_sms_campaign_recipients' => ['error_message', 'contact_name'],
        $wpdb->prefix . 'vmo_tickets' => ['subject', 'message', 'description', 'notes'],
        $wpdb->prefix . 'vmo_bookings' => ['notes', 'service', 'customer_name'],
    ];
    foreach ($candidate_tables as $table => $text_columns) {
        if (!vmo_table_exists($table)) continue;
        $columns = vmo_seo_table_columns($table);
        $date_column = in_array('created_at', $columns, true) ? 'created_at' : (in_array('call_time', $columns, true) ? 'call_time' : (in_array('updated_at', $columns, true) ? 'updated_at' : ''));
        $available_text_columns = array_values(array_intersect($text_columns, $columns));
        if (empty($available_text_columns) || !in_array('user_id', $columns, true)) continue;
        $where = "user_id=%d";
        $args = [$user_id];
        if ($date_column !== '') { $where .= " AND {$date_column} >= %s"; $args[] = $since; }
        $rows = $wpdb->get_results($wpdb->prepare("SELECT " . implode(',', $available_text_columns) . " FROM {$table} WHERE {$where} LIMIT 120", $args), ARRAY_A) ?: [];
        foreach ($rows as $row) {
            foreach ($available_text_columns as $column) {
                $value = wp_strip_all_tags((string) ($row[$column] ?? ''));
                if (trim($value) !== '') $texts[] = $value;
            }
        }
    }
    return array_values(array_filter(array_unique($texts)));
}

function vmo_seo_ai_cluster_customer_language($user_id, array $texts) {
    $cached = get_user_meta($user_id, 'vmo_seo_customer_language_ai_cluster', true);
    if (is_array($cached) && !empty($cached['generated_at']) && strtotime((string) $cached['generated_at']) > strtotime('-24 hours')) return $cached;
    $api_key = (string) get_option('vmo_openai_api_key', '');
    if ($api_key === '' || empty($texts) || !vmo_seo_has_token_balance($user_id, 'VMO_TOKENS_SEO_TRANSCRIPT_MINING')) return [];
    $sample = substr(implode("\n---\n", array_slice($texts, 0, 80)), 0, 12000);
    $model = function_exists('vmo_get_model') ? vmo_get_model('mini') : 'gpt-5.5-mini';
    $prompt = "Cluster customer language for SEO. Extract repeated questions, buyer objections, pricing concerns, urgency language, local phrases, service requests, competitor mentions, FAQ ideas, blog topics, service page opportunities, and exact wording customers use. Return JSON only with keys: repeated_questions, buyer_objections, pricing_concerns, urgency_language, local_terms, service_requests, competitor_mentions, faq_ideas, blog_topics, service_page_opportunities, customer_language_keywords.\n\nTEXTS:\n" . $sample;
    $body = array_merge(['model' => $model, 'messages' => [['role' => 'system', 'content' => 'You turn real customer conversations into SEO and AI-search growth opportunities.'], ['role' => 'user', 'content' => $prompt]], 'temperature' => 0.25], function_exists('vmo_get_token_param') ? vmo_get_token_param($model, 1800) : ['max_completion_tokens' => 1800]);
    $response = wp_remote_post('https://api.openai.com/v1/chat/completions', ['headers' => ['Authorization' => 'Bearer ' . $api_key, 'Content-Type' => 'application/json'], 'body' => wp_json_encode($body), 'timeout' => 35]);
    if (is_wp_error($response)) return [];
    $raw = json_decode((string) wp_remote_retrieve_body($response), true);
    $content = (string) ($raw['choices'][0]['message']['content'] ?? '');
    $parsed = function_exists('vmo_extract_json_object_from_text') ? vmo_extract_json_object_from_text($content) : json_decode($content, true);
    if (!is_array($parsed)) return [];
    $parsed['generated_at'] = current_time('mysql');
    $parsed['source'] = 'AI customer-language clustering';
    update_user_meta($user_id, 'vmo_seo_customer_language_ai_cluster', $parsed);
    vmo_seo_charge_tokens($user_id, 'VMO_TOKENS_SEO_TRANSCRIPT_MINING', 'seo_transcript_mining', 'AI clustered customer language for SEO recommendations');
    return $parsed;
}

function vmo_seo_mine_customer_language($user_id, $days = 30) {
    $texts = vmo_seo_collect_customer_language_texts($user_id, $days);
    $blob = strtolower(implode("\n", array_filter(array_map('wp_strip_all_tags', $texts))));
    preg_match_all('/\b(?:do you|can you|how much|how soon|emergency|weekend|near me|pricing|quote|appointment|delivery|shipping|refund|warranty)[^\n\.?]{0,120}/i', $blob, $matches);
    $phrases = array_values(array_unique(array_slice(array_map('trim', $matches[0] ?? []), 0, 20)));
    $ai = vmo_seo_ai_cluster_customer_language($user_id, $texts);
    $result = ['top_questions' => $phrases, 'seo_faq_opportunities' => $phrases, 'new_blog_topics' => array_map(static fn($p) => 'Answer customer question: ' . $p, array_slice($phrases, 0, 5)), 'new_service_pages' => [], 'local_search_phrases' => array_values(array_filter($phrases, static fn($p) => strpos($p, 'near me') !== false || strpos($p, 'local') !== false)), 'customer_language_keywords' => $phrases, 'conversion_objections' => array_values(array_filter($phrases, static fn($p) => preg_match('/price|cost|how much|refund|warranty/', $p))), 'texts_scanned' => count($texts), 'source' => empty($ai) ? 'lightweight regex mining' : 'AI clustering plus lightweight regex'];
    if (!empty($ai)) {
        $result = array_merge($result, [
            'repeated_questions' => (array) ($ai['repeated_questions'] ?? $phrases),
            'buyer_objections' => (array) ($ai['buyer_objections'] ?? []),
            'pricing_concerns' => (array) ($ai['pricing_concerns'] ?? []),
            'urgency_language' => (array) ($ai['urgency_language'] ?? []),
            'local_terms' => (array) ($ai['local_terms'] ?? []),
            'service_requests' => (array) ($ai['service_requests'] ?? []),
            'competitor_mentions' => (array) ($ai['competitor_mentions'] ?? []),
            'faq_ideas' => (array) ($ai['faq_ideas'] ?? []),
            'blog_topics' => (array) ($ai['blog_topics'] ?? []),
            'service_page_opportunities' => (array) ($ai['service_page_opportunities'] ?? []),
            'customer_language_keywords' => array_values(array_unique(array_merge($phrases, (array) ($ai['customer_language_keywords'] ?? [])))),
        ]);
    }
    update_user_meta($user_id, 'vmo_seo_customer_language', $result);
    return $result;
}

function vmo_seo_deep_technical_audit($user_id) {
    $history = get_user_meta($user_id, 'vmo_seo_technical_audit_history', true);
    if (is_array($history) && !empty($history)) {
        $latest = end($history);
        if (is_array($latest) && !empty($latest['date']) && strtotime((string) $latest['date']) > strtotime('-6 hours')) return $latest;
    }
    $posts = get_posts(['author' => $user_id, 'post_type' => ['post', 'page'], 'post_status' => 'publish', 'numberposts' => 50]);
    if (empty($posts)) $posts = get_posts(['post_type' => ['post', 'page'], 'post_status' => 'publish', 'numberposts' => 50]);
    $issues = [];
    $title_map = [];
    $description_map = [];
    $linked_internal = [];
    $known_urls = [];
    $links_to_check = [];
    foreach ($posts as $post) {
        $content = (string) $post->post_content;
        $url = get_permalink($post);
        $known_urls[] = trailingslashit($url);
        $title = get_post_meta($post->ID, 'vmo_seo_title', true) ?: get_the_title($post);
        $description = get_post_meta($post->ID, 'vmo_seo_description', true) ?: get_post_meta($post->ID, '_vmo_seo_description', true) ?: $post->post_excerpt;
        $title_map[strtolower(trim((string) $title))][] = $url;
        if (trim((string) $description) !== '') $description_map[strtolower(trim((string) $description))][] = $url;
        $h1_count = preg_match_all('/<h1\b/i', $content);
        if ($h1_count === 0) $issues[] = ['issue' => 'Missing H1', 'page' => $url, 'severity' => 'high', 'category' => 'heading', 'auto_fix_available' => true, 'recommended_fix' => 'Add one clear H1 using the primary service keyword.'];
        if ($h1_count > 1) $issues[] = ['issue' => 'Multiple H1s', 'page' => $url, 'severity' => 'medium', 'category' => 'heading', 'auto_fix_available' => false, 'recommended_fix' => 'Keep one H1 and convert extra H1s to H2/H3.'];
        if (str_word_count(wp_strip_all_tags($content)) < 300) $issues[] = ['issue' => 'Thin content', 'page' => $url, 'severity' => 'medium', 'category' => 'content', 'auto_fix_available' => false, 'recommended_fix' => 'Expand the page with buyer questions, service details, FAQs, and next steps.'];
        if (preg_match_all('/<img\b(?![^>]*\salt=)/i', $content) > 0) $issues[] = ['issue' => 'Missing image alt text', 'page' => $url, 'severity' => 'medium', 'category' => 'images', 'auto_fix_available' => true, 'recommended_fix' => 'Add descriptive alt text based on image context.'];
        if (preg_match_all('/<img\b(?![^>]*\sloading=)/i', $content) > 0) $issues[] = ['issue' => 'Images missing lazy loading', 'page' => $url, 'severity' => 'low', 'category' => 'images', 'auto_fix_available' => true, 'recommended_fix' => 'Add loading="lazy" to non-critical images.'];
        if (preg_match('/http:\/\//i', $content) && is_ssl()) $issues[] = ['issue' => 'Possible mixed content', 'page' => $url, 'severity' => 'high', 'category' => 'security', 'auto_fix_available' => false, 'recommended_fix' => 'Replace embedded HTTP assets with HTTPS URLs.'];
        if (preg_match('/<meta[^>]+name=["\']robots["\'][^>]+noindex/i', $content) || get_post_meta($post->ID, '_yoast_wpseo_meta-robots-noindex', true) === '1') $issues[] = ['issue' => 'Noindex directive detected', 'page' => $url, 'severity' => 'high', 'category' => 'crawlability', 'auto_fix_available' => false, 'recommended_fix' => 'Confirm whether this page should be excluded from search.'];
        if (preg_match('/rel=["\']canonical["\'][^>]+href=["\']([^"\']+)/i', $content, $canonical) && !empty($canonical[1]) && trailingslashit(esc_url_raw($canonical[1])) !== trailingslashit($url)) $issues[] = ['issue' => 'Canonical points elsewhere', 'page' => $url, 'severity' => 'medium', 'category' => 'canonical', 'auto_fix_available' => false, 'recommended_fix' => 'Verify canonical target or remove conflicting canonical markup.'];
        if (!preg_match('/application\/ld\+json|itemscope|schema\.org/i', $content . ' ' . get_post_meta($post->ID, 'vmo_seo_json_ld', true))) $issues[] = ['issue' => 'Schema markup missing', 'page' => $url, 'severity' => 'medium', 'category' => 'schema', 'auto_fix_available' => true, 'recommended_fix' => 'Attach relevant LocalBusiness, Service, Product, FAQ, or Article schema.'];
        preg_match_all('/<a[^>]+href=["\']([^"\']+)["\']/i', $content, $link_matches);
        foreach (array_slice((array) ($link_matches[1] ?? []), 0, 20) as $href) {
            $href = esc_url_raw(html_entity_decode((string) $href, ENT_QUOTES, 'UTF-8'));
            if ($href === '' || strpos($href, 'mailto:') === 0 || strpos($href, 'tel:') === 0 || strpos($href, '#') === 0) continue;
            $absolute = strpos($href, 'http') === 0 ? $href : home_url($href);
            if (strpos($absolute, home_url()) === 0) $linked_internal[] = trailingslashit($absolute);
            $links_to_check[$absolute] = $url;
        }
    }
    foreach ($title_map as $title => $urls) if ($title !== '' && count($urls) > 1) $issues[] = ['issue' => 'Duplicate title', 'page' => implode(', ', array_slice($urls, 0, 3)), 'severity' => 'medium', 'category' => 'metadata', 'auto_fix_available' => true, 'recommended_fix' => 'Create unique SEO titles for each page.'];
    foreach ($description_map as $description => $urls) if ($description !== '' && count($urls) > 1) $issues[] = ['issue' => 'Duplicate meta description', 'page' => implode(', ', array_slice($urls, 0, 3)), 'severity' => 'medium', 'category' => 'metadata', 'auto_fix_available' => true, 'recommended_fix' => 'Create unique search descriptions for each page.'];
    $orphans = array_diff(array_map('trailingslashit', $known_urls), array_unique($linked_internal));
    foreach (array_slice($orphans, 0, 8) as $orphan) $issues[] = ['issue' => 'Potential orphan page', 'page' => $orphan, 'severity' => 'medium', 'category' => 'internal_linking', 'auto_fix_available' => false, 'recommended_fix' => 'Add contextual internal links from relevant pages.'];
    $checked = 0;
    foreach ($links_to_check as $target => $source_page) {
        if (++$checked > 25) break;
        $response = wp_remote_head($target, ['timeout' => 6, 'redirection' => 0]);
        if (is_wp_error($response)) { $issues[] = ['issue' => 'Broken link check failed', 'page' => $source_page, 'severity' => 'low', 'category' => 'links', 'auto_fix_available' => false, 'recommended_fix' => 'Manually verify link: ' . $target]; continue; }
        $code = (int) wp_remote_retrieve_response_code($response);
        if ($code >= 400) $issues[] = ['issue' => $code === 404 ? '404 broken link' : 'Broken link HTTP ' . $code, 'page' => $source_page, 'severity' => 'high', 'category' => 'links', 'auto_fix_available' => true, 'recommended_fix' => 'Update or remove this link: ' . $target];
        if ($code >= 300 && $code < 400) $issues[] = ['issue' => 'Redirecting link', 'page' => $source_page, 'severity' => 'low', 'category' => 'redirects', 'auto_fix_available' => true, 'recommended_fix' => 'Update the link to its final destination: ' . $target];
    }
    $robots_response = wp_remote_get(home_url('/robots.txt'), ['timeout' => 6]);
    if (!is_wp_error($robots_response)) {
        $robots = strtolower((string) wp_remote_retrieve_body($robots_response));
        if (strpos($robots, "disallow: /") !== false) $issues[] = ['issue' => 'Robots.txt may block crawling', 'page' => home_url('/robots.txt'), 'severity' => 'critical', 'category' => 'crawlability', 'auto_fix_available' => false, 'recommended_fix' => 'Review robots.txt before launch.'];
    }
    $sitemap_response = wp_remote_head(home_url('/sitemap.xml'), ['timeout' => 6]);
    $sitemap_code = is_wp_error($sitemap_response) ? 0 : (int) wp_remote_retrieve_response_code($sitemap_response);
    if ($sitemap_code >= 400 || $sitemap_code === 0) $issues[] = ['issue' => 'Sitemap unavailable', 'page' => home_url('/sitemap.xml'), 'severity' => 'high', 'category' => 'sitemap', 'auto_fix_available' => false, 'recommended_fix' => 'Generate and submit an XML sitemap.'];
    $home_response = wp_remote_get(home_url('/'), ['timeout' => 8]);
    if (!is_wp_error($home_response)) {
        $home_html = (string) wp_remote_retrieve_body($home_response);
        if (!preg_match('/<meta[^>]+name=["\']viewport["\']/i', $home_html)) $issues[] = ['issue' => 'Mobile viewport missing', 'page' => home_url('/'), 'severity' => 'high', 'category' => 'mobile', 'auto_fix_available' => true, 'recommended_fix' => 'Add responsive viewport meta tag.'];
        $bytes = strlen($home_html);
        if ($bytes > 500000) $issues[] = ['issue' => 'Large HTML payload', 'page' => home_url('/'), 'severity' => 'medium', 'category' => 'performance', 'auto_fix_available' => false, 'recommended_fix' => 'Reduce page payload and defer non-critical scripts.'];
    }
    if (!is_array($history)) $history = [];
    $snapshot = ['date' => current_time('mysql'), 'issues' => $issues, 'issue_count' => count($issues), 'checks' => ['broken_links', '404s', 'redirects', 'canonical_conflicts', 'duplicate_titles', 'duplicate_meta_descriptions', 'noindex_pages', 'orphan_pages', 'sitemap_validation', 'robots_txt_blocking', 'schema_validation', 'oversized_images_proxy', 'lazy_loading', 'https_mixed_content', 'mobile_viewport', 'core_web_vitals_proxy'], 'source' => 'VMO deep technical audit'];
    $history[] = $snapshot;
    update_user_meta($user_id, 'vmo_seo_technical_audit_history', array_slice($history, -12));
    return $snapshot;
}

function vmo_seo_competitor_names($user_id) {
    $analysis = get_user_meta($user_id, 'vmo_latest_competitive_analysis', true);
    $names = [];
    foreach ((array) ($analysis['competitors'] ?? $analysis['competitors_identified'] ?? []) as $competitor) {
        if (is_array($competitor)) $names[] = sanitize_text_field((string) ($competitor['name'] ?? $competitor['business_name'] ?? $competitor['domain'] ?? ''));
        else $names[] = sanitize_text_field((string) $competitor);
    }
    return array_values(array_filter(array_unique($names)));
}

function vmo_seo_discover_competitor_pages($user_id, array $competitors = []) {
    $profile = function_exists('vmo_get_consultation_profile') ? vmo_get_consultation_profile($user_id) : [];
    $services = vmo_seo_value_as_list($profile['services'] ?? $profile['products_services'] ?? $profile['products'] ?? []);
    $location = vmo_seo_profile_value((array) $profile, ['city', 'location', 'service_area'], '');
    $items = [];
    foreach (array_slice($competitors, 0, 5) as $competitor) {
        foreach (array_slice($services ?: ['services'], 0, 4) as $service) {
            $query = $competitor . ' ' . $service . ' ' . $location;
            $url = vmo_seo_discover_url_from_search($query, [wp_parse_url(home_url('/'), PHP_URL_HOST)]);
            if ($url) $items[] = ['competitor_name' => $competitor, 'topic' => $service, 'url' => $url, 'source_type' => 'specific_discovered_url', 'confidence' => 'medium'];
        }
        $faq_url = vmo_seo_discover_url_from_search($competitor . ' FAQ questions services ' . $location, [wp_parse_url(home_url('/'), PHP_URL_HOST)]);
        if ($faq_url) $items[] = ['competitor_name' => $competitor, 'topic' => 'FAQ structure', 'url' => $faq_url, 'source_type' => 'specific_discovered_url', 'confidence' => 'medium'];
    }
    return array_values(array_unique($items, SORT_REGULAR));
}

function vmo_seo_discover_competitor_authority_signals($user_id, array $competitors = []) {
    $signals = [];
    foreach (array_slice($competitors, 0, 5) as $competitor) {
        foreach (['directory listing', 'chamber of commerce', 'podcast interview', 'press release', 'reviews'] as $signal) {
            $url = vmo_seo_discover_url_from_search('"' . $competitor . '" ' . $signal, [wp_parse_url(home_url('/'), PHP_URL_HOST)]);
            if ($url) $signals[] = ['competitor_name' => $competitor, 'signal' => $signal, 'url' => $url, 'source_type' => 'specific_discovered_url', 'confidence' => 'medium'];
        }
    }
    return array_values(array_unique($signals, SORT_REGULAR));
}

function vmo_seo_compare_competitor_content_footprint($user_id, array $competitor_pages) {
    $user_pages = get_posts(['post_type' => ['post', 'page'], 'post_status' => 'publish', 'numberposts' => 200, 'fields' => 'ids']);
    $user_blob = strtolower(implode(' ', array_map(static fn($id) => get_the_title($id) . ' ' . wp_strip_all_tags((string) get_post_field('post_content', $id)), $user_pages)));
    $gaps = [];
    foreach ($competitor_pages as $page) {
        $topic = strtolower((string) ($page['topic'] ?? ''));
        if ($topic !== '' && strpos($user_blob, $topic) === false) {
            $gaps[] = ['type' => 'Create page', 'competitor_name' => $page['competitor_name'] ?? '', 'what_competitor_has' => $page['url'] ?? '', 'what_user_lacks' => 'Published content for ' . $topic, 'recommended_action' => 'Create a ranking-oriented page or FAQ section for ' . $topic . '.', 'expected_impact' => 'high', 'source_url' => $page['url'] ?? '', 'approval_required' => true];
        }
    }
    return $gaps;
}

function vmo_seo_compare_competitor_local_visibility($user_id, array $authority_signals) {
    $authority_tasks = vmo_seo_get_authority_task_rows($user_id);
    $completed_links = array_map(static fn($task) => strtolower((string) ($task['suggested_link'] ?? '')), array_filter($authority_tasks, static fn($task) => ($task['status'] ?? '') === 'completed'));
    $gaps = [];
    foreach ($authority_signals as $signal) {
        $url = strtolower((string) ($signal['url'] ?? ''));
        $covered = false;
        foreach ($completed_links as $done) if ($done !== '' && ($url === $done || strpos($url, wp_parse_url($done, PHP_URL_HOST) ?: '___') !== false)) { $covered = true; break; }
        if (!$covered) $gaps[] = ['type' => 'Pursue authority signal', 'competitor_name' => $signal['competitor_name'] ?? '', 'what_competitor_has' => $signal['signal'] ?? 'authority mention', 'what_user_lacks' => $signal['url'] ?? '', 'recommended_action' => 'Apply, pitch, or create a profile where the competitor has a visible authority signal.', 'expected_impact' => in_array(($signal['signal'] ?? ''), ['reviews', 'chamber of commerce'], true) ? 'high' : 'medium', 'source_url' => $signal['url'] ?? '', 'approval_required' => true];
    }
    return $gaps;
}

function vmo_seo_competitor_gap_engine($user_id) {
    $cached = get_user_meta($user_id, 'vmo_seo_competitor_gaps', true);
    $discovery_cache = get_user_meta($user_id, 'vmo_seo_competitor_discovery', true);
    if (is_array($cached) && is_array($discovery_cache) && !empty($discovery_cache['generated_at']) && strtotime((string) $discovery_cache['generated_at']) > strtotime('-24 hours')) return $cached;
    $analysis = get_user_meta($user_id, 'vmo_latest_competitive_analysis', true);
    $competitors = vmo_seo_competitor_names($user_id);
    $can_run_discovery = vmo_seo_has_token_balance($user_id, 'VMO_TOKENS_SEO_COMPETITOR_GAP_SCAN');
    $competitor_pages = $can_run_discovery ? vmo_seo_discover_competitor_pages($user_id, $competitors) : [];
    $authority_signals = $can_run_discovery ? vmo_seo_discover_competitor_authority_signals($user_id, $competitors) : [];
    $gaps = [];
    $gaps = array_merge($gaps, vmo_seo_compare_competitor_content_footprint($user_id, $competitor_pages));
    $gaps = array_merge($gaps, vmo_seo_compare_competitor_local_visibility($user_id, $authority_signals));
    foreach ((array) ($analysis['content_gaps'] ?? []) as $gap) $gaps[] = ['type' => 'Create page', 'competitor_name' => '', 'what_competitor_has' => sanitize_text_field((string) $gap), 'what_user_lacks' => 'Equivalent content asset', 'recommended_action' => 'Create or strengthen a page covering this gap.', 'expected_impact' => 'medium'];
    foreach (vmo_seo_get_authority_task_rows($user_id) as $task) if (($task['status'] ?? '') !== 'completed') $gaps[] = ['type' => 'Pursue directory listing', 'competitor_name' => (string) ($task['related_competitor'] ?? ''), 'what_competitor_has' => (string) ($task['type'] ?? 'authority signal'), 'what_user_lacks' => (string) ($task['title'] ?? ''), 'recommended_action' => 'Complete the authority growth task.', 'expected_impact' => (string) ($task['impact'] ?? 'medium')];
    $gaps = array_slice(array_values(array_unique($gaps, SORT_REGULAR)), 0, 50);
    update_user_meta($user_id, 'vmo_seo_competitor_discovery', ['competitor_pages' => $competitor_pages, 'authority_signals' => $authority_signals, 'generated_at' => current_time('mysql')]);
    update_user_meta($user_id, 'vmo_seo_competitor_gaps', $gaps);
    if ($can_run_discovery && (!empty($competitor_pages) || !empty($authority_signals))) vmo_seo_charge_tokens($user_id, 'VMO_TOKENS_SEO_COMPETITOR_GAP_SCAN', 'seo_competitor_gap', 'Discovered competitor SEO gaps and authority signals');
    return $gaps;
}

function vmo_seo_content_quality_gate($post_id, $content, $target_keyword, $kind) {
    $plain = wp_strip_all_tags((string) $content);
    $word_count = str_word_count($plain);
    $min = $kind === 'blog_post' ? 900 : ($kind === 'page' ? 700 : 500);
    $checks = ['no_placeholder_language' => !vmo_seo_content_has_placeholder_language($content), 'minimum_word_count' => $word_count >= $min, 'one_h1_max' => preg_match_all('/<h1\b/i', (string) $content) <= 1, 'heading_structure' => preg_match('/<h2\b/i', (string) $content), 'keyword_included' => $target_keyword === '' || stripos($plain, (string) $target_keyword) !== false, 'cta_included' => preg_match('/contact|call|book|schedule|get started|buy|shop/i', $plain), 'faq_or_support_included' => preg_match('/faq|question|asked|answer/i', $plain), 'schema_expected' => true, 'no_fake_statistics' => !preg_match('/\b\d{2,3}%\b|\b\d+x\b/i', $plain)];
    $passed = !in_array(false, array_map('boolval', $checks), true);
    return ['passed' => $passed, 'checks' => $checks, 'word_count' => $word_count, 'minimum_word_count' => $min, 'message' => $passed ? 'Content quality gate passed.' : 'Draft created but held for review because quality gate did not pass.'];
}

function vmo_seo_ai_quality_review($user_id, $content, $target_keyword, $kind) {
    $api_key = (string) get_option('vmo_openai_api_key', '');
    if ($api_key === '' || !vmo_seo_has_token_balance($user_id, 'VMO_TOKENS_SEO_AI_QUALITY_REVIEW')) return ['used_ai' => false, 'passed' => true, 'message' => 'AI quality review skipped; using mechanical gate only.'];
    $plain = substr(wp_strip_all_tags((string) $content), 0, 10000);
    $model = function_exists('vmo_get_model') ? vmo_get_model('mini') : 'gpt-5.5-mini';
    $prompt = "Review this SEO content before publishing. Judge usefulness, factual caution, duplicate/thin feel, natural keyword use, local relevance, conversion strength, search intent match, and schema/FAQ suitability. Return JSON only: {\"passed\":true|false,\"score\":0-100,\"reasons\":[...],\"required_fixes\":[...]}.\nTYPE: {$kind}\nTARGET KEYWORD: {$target_keyword}\nCONTENT:\n{$plain}";
    $body = array_merge(['model' => $model, 'messages' => [['role' => 'system', 'content' => 'You are a strict SEO editor reviewing content before publication.'], ['role' => 'user', 'content' => $prompt]], 'temperature' => 0.2], function_exists('vmo_get_token_param') ? vmo_get_token_param($model, 1200) : ['max_completion_tokens' => 1200]);
    $response = wp_remote_post('https://api.openai.com/v1/chat/completions', ['headers' => ['Authorization' => 'Bearer ' . $api_key, 'Content-Type' => 'application/json'], 'body' => wp_json_encode($body), 'timeout' => 35]);
    if (is_wp_error($response)) return ['used_ai' => false, 'passed' => true, 'message' => 'AI quality review unavailable; using mechanical gate only.'];
    $raw = json_decode((string) wp_remote_retrieve_body($response), true);
    $text = (string) ($raw['choices'][0]['message']['content'] ?? '');
    $parsed = function_exists('vmo_extract_json_object_from_text') ? vmo_extract_json_object_from_text($text) : json_decode($text, true);
    if (!is_array($parsed)) return ['used_ai' => false, 'passed' => true, 'message' => 'AI quality review returned unreadable output; using mechanical gate only.'];
    vmo_seo_charge_tokens($user_id, 'VMO_TOKENS_SEO_AI_QUALITY_REVIEW', 'seo_ai_quality_review', 'AI reviewed generated SEO content before publication');
    return ['used_ai' => true, 'passed' => !empty($parsed['passed']) && (int) ($parsed['score'] ?? 0) >= 75, 'score' => (int) ($parsed['score'] ?? 0), 'reasons' => (array) ($parsed['reasons'] ?? []), 'required_fixes' => (array) ($parsed['required_fixes'] ?? []), 'message' => !empty($parsed['passed']) ? 'AI quality review passed.' : 'Draft created but held for review because AI quality review did not pass.'];
}

function vmo_seo_get_growth_modules() {
    vmo_seo_ajax_guard();
    $user_id = get_current_user_id();
    $profile = function_exists('vmo_get_consultation_profile') ? vmo_get_consultation_profile($user_id) : [];
    $authority = vmo_seo_get_authority_task_rows($user_id);
    $local = vmo_seo_get_local_visibility_checklist($user_id);
    $review = vmo_seo_review_growth_dashboard();
    $ai_search = vmo_seo_ai_search_audit($user_id);
    $technical = vmo_seo_deep_technical_audit($user_id);
    $competitor_gaps = vmo_seo_competitor_gap_engine($user_id);
    $customer_language = vmo_seo_mine_customer_language($user_id, 30);
    $gsc = vmo_gsc_get_latest_metrics($user_id);
    wp_send_json_success(['authority_growth' => ['items' => $authority, 'nap' => vmo_seo_business_nap($user_id, (array) $profile)], 'local_visibility' => $local, 'review_growth' => $review, 'ai_search_readiness' => $ai_search, 'technical_audit' => $technical, 'competitor_gaps' => $competitor_gaps, 'customer_language' => $customer_language, 'gsc' => $gsc, 'token_policy' => vmo_seo_token_policy()]);
}

function vmo_seo_token_policy() {
    return [
        'lightweight_checks_free' => ['local_visibility_checklist', 'mechanical_content_quality_gate', 'technical_audit_read', 'review_task_drafts'],
        'token_backed_operations' => [
            'seo_recommendation' => VMO_TOKENS_SEO_RECOMMENDATION_REGEN,
            'seo_content_generation' => VMO_TOKENS_SEO_CONTENT_GENERATION,
            'seo_authority' => VMO_TOKENS_SEO_AUTHORITY_SCAN,
            'seo_ai_search' => VMO_TOKENS_SEO_AI_SEARCH_AUDIT,
            'seo_review_growth' => VMO_TOKENS_SEO_REVIEW_MESSAGE,
            'seo_transcript_mining' => VMO_TOKENS_SEO_TRANSCRIPT_MINING,
            'seo_competitor_gap' => VMO_TOKENS_SEO_COMPETITOR_GAP_SCAN,
            'seo_ai_quality_review' => VMO_TOKENS_SEO_AI_QUALITY_REVIEW,
        ],
        'fallback_rule' => 'Do not deduct for non-AI/template fallback output.',
    ];
}

/* --------------------------------------------------------------
   Strategic Dashboard: SEO + CRM + Tax endpoints
---------------------------------------------------------------- */

function vmo_get_seo_overview() {
    if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['nonce'] ?? $_REQUEST['security'] ?? '')), 'vmo_nonce')) { wp_send_json_error(['message' => 'Security check failed'], 403); }
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if ((isset($_POST['security']) || isset($_POST['nonce'])) && !vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Invalid nonce', 'status' => 'invalid_nonce'], 403);
    }

    $user_id = get_current_user_id();
    $consultation_profile = function_exists('vmo_get_consultation_profile') ? vmo_get_consultation_profile($user_id) : [];
    if (empty($consultation_profile)) {
        wp_send_json_success([
            'score' => '—',
            'pages_indexed' => '—',
            'errors' => '—',
            'warnings' => '—',
            'keyword_trend' => [],
            'levels' => [],
            'domain_authority' => '—',
            'keyword_opportunity' => '—',
            'metric_sources' => [],
            'metric_labels' => [],
            'status' => 'consultation_pending',
            'message' => 'SEO analysis available after consultation completion'
        ]);
    }

    $website_analysis = vmo_analyze_website_seo($user_id, $consultation_profile);
    $score = isset($website_analysis['overall_score']) ? (int) $website_analysis['overall_score'] : 0;
    $kw = isset($website_analysis['keyword_opportunity']) ? (int) $website_analysis['keyword_opportunity'] : 0;

    // Build keyword trend from real weekly content counts (last 10 weeks)
    $keyword_trend = [];
    global $wpdb;
    $cumulative = 0;
    for ($w = 9; $w >= 0; $w--) {
        $week_start = date('Y-m-d', strtotime("-{$w} weeks Monday"));
        $week_end   = date('Y-m-d', strtotime("-{$w} weeks Sunday"));
        $count = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$wpdb->posts}
             WHERE post_author = %d AND post_status = 'publish'
             AND post_date BETWEEN %s AND %s",
            $user_id, $week_start . ' 00:00:00', $week_end . ' 23:59:59'
        ));
        $cumulative += $count;
        // Scale: each published page adds ~10 to keyword opportunity score (capped at 100)
        $keyword_trend[] = min(100, $cumulative * 10);
    }
    // If no content exists, fall back to the computed keyword opportunity score
    if (array_sum($keyword_trend) === 0) {
        $base = max(0, min(100, $kw));
        for ($i = 0; $i < 10; $i++) {
            $keyword_trend[$i] = $base;
        }
    }

    wp_send_json_success([
        'score' => $score,
        'pages_indexed' => (int) ($website_analysis['indexed_pages'] ?? 0),
        'errors' => (int) ($website_analysis['errors'] ?? 0),
        'warnings' => (int) ($website_analysis['warnings'] ?? 0),
        'keyword_trend' => $keyword_trend,
        'levels' => [
            'onpage' => (int) ($website_analysis['onpage_score'] ?? 0),
            'technical' => (int) ($website_analysis['technical_score'] ?? 0),
            'content' => (int) ($website_analysis['content_score'] ?? 0),
            'backlink' => (int) ($website_analysis['backlink_authority'] ?? 0),
            'local' => (int) ($website_analysis['local_score'] ?? 0),
        ],
        'domain_authority' => is_numeric($website_analysis['domain_authority'] ?? null) ? round((float) $website_analysis['domain_authority'], 1) : '—',
        'backlink_count' => isset($website_analysis['backlink_count']) ? (int) $website_analysis['backlink_count'] : null,
        'metric_sources' => (array) ($website_analysis['metric_sources'] ?? []),
        'metric_labels' => (array) ($website_analysis['metric_labels'] ?? []),
        'keyword_opportunity' => (int) ($website_analysis['keyword_opportunity'] ?? 0),
        'status' => 'active'
    ]);
}

function vmo_get_seo_integrations() {
    if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['nonce'] ?? $_REQUEST['security'] ?? '')), 'vmo_nonce')) { wp_send_json_error(['message' => 'Security check failed'], 403); }
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if (!vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Invalid nonce', 'status' => 'invalid_nonce'], 403);
    }
    $user_id = get_current_user_id();
    $ah = (string) get_user_meta($user_id, 'vmo_ahrefs_api_key', true);
    $se = (string) get_user_meta($user_id, 'vmo_semrush_api_key', true);
    $auto = (int) get_user_meta($user_id, 'vmo_seo_auto_execute', true);
    $last = (string) get_user_meta($user_id, 'vmo_seo_last_refresh', true);
    $provider_metrics = get_user_meta($user_id, 'vmo_seo_provider_metrics', true);
    $provider_metrics = is_array($provider_metrics) ? $provider_metrics : [];

    $provider_label = function($connected, $provider) use ($provider_metrics) {
        if (!$connected) return 'Not connected';
        $status = (string) ($provider_metrics[$provider]['status'] ?? 'connected_pending');
        if ($status === 'synced') return 'Connected - enriched data synced';
        if ($status === 'error') return 'Connected - refresh needs attention';
        return 'Connected - enrichment pending';
    };

    $mask = function($k){
        $k = (string) $k;
        if ($k === '') return '';
        $len = strlen($k);
        if ($len <= 6) return str_repeat('•', max(0, $len));
        return substr($k, 0, 3) . str_repeat('•', max(0, $len - 6)) . substr($k, -3);
    };

    wp_send_json_success([
        'ahrefs_connected' => $ah !== '',
        'semrush_connected' => $se !== '',
        'ahrefs_status' => $provider_label($ah !== '', 'ahrefs'),
        'semrush_status' => $provider_label($se !== '', 'semrush'),
        'provider_metrics' => $provider_metrics,
        'ahrefs_key_masked' => $ah !== '' ? $mask($ah) : null,
        'semrush_key_masked' => $se !== '' ? $mask($se) : null,
        'auto_execute' => (bool) $auto,
        'last_refresh' => $last ?: '—',
    ]);
}

function vmo_save_seo_integrations() {
    if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['nonce'] ?? $_REQUEST['security'] ?? '')), 'vmo_nonce')) { wp_send_json_error(['message' => 'Security check failed'], 403); }
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if (!vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Invalid nonce', 'status' => 'invalid_nonce'], 403);
    }
    $user_id = get_current_user_id();
    $ah = isset($_POST['ahrefs_key']) ? sanitize_text_field(wp_unslash($_POST['ahrefs_key'])) : '';
    $se = isset($_POST['semrush_key']) ? sanitize_text_field(wp_unslash($_POST['semrush_key'])) : '';
    if ($ah !== '') update_user_meta($user_id, 'vmo_ahrefs_api_key', $ah);
    if ($se !== '') update_user_meta($user_id, 'vmo_semrush_api_key', $se);
    wp_send_json_success(['saved' => true]);
}

function vmo_set_seo_auto_execute() {
    if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['nonce'] ?? $_REQUEST['security'] ?? '')), 'vmo_nonce')) { wp_send_json_error(['message' => 'Security check failed'], 403); }
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if (!vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Invalid nonce', 'status' => 'invalid_nonce'], 403);
    }
    $user_id = get_current_user_id();
    $value = isset($_POST['value']) ? (int) $_POST['value'] : 0;
    update_user_meta($user_id, 'vmo_seo_auto_execute', $value ? 1 : 0);
    wp_send_json_success(['auto_execute' => (bool) $value]);
}

function vmo_seo_public_domain() {
    $host = wp_parse_url(home_url('/'), PHP_URL_HOST);
    $host = strtolower((string) $host);
    return preg_replace('/^www\./', '', $host) ?: '';
}

function vmo_seo_find_numeric_value($data, array $keys) {
    if (!is_array($data)) return null;
    foreach ($keys as $key) {
        if (isset($data[$key]) && is_numeric($data[$key])) {
            return (float) $data[$key];
        }
    }
    foreach ($data as $value) {
        if (is_array($value)) {
            $found = vmo_seo_find_numeric_value($value, $keys);
            if ($found !== null) return $found;
        }
    }
    return null;
}

function vmo_seo_csv_rows($body) {
    $lines = array_values(array_filter(array_map('trim', preg_split('/\r\n|\r|\n/', (string) $body))));
    if (count($lines) < 2) return [];
    $header_line = array_shift($lines);
    $headers = str_getcsv($header_line, ';');
    if (count($headers) < 2) $headers = str_getcsv($header_line, ',');
    $rows = [];
    foreach ($lines as $line) {
        $columns = str_getcsv($line, ';');
        if (count($columns) < 2) $columns = str_getcsv($line, ',');
        $row = [];
        foreach ($headers as $index => $header) {
            $row[trim((string) $header)] = $columns[$index] ?? '';
        }
        $rows[] = $row;
    }
    return $rows;
}

function vmo_seo_target_keywords($user_id, array $consultation_profile = []) {
    $keywords = [];
    $saved_keywords = get_user_meta($user_id, 'vmo_seo_target_keywords', true);
    if (is_array($saved_keywords)) $keywords = array_merge($keywords, $saved_keywords);
    if (!empty($consultation_profile['target_keywords'])) $keywords = array_merge($keywords, (array) $consultation_profile['target_keywords']);
    $state = get_user_meta($user_id, 'vmo_seo_recommendations_state', true);
    if (is_array($state) && !empty($state['keywords'])) {
        foreach ($state['keywords'] as $keyword_row) {
            $keywords[] = is_array($keyword_row) ? ($keyword_row['keyword'] ?? '') : $keyword_row;
        }
    }
    $keywords = array_map('sanitize_text_field', array_map('strval', $keywords));
    $keywords = array_values(array_unique(array_filter($keywords)));
    return array_slice($keywords, 0, 20);
}

function vmo_seo_fetch_ahrefs_metrics($api_key, $domain) {
    if ($api_key === '' || $domain === '') return ['status' => 'not_configured'];
    $result = ['status' => 'connected_pending', 'source' => 'ahrefs'];
    $headers = ['Authorization' => 'Bearer ' . $api_key, 'Accept' => 'application/json'];
    $endpoints = [
        'domain_rating' => add_query_arg(['target' => $domain, 'mode' => 'subdomains'], 'https://api.ahrefs.com/v3/site-explorer/domain-rating'),
        'backlinks' => add_query_arg(['target' => $domain, 'mode' => 'subdomains'], 'https://api.ahrefs.com/v3/site-explorer/backlinks-stats'),
    ];
    foreach ($endpoints as $name => $url) {
        $response = wp_remote_get($url, ['headers' => $headers, 'timeout' => 12]);
        if (is_wp_error($response)) {
            $result['status'] = 'error';
            $result['message'] = $response->get_error_message();
            continue;
        }
        $code = (int) wp_remote_retrieve_response_code($response);
        $decoded = json_decode(wp_remote_retrieve_body($response), true);
        if ($code < 200 || $code >= 300 || !is_array($decoded)) {
            $result['status'] = 'error';
            $result['message'] = 'Ahrefs returned HTTP ' . $code;
            continue;
        }
        if ($name === 'domain_rating') {
            $rating = vmo_seo_find_numeric_value($decoded, ['domain_rating', 'domainRating', 'dr', 'rating']);
            if ($rating !== null) $result['domain_authority'] = round($rating, 1);
        }
        if ($name === 'backlinks') {
            $backlinks = vmo_seo_find_numeric_value($decoded, ['backlinks', 'backlinks_count', 'total_backlinks', 'refdomains', 'refdomains_count']);
            if ($backlinks !== null) $result['backlinks'] = (int) $backlinks;
        }
        if (!empty($result['domain_authority']) || isset($result['backlinks'])) $result['status'] = 'synced';
    }
    return $result;
}

function vmo_seo_fetch_semrush_metrics($api_key, $domain, array $keywords = []) {
    if ($api_key === '' || $domain === '') return ['status' => 'not_configured'];
    $result = ['status' => 'connected_pending', 'source' => 'semrush', 'keyword_volumes' => []];
    $rank_url = add_query_arg([
        'type' => 'domain_rank',
        'key' => $api_key,
        'export_columns' => 'Dn,Rk,Or,Ot,Oc,Ad,At',
        'domain' => $domain,
        'database' => 'us',
    ], 'https://api.semrush.com/');
    $response = wp_remote_get($rank_url, ['timeout' => 12]);
    if (is_wp_error($response)) {
        $result['status'] = 'error';
        $result['message'] = $response->get_error_message();
    } else {
        $code = (int) wp_remote_retrieve_response_code($response);
        $rows = vmo_seo_csv_rows(wp_remote_retrieve_body($response));
        if ($code >= 200 && $code < 300 && !empty($rows[0])) {
            $row = $rows[0];
            if (isset($row['Or']) && is_numeric($row['Or'])) $result['organic_keywords'] = (int) $row['Or'];
            if (isset($row['Ot']) && is_numeric($row['Ot'])) $result['organic_traffic'] = (int) $row['Ot'];
            $result['status'] = 'synced';
        } else {
            $result['status'] = 'error';
            $result['message'] = 'SEMrush returned HTTP ' . $code;
        }
    }
    foreach (array_slice($keywords, 0, 5) as $keyword) {
        $keyword_url = add_query_arg([
            'type' => 'phrase_this',
            'key' => $api_key,
            'phrase' => $keyword,
            'export_columns' => 'Ph,Nq,Cp,Co,Nr,Td',
            'database' => 'us',
        ], 'https://api.semrush.com/');
        $keyword_response = wp_remote_get($keyword_url, ['timeout' => 10]);
        if (is_wp_error($keyword_response)) continue;
        $keyword_rows = vmo_seo_csv_rows(wp_remote_retrieve_body($keyword_response));
        if (!empty($keyword_rows[0]['Nq']) && is_numeric($keyword_rows[0]['Nq'])) {
            $result['keyword_volumes'][$keyword] = (int) $keyword_rows[0]['Nq'];
            $result['status'] = 'synced';
        }
    }
    return $result;
}

function vmo_seo_refresh_provider_metrics($user_id, array $consultation_profile = []) {
    $domain = vmo_seo_public_domain();
    $keywords = vmo_seo_target_keywords($user_id, $consultation_profile);
    $ahrefs_key = (string) get_user_meta($user_id, 'vmo_ahrefs_api_key', true);
    $semrush_key = (string) get_user_meta($user_id, 'vmo_semrush_api_key', true);
    $metrics = [
        'domain' => $domain,
        'refreshed_at' => current_time('mysql'),
        'ahrefs' => vmo_seo_fetch_ahrefs_metrics($ahrefs_key, $domain),
        'semrush' => vmo_seo_fetch_semrush_metrics($semrush_key, $domain, $keywords),
    ];
    update_user_meta($user_id, 'vmo_seo_provider_metrics', $metrics);
    return $metrics;
}

function vmo_seo_issue_breakdown($user_id, array $analysis) {
    global $wpdb;
    $post_types_sql = "('post','page')";
    $base_where = $wpdb->prepare("p.post_author = %d AND p.post_status = 'publish' AND p.post_type IN {$post_types_sql}", $user_id);
    $published_count = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->posts} p WHERE {$base_where}");
    if ($published_count <= 0) {
        $base_where = "p.post_status = 'publish' AND p.post_type IN {$post_types_sql}";
        $published_count = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->posts} p WHERE {$base_where}");
    }
    $missing_meta = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->posts} p WHERE {$base_where} AND NOT EXISTS (SELECT 1 FROM {$wpdb->postmeta} pm WHERE pm.post_id = p.ID AND pm.meta_key IN ('vmo_seo_description','_vmo_seo_description','_yoast_wpseo_metadesc','_aioseo_description','_rank_math_description') AND pm.meta_value <> '')");
    $missing_titles = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->posts} p WHERE {$base_where} AND NOT EXISTS (SELECT 1 FROM {$wpdb->postmeta} pm WHERE pm.post_id = p.ID AND pm.meta_key IN ('vmo_seo_title','_vmo_seo_title','_yoast_wpseo_title','_aioseo_title','_rank_math_title') AND pm.meta_value <> '')");
    $thin_content = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->posts} p WHERE {$base_where} AND CHAR_LENGTH(TRIM(p.post_content)) < 1200");
    $missing_images = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->posts} p WHERE {$base_where} AND NOT EXISTS (SELECT 1 FROM {$wpdb->postmeta} pm WHERE pm.post_id = p.ID AND pm.meta_key = '_thumbnail_id' AND pm.meta_value <> '')");
    $issues = [
        'Published page proxy' => $published_count,
        'Missing meta descriptions' => max(0, $missing_meta),
        'Missing SEO titles' => max(0, $missing_titles),
        'Thin content pages' => max(0, $thin_content),
        'Pages without featured image' => max(0, $missing_images),
        'SSL issue' => is_ssl() ? 0 : 1,
        'Sitemap unavailable' => !empty($analysis['sitemap_found']) ? 0 : 1,
        'Robots check unavailable' => !empty($analysis['robots_found']) ? 0 : 1,
    ];
    return array_filter($issues, static function($value) { return (int) $value > 0; });
}

function vmo_seo_save_report_snapshot($user_id, array $snapshot) {
    $history = get_user_meta($user_id, 'vmo_seo_report_history', true);
    if (!is_array($history)) $history = [];
    $history[] = $snapshot;
    $history = array_slice($history, -52);
    update_user_meta($user_id, 'vmo_seo_report_history', $history);
    update_user_meta($user_id, 'vmo_seo_latest_report', $snapshot);
    return $history;
}

function vmo_seo_build_report_snapshot($user_id, $persist = false) {
    $consultation_profile = function_exists('vmo_get_consultation_profile') ? vmo_get_consultation_profile($user_id) : [];
    $analysis = vmo_analyze_website_seo($user_id, $consultation_profile);
    $provider_metrics = get_user_meta($user_id, 'vmo_seo_provider_metrics', true);
    $provider_metrics = is_array($provider_metrics) ? $provider_metrics : [];
    $gsc_metrics = function_exists('vmo_gsc_get_latest_metrics') ? vmo_gsc_get_latest_metrics($user_id) : [];
    $gsc_connected = !empty($gsc_metrics['connected']);
    $keywords = vmo_seo_target_keywords($user_id, $consultation_profile);
    $ranking_history = get_user_meta($user_id, 'vmo_ranking_history', true);
    $latest_ranking = is_array($ranking_history) && !empty($ranking_history) ? end($ranking_history) : [];
    if (is_array($latest_ranking) && !empty($latest_ranking['keywords'])) {
        foreach ($latest_ranking['keywords'] as $ranking_row) {
            if (!empty($ranking_row['keyword'])) $keywords[] = sanitize_text_field($ranking_row['keyword']);
        }
    }
    $keywords = array_values(array_unique(array_filter($keywords)));
    $issue_breakdown = vmo_seo_issue_breakdown($user_id, $analysis);
    $backlinks = null;
    if (isset($provider_metrics['ahrefs']['backlinks'])) $backlinks = (int) $provider_metrics['ahrefs']['backlinks'];
    $backlink_proxy = (int) ($analysis['backlink_authority'] ?? 0);
    $content_growth_posts = get_posts(['author' => $user_id, 'post_type' => ['post', 'page'], 'post_status' => ['publish', 'draft'], 'numberposts' => 100, 'meta_key' => 'vmo_seo_optimized']);
    if (empty($content_growth_posts)) $content_growth_posts = get_posts(['post_type' => ['post', 'page'], 'post_status' => ['publish', 'draft'], 'numberposts' => 100, 'meta_key' => 'vmo_seo_optimized']);
    $authority_tasks = function_exists('vmo_seo_get_authority_task_rows') ? vmo_seo_get_authority_task_rows($user_id) : [];
    $local_visibility = function_exists('vmo_seo_get_local_visibility_checklist') ? vmo_seo_get_local_visibility_checklist($user_id) : ['items' => []];
    $review_status = function_exists('vmo_seo_get_review_campaign_status') ? vmo_seo_get_review_campaign_status($user_id) : [];
    $business_outcomes = function_exists('vmo_seo_get_business_outcomes') ? vmo_seo_get_business_outcomes($user_id) : [];
    $ai_search = function_exists('vmo_seo_ai_search_audit') ? vmo_seo_ai_search_audit($user_id) : [];
    $technical_audit = function_exists('vmo_seo_deep_technical_audit') ? vmo_seo_deep_technical_audit($user_id) : [];
    $customer_language = function_exists('vmo_seo_mine_customer_language') ? vmo_seo_mine_customer_language($user_id, 30) : [];
    $competitor_gaps = function_exists('vmo_seo_competitor_gap_engine') ? vmo_seo_competitor_gap_engine($user_id) : [];
    $completed_authority = count(array_filter($authority_tasks, static fn($task) => ($task['status'] ?? '') === 'completed'));
    $completed_local = count(array_filter((array) ($local_visibility['items'] ?? []), static fn($item) => ($item['status'] ?? '') === 'completed'));
    $snapshot = [
        'date' => current_time('mysql'),
        'score' => (int) ($analysis['overall_score'] ?? 0),
        'summary' => 'SEO report generated from site content, VMO metadata, Search Console when connected, ranking history, optional provider enrichment, CRM/call/review signals, and VMO attribution.',
        'core_metrics' => [
            'onpage' => (int) ($analysis['onpage_score'] ?? 0),
            'technical' => (int) ($analysis['technical_score'] ?? 0),
            'content' => (int) ($analysis['content_score'] ?? 0),
            'local' => (int) ($analysis['local_score'] ?? 0),
            'overall' => (int) ($analysis['overall_score'] ?? 0),
        ],
        'index_coverage' => [
            'published_pages_proxy' => (int) ($analysis['indexed_pages'] ?? 0),
            'indexed_pages' => $gsc_connected ? ($gsc_metrics['indexed_pages'] ?? null) : null,
            'source' => $gsc_connected ? 'google_search_console' : 'wordpress_published_content_proxy',
            'label' => $gsc_connected ? 'Google Search Console' : 'WordPress content proxy',
        ],
        'top_keywords' => array_slice($keywords, 0, 10),
        'issue_breakdown' => $issue_breakdown,
        'backlinks' => [
            'value' => $backlinks !== null ? $backlinks : $backlink_proxy,
            'source' => $backlinks !== null ? 'provider_enriched' : 'site_age_content_proxy',
            'label' => $backlinks !== null ? 'Provider-enriched' : 'Estimated from site content',
        ],
        'provider_metrics' => $provider_metrics,
        'search_visibility' => [
            'source_priority' => ['Google Search Console', 'Ahrefs/SEMrush', 'VMO internal analytics', 'WordPress proxy metrics'],
            'active_source' => $gsc_connected ? 'Google Search Console' : (!empty($provider_metrics) ? 'Ahrefs/SEMrush' : 'WordPress proxy metrics'),
            'clicks' => (int) ($gsc_metrics['clicks'] ?? 0),
            'impressions' => (int) ($gsc_metrics['impressions'] ?? 0),
            'ctr' => (float) ($gsc_metrics['ctr'] ?? 0),
            'average_position' => (float) ($gsc_metrics['average_position'] ?? 0),
            'queries' => (array) ($gsc_metrics['queries'] ?? []),
            'pages' => (array) ($gsc_metrics['pages'] ?? []),
            'devices' => (array) ($gsc_metrics['devices'] ?? []),
            'countries' => (array) ($gsc_metrics['countries'] ?? []),
            'date_trends' => (array) ($gsc_metrics['date_trends'] ?? []),
            'top_pages' => (array) ($gsc_metrics['top_pages'] ?? []),
            'underperforming_pages' => (array) ($gsc_metrics['underperforming_pages'] ?? []),
            'keyword_opportunities' => (array) ($gsc_metrics['keyword_opportunities'] ?? []),
        ],
        'business_outcomes' => $business_outcomes,
        'content_growth' => [
            'seo_assets_total' => count($content_growth_posts),
            'published_assets' => count(array_filter($content_growth_posts, static fn($post) => get_post_status($post) === 'publish')),
            'draft_assets' => count(array_filter($content_growth_posts, static fn($post) => get_post_status($post) === 'draft')),
            'source' => 'VMO generated content metadata',
        ],
        'authority_growth' => [
            'total_tasks' => count($authority_tasks),
            'completed_tasks' => $completed_authority,
            'open_tasks' => max(0, count($authority_tasks) - $completed_authority),
            'source' => 'VMO authority task board',
        ],
        'local_visibility' => [
            'total_items' => count((array) ($local_visibility['items'] ?? [])),
            'completed_items' => $completed_local,
            'source' => 'VMO local visibility checklist',
            'deferred_next_version' => (array) ($local_visibility['helpers']['deferred_next_version'] ?? []),
        ],
        'review_growth' => $review_status,
        'competitor_gaps' => array_slice((array) $competitor_gaps, 0, 10),
        'ai_search' => $ai_search,
        'customer_language' => $customer_language,
        'technical_audit' => $technical_audit,
        'token_policy' => function_exists('vmo_seo_token_policy') ? vmo_seo_token_policy() : [],
        'metric_sources' => $analysis['metric_sources'] ?? [],
        'metric_labels' => $analysis['metric_labels'] ?? [],
    ];
    if ($persist) vmo_seo_save_report_snapshot($user_id, $snapshot);
    return $snapshot;
}

function vmo_refresh_seo_data() {
    if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['nonce'] ?? $_REQUEST['security'] ?? '')), 'vmo_nonce')) { wp_send_json_error(['message' => 'Security check failed'], 403); }
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if (!vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Invalid nonce', 'status' => 'invalid_nonce'], 403);
    }
    $user_id = get_current_user_id();
    $ts = current_time('mysql');
    $consultation_profile = function_exists('vmo_get_consultation_profile') ? vmo_get_consultation_profile($user_id) : [];
    $provider_metrics = vmo_seo_refresh_provider_metrics($user_id, $consultation_profile);
    $gsc_metrics = function_exists('vmo_gsc_refresh_metrics') ? vmo_gsc_refresh_metrics($user_id) : [];
    $snapshot = vmo_seo_build_report_snapshot($user_id, true);
    update_user_meta($user_id, 'vmo_seo_last_refresh', $ts);
    wp_send_json_success([
        'message' => 'SEO data refreshed and report snapshot saved',
        'last_refresh' => $ts,
        'report' => $snapshot,
        'provider_metrics' => $provider_metrics,
        'gsc_metrics' => $gsc_metrics,
    ]);
}

function vmo_get_seo_reports() {
    if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['nonce'] ?? $_REQUEST['security'] ?? '')), 'vmo_nonce')) { wp_send_json_error(['message' => 'Security check failed'], 403); }
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if (!vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Invalid nonce', 'status' => 'invalid_nonce'], 403);
    }
    $user_id = get_current_user_id();
    $history = get_user_meta($user_id, 'vmo_seo_report_history', true);
    if (!is_array($history) || empty($history)) {
        $history = [vmo_seo_build_report_snapshot($user_id, true)];
    }
    $history = array_values(array_filter($history, 'is_array'));
    $history = array_slice($history, -12);
    $latest = !empty($history) ? end($history) : vmo_seo_build_report_snapshot($user_id, false);
    $reports = array_reverse(array_map(static function($report) {
        return [
            'date' => (string) ($report['date'] ?? ''),
            'score' => (int) ($report['score'] ?? 0),
            'summary' => (string) ($report['summary'] ?? 'SEO report snapshot'),
        ];
    }, $history));
    $core_trend = array_map(static function($report) { return (int) ($report['score'] ?? 0); }, $history);
    $backlinks_trend = array_map(static function($report) { return (int) ($report['backlinks']['value'] ?? 0); }, $history);
    $index_trend = array_map(static function($report) { return (int) ($report['index_coverage']['published_pages_proxy'] ?? 0); }, $history);
    wp_send_json_success([
        'reports' => $reports,
        'core_trend' => $core_trend,
        'top_keywords' => (array) ($latest['top_keywords'] ?? []),
        'backlinks_trend' => $backlinks_trend,
        'index_trend' => $index_trend,
        'issue_breakdown' => (array) ($latest['issue_breakdown'] ?? []),
        'core_metrics' => (array) ($latest['core_metrics'] ?? []),
        'index_coverage' => (array) ($latest['index_coverage'] ?? []),
        'search_visibility' => (array) ($latest['search_visibility'] ?? []),
        'business_outcomes' => (array) ($latest['business_outcomes'] ?? []),
        'content_growth' => (array) ($latest['content_growth'] ?? []),
        'authority_growth' => (array) ($latest['authority_growth'] ?? []),
        'local_visibility' => (array) ($latest['local_visibility'] ?? []),
        'review_growth' => (array) ($latest['review_growth'] ?? []),
        'competitor_gaps' => (array) ($latest['competitor_gaps'] ?? []),
        'ai_search' => (array) ($latest['ai_search'] ?? []),
        'customer_language' => (array) ($latest['customer_language'] ?? []),
        'technical_audit' => (array) ($latest['technical_audit'] ?? []),
        'metric_sources' => (array) ($latest['metric_sources'] ?? []),
        'metric_labels' => (array) ($latest['metric_labels'] ?? []),
        'provider_metrics' => (array) ($latest['provider_metrics'] ?? []),
    ]);
}

function vmo_seo_get_reco_state($user_id) {
    $state = get_user_meta($user_id, 'vmo_seo_recommendations_state', true);
    if (is_array($state) && isset($state['keywords'], $state['blog_topics'], $state['actions'])) {
        return $state;
    }
    $profile = function_exists('vmo_get_consultation_profile') ? vmo_get_consultation_profile($user_id) : [];
    $insights = vmo_get_cached_industry_insights($user_id);
    $recs = vmo_generate_ai_seo_recommendations($user_id, $profile, $insights);
    $keywords = [];
    foreach (($recs['keywords'] ?? []) as $k) {
        $kw = is_array($k) ? $k : ['keyword' => (string) $k];
        $kw['approved'] = false;
        $keywords[] = $kw;
    }
    $blogs = array_map(function($topic){ return ['topic' => (string) $topic, 'approved' => false]; }, (array) ($recs['blog_topics'] ?? []));
    $action_texts = (array) ($recs['technical_actions'] ?? []);
    foreach ((array) ($recs['content_gaps'] ?? []) as $gap) {
        $gap_text = is_array($gap) ? ($gap['text'] ?? $gap['title'] ?? wp_json_encode($gap)) : $gap;
        if ($gap_text !== '') $action_texts[] = 'Create SEO content page: ' . $gap_text;
    }
    foreach (['service_pages', 'location_pages', 'support_pages'] as $page_key) {
        foreach ((array) ($recs[$page_key] ?? []) as $page_recommendation) {
            $page_text = is_array($page_recommendation) ? ($page_recommendation['text'] ?? $page_recommendation['title'] ?? wp_json_encode($page_recommendation)) : $page_recommendation;
            if ($page_text !== '') $action_texts[] = 'Create ' . str_replace('_', ' ', $page_key) . ': ' . $page_text;
        }
    }
    $action_texts = array_values(array_unique(array_filter(array_map('strval', $action_texts))));
    $actions = array_map(function($action_text){ return ['text' => (string) $action_text, 'approved' => false]; }, $action_texts);
    $state = [
        'keywords' => $keywords,
        'blog_topics' => $blogs,
        'actions' => $actions,
        'generated_at' => current_time('mysql'),
    ];
    update_user_meta($user_id, 'vmo_seo_recommendations_state', $state);
    return $state;
}

function vmo_regenerate_seo_recommendations() {
    if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['nonce'] ?? $_REQUEST['security'] ?? '')), 'vmo_nonce')) { wp_send_json_error(['message' => 'Security check failed'], 403); }
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if (!vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Invalid nonce', 'status' => 'invalid_nonce'], 403);
    }
    $user_id = get_current_user_id();
    if (get_option('vmo_openai_api_key', '') !== '' && !vmo_seo_has_token_balance($user_id, 'VMO_TOKENS_SEO_RECOMMENDATION_REGEN')) {
        wp_send_json_error(['message' => 'Not enough tokens to regenerate AI SEO recommendations.']);
    }
    delete_user_meta($user_id, 'vmo_seo_recommendations_state');
    vmo_seo_get_reco_state($user_id);
    if (get_option('vmo_openai_api_key', '') !== '') {
        vmo_seo_charge_tokens($user_id, 'VMO_TOKENS_SEO_RECOMMENDATION_REGEN', 'seo_recommendation', 'Regenerated AI SEO recommendations');
    }
    wp_send_json_success(['message' => 'Recommendations regenerated']);
}

function vmo_update_seo_recommendation() {
    if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['nonce'] ?? $_REQUEST['security'] ?? '')), 'vmo_nonce')) { wp_send_json_error(['message' => 'Security check failed'], 403); }
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if (!vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Invalid nonce', 'status' => 'invalid_nonce'], 403);
    }
    $user_id = get_current_user_id();
    $type = sanitize_key($_POST['type'] ?? '');
    $index = isset($_POST['index']) ? (int) $_POST['index'] : -1;
    $text = sanitize_text_field(wp_unslash($_POST['text'] ?? ''));
    if ($type === '' || $index < 0 || $text === '') {
        wp_send_json_error(['message' => 'Invalid input']);
    }
    $state = vmo_seo_get_reco_state($user_id);
    if (!isset($state[$type]) || !is_array($state[$type]) || !isset($state[$type][$index])) {
        wp_send_json_error(['message' => 'Recommendation not found']);
    }
    if ($type === 'keywords') {
        $state[$type][$index]['keyword'] = $text;
        $state[$type][$index]['approved'] = false;
    } elseif ($type === 'blog_topics') {
        $state[$type][$index]['topic'] = $text;
        $state[$type][$index]['approved'] = false;
    } else {
        $state[$type][$index]['text'] = $text;
        $state[$type][$index]['approved'] = false;
    }
    update_user_meta($user_id, 'vmo_seo_recommendations_state', $state);
    wp_send_json_success(['updated' => true]);
}

function vmo_approve_seo_recommendation() {
    if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['nonce'] ?? $_REQUEST['security'] ?? '')), 'vmo_nonce')) { wp_send_json_error(['message' => 'Security check failed'], 403); }
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if (!vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Invalid nonce', 'status' => 'invalid_nonce'], 403);
    }
    global $wpdb;
    $user_id = get_current_user_id();
    $scope = sanitize_key($_POST['scope'] ?? '');
    $type = sanitize_key($_POST['type'] ?? '');
    $index = isset($_POST['index']) ? (int) $_POST['index'] : -1;
    $auto = (int) get_user_meta($user_id, 'vmo_seo_auto_execute', true);

    $state = vmo_seo_get_reco_state($user_id);
    if ($scope === 'all') {
        foreach (['keywords','blog_topics','actions'] as $k) {
            if (!isset($state[$k]) || !is_array($state[$k])) continue;
            foreach ($state[$k] as $i => $it) {
                $state[$k][$i]['approved'] = true;
            }
        }
        update_user_meta($user_id, 'vmo_seo_recommendations_state', $state);
        // Log approval to legacy table
        $table = $wpdb->prefix . 'vmo_approval_queue';
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $wpdb->insert($table, [
            'user_id' => $user_id,
            'type' => 'seo',
            'title' => 'SEO: Approved all recommendations',
            'payload' => wp_json_encode(['scope' => 'all']),
            'status' => 'approved',
            'notes' => $auto ? 'auto_execute=1' : 'auto_execute=0',
            'created_at' => current_time('mysql'),
            'updated_at' => current_time('mysql'),
        ]);
        // Also log to unified approvals table for Strategic Dashboard
        $approvals_table = $wpdb->prefix . 'vmo_approvals';
        if (vmo_table_exists($approvals_table)) {
            $wpdb->insert($approvals_table, [
                'user_id'       => $user_id,
                'title'         => 'SEO: Approved all recommendations',
                'description'   => 'Bulk approval of all SEO keyword, blog topic, and action recommendations',
                'approval_type' => 'seo_change',
                'source_system' => 'seo',
                'payload'       => wp_json_encode(['scope' => 'all']),
                'status'        => 'approved',
                'decided_at'    => current_time('mysql'),
                'created_at'    => current_time('mysql'),
                'updated_at'    => current_time('mysql'),
            ]);
        }
        if ($auto) {
            // Auto execute “all” by enqueueing tasks/drafts best-effort
            foreach (['keywords','blog_topics','actions'] as $k) {
                if (!isset($state[$k]) || !is_array($state[$k])) continue;
                foreach ($state[$k] as $i => $it) {
                    // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- intentionally setting POST data for internal dispatch
                    $_POST['type'] = $k;
                    // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- intentionally setting POST data for internal dispatch
                    $_POST['index'] = $i;
                    // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- intentionally setting POST data for internal dispatch
                    $_POST['text'] = ($k==='keywords') ? ($it['keyword'] ?? '') : (($k==='blog_topics') ? ($it['topic'] ?? '') : ($it['text'] ?? ''));
                    vmo_execute_seo_recommendation(false);
                }
            }
        }
        wp_send_json_success(['approved' => true]);
    }

    if ($type === '' || $index < 0) {
        wp_send_json_error(['message' => 'Invalid input']);
    }
    if (!isset($state[$type]) || !is_array($state[$type]) || !isset($state[$type][$index])) {
        wp_send_json_error(['message' => 'Recommendation not found']);
    }
    $state[$type][$index]['approved'] = true;
    update_user_meta($user_id, 'vmo_seo_recommendations_state', $state);

    // Log approval
    $txt = ($type==='keywords') ? ($state[$type][$index]['keyword'] ?? '') : (($type==='blog_topics') ? ($state[$type][$index]['topic'] ?? '') : ($state[$type][$index]['text'] ?? ''));
    $table = $wpdb->prefix . 'vmo_approval_queue';
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    $wpdb->insert($table, [
        'user_id' => $user_id,
        'type' => 'seo',
        'title' => 'SEO: Approved',
        'payload' => wp_json_encode(['type' => $type, 'index' => $index, 'text' => $txt]),
        'status' => 'approved',
        'notes' => $auto ? 'auto_execute=1' : 'auto_execute=0',
        'created_at' => current_time('mysql'),
        'updated_at' => current_time('mysql'),
    ]);

    if ($auto) {
        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- intentionally setting POST data for internal dispatch
        $_POST['text'] = $txt;
        vmo_execute_seo_recommendation(false);
    }
    wp_send_json_success(['approved' => true]);
}

function vmo_seo_value_as_list($value) {
    if (is_array($value)) {
        $items = [];
        foreach ($value as $entry) {
            if (is_array($entry)) {
                $items[] = sanitize_text_field((string) ($entry['name'] ?? $entry['title'] ?? $entry['keyword'] ?? $entry['text'] ?? ''));
            } else {
                $items[] = sanitize_text_field((string) $entry);
            }
        }
        return array_values(array_filter($items));
    }
    if (is_string($value) && strpos($value, ',') !== false) {
        return array_values(array_filter(array_map('trim', explode(',', $value))));
    }
    return array_values(array_filter([sanitize_text_field((string) $value)]));
}

function vmo_seo_pick_focus_keyword($text, array $keywords) {
    $text_lower = strtolower((string) $text);
    foreach ($keywords as $keyword) {
        $keyword = sanitize_text_field((string) $keyword);
        if ($keyword !== '' && strpos($text_lower, strtolower($keyword)) !== false) {
            return $keyword;
        }
    }
    return sanitize_text_field((string) ($keywords[0] ?? $text));
}

function vmo_seo_content_action_kind($text) {
    $text_lower = strtolower((string) $text);
    if (preg_match('/\b(blog|article|post)\b/', $text_lower)) return 'blog_post';
    if (preg_match('/\b(service pages?|location pages?|landing pages?|support pages?|faq pages?|content pages?|city pages?|local pages?)\b/', $text_lower)) return 'page';
    if (preg_match('/\b(create|build|draft|write|publish)\b.*\b(pages?|guides?|faqs?|content)\b/', $text_lower)) return 'page';
    return 'task';
}

function vmo_seo_governance_decision($user_id, $type, $text) {
    $auto_execute = (bool) get_user_meta($user_id, 'vmo_seo_auto_execute', true);
    $kind = $type === 'blog_topics' ? 'blog_post' : ($type === 'actions' ? vmo_seo_content_action_kind($text) : 'metadata');
    $public_content = in_array($kind, ['blog_post', 'page'], true);
    return [
        'auto_execute_enabled' => $auto_execute,
        'execution_kind' => $kind,
        'public_content' => $public_content,
        'requires_approval' => $public_content,
        'post_status' => ($public_content && $auto_execute) ? 'publish' : 'draft',
        'reason' => $public_content ? 'Public SEO content is drafted first unless auto-execute is explicitly enabled.' : 'Non-public SEO task can be saved for follow-up.',
    ];
}

function vmo_seo_internal_link_suggestions($user_id, $limit = 5) {
    $posts = get_posts([
        'author' => $user_id,
        'post_type' => ['post', 'page'],
        'post_status' => 'publish',
        'numberposts' => $limit,
        'orderby' => 'date',
        'order' => 'DESC',
    ]);
    if (empty($posts)) {
        $posts = get_posts([
            'post_type' => ['post', 'page'],
            'post_status' => 'publish',
            'numberposts' => $limit,
            'orderby' => 'date',
            'order' => 'DESC',
        ]);
    }
    $links = [];
    foreach ($posts as $post) {
        $links[] = [
            'title' => get_the_title($post),
            'url' => get_permalink($post),
        ];
    }
    return $links;
}

function vmo_seo_content_has_placeholder_language($html) {
    $plain = strtolower(wp_strip_all_tags((string) $html));
    foreach (['use this section', 'recommended structure', 'add links to', 'lorem ipsum', 'todo:', 'placeholder'] as $phrase) {
        if (strpos($plain, $phrase) !== false) return true;
    }
    return false;
}

function vmo_seo_content_schema($kind, $title, $meta_description, $business_name, $location, $focus_keyword, array $related_keywords, array $faqs = []) {
    $faq_entities = [];
    foreach ($faqs as $faq) {
        $question = sanitize_text_field((string) ($faq['question'] ?? $faq['q'] ?? ''));
        $answer = sanitize_text_field((string) ($faq['answer'] ?? $faq['a'] ?? ''));
        if ($question !== '' && $answer !== '') {
            $faq_entities[] = ['@type' => 'Question', 'name' => $question, 'acceptedAnswer' => ['@type' => 'Answer', 'text' => $answer]];
        }
    }
    if (empty($faq_entities)) {
        $faq_entities = [
            ['@type' => 'Question', 'name' => 'How do I know if this is the right service?', 'acceptedAnswer' => ['@type' => 'Answer', 'text' => 'Compare your needs with the service details and contact the business to confirm the best next step.']],
            ['@type' => 'Question', 'name' => 'How soon should I contact the business?', 'acceptedAnswer' => ['@type' => 'Answer', 'text' => 'Contact the business promptly when the issue affects revenue, customer experience, safety, compliance, or daily operations.']],
        ];
    }
    return [
        '@context' => 'https://schema.org',
        '@graph' => [
            [
                '@type' => $kind === 'page' ? 'Service' : 'Article',
                'name' => $title,
                'headline' => $title,
                'description' => $meta_description,
                'provider' => ['@type' => 'Organization', 'name' => $business_name],
                'areaServed' => $location,
                'keywords' => array_values(array_filter(array_merge([$focus_keyword], $related_keywords))),
            ],
            ['@type' => 'FAQPage', 'mainEntity' => $faq_entities],
        ],
    ];
}

function vmo_seo_finished_content_fallback($kind, $title, $business_name, $industry, $location, array $services, $focus_keyword, array $related_keywords, array $internal_links) {
    $service_line = !empty($services) ? implode(', ', array_slice($services, 0, 5)) : $industry . ' services';
    $location_phrase = $location ? ' in ' . $location : '';
    $related_terms = array_values(array_filter(array_slice($related_keywords, 0, 5)));
    $related_html = '';
    foreach ($related_terms as $related_keyword) {
        $related_html .= '<li>' . esc_html($related_keyword) . '</li>';
    }
    $links_html = '';
    foreach ($internal_links as $link) {
        $links_html .= '<li><a href="' . esc_url($link['url']) . '">' . esc_html($link['title']) . '</a></li>';
    }
    $faqs = [
        ['question' => 'What should I look for before choosing a provider?', 'answer' => 'Look for clear service details, proof of experience, responsive communication, realistic timelines, and a process that fits your goals.'],
        ['question' => 'How can I get the fastest answer?', 'answer' => 'Contact the business with your location, timeline, budget range, and the outcome you need so the team can recommend the right next step.'],
    ];
    $content = '<h1>' . esc_html($title) . '</h1>';
    $content .= '<p>' . esc_html($business_name) . ' helps customers understand ' . esc_html($focus_keyword) . ' and choose a practical next step with confidence. This guide explains what matters, what to expect, and how the right support can turn a search into a clear decision.</p>';
    $content .= '<h2>Why ' . esc_html($focus_keyword) . ' Matters</h2>';
    $content .= '<p>People usually search for ' . esc_html($focus_keyword) . ' when they need a specific result, not just information. The right provider should explain the options plainly, identify the real priority, and connect the recommendation to the customer\'s timeline, budget, and risk level.</p>';
    $content .= '<p>For ' . esc_html($industry) . ' customers' . esc_html($location_phrase) . ', the best experience is direct, organized, and easy to act on. Strong service pages and articles should answer the questions a buyer would ask before calling, booking, or purchasing.</p>';
    $content .= '<h2>How ' . esc_html($business_name) . ' Helps</h2>';
    $content .= '<p>The core focus is ' . esc_html($service_line) . '. Each recommendation is matched to the customer\'s situation so the path forward feels clear instead of generic. That means explaining the need, confirming the right fit, and giving the customer enough context to move without second-guessing.</p>';
    $content .= '<h2>What To Expect</h2><p>A strong process starts with the customer\'s goal, reviews the available options, explains timing and tradeoffs, and ends with a concrete next step. Customers should leave the page knowing what the service does, who it helps, what information to prepare, and how to begin.</p>';
    if (!empty($related_html)) $content .= '<h2>Related Topics</h2><ul>' . $related_html . '</ul>';
    if (!empty($links_html)) $content .= '<h2>Helpful Internal Links</h2><ul>' . $links_html . '</ul>';
    $content .= '<h2>Frequently Asked Questions</h2>';
    foreach ($faqs as $faq) {
        $content .= '<h3>' . esc_html($faq['question']) . '</h3><p>' . esc_html($faq['answer']) . '</p>';
    }
    $content .= '<h2>Next Step</h2><p>Contact ' . esc_html($business_name) . ' to confirm fit, availability, pricing, and the fastest path to the outcome you need.</p>';
    $meta_description = substr(sprintf('%s explains %s with clear next steps for %s%s.', $business_name, $focus_keyword, $industry, $location_phrase), 0, 155);
    return [
        'title' => $title,
        'meta_description' => $meta_description,
        'html' => $content,
        'faqs' => $faqs,
        'source' => 'finished_fallback',
    ];
}

function vmo_seo_generate_finished_content_copy($user_id, $kind, $topic, $title, array $profile, $business_name, $industry, $location, array $services, $focus_keyword, array $related_keywords, array $internal_links) {
    $fallback = vmo_seo_finished_content_fallback($kind, $title, $business_name, $industry, $location, $services, $focus_keyword, $related_keywords, $internal_links);
    $api_key = (string) get_option('vmo_openai_api_key', '');
    if ($api_key === '') return $fallback;
    if (!vmo_seo_has_token_balance($user_id, 'VMO_TOKENS_SEO_CONTENT_GENERATION')) return $fallback;

    $link_context = [];
    foreach ($internal_links as $link) {
        $link_context[] = sanitize_text_field((string) ($link['title'] ?? '')) . ' - ' . esc_url_raw((string) ($link['url'] ?? ''));
    }
    $profile_context = substr(wp_json_encode($profile, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), 0, 5000);
    $prompt = "Generate finished, publication-ready SEO copy for a customer website. Do not write instructions, placeholders, outlines, TODOs, or phrases like 'use this section'. The copy must read like a complete article/page a visitor could read today.\n\n" .
        "CONTENT TYPE: " . ($kind === 'page' ? 'service/location/support page' : 'blog article') . "\n" .
        "TOPIC: {$topic}\nTITLE DIRECTION: {$title}\nBUSINESS: {$business_name}\nINDUSTRY: {$industry}\nLOCATION: {$location}\nSERVICES/PRODUCTS: " . implode(', ', $services) . "\nFOCUS KEYWORD: {$focus_keyword}\nRELATED KEYWORDS: " . implode(', ', $related_keywords) . "\nINTERNAL LINKS: " . implode(' | ', array_filter($link_context)) . "\nCONSULTATION PROFILE JSON:\n{$profile_context}\n\n" .
        "Return valid JSON only with this schema: {\"title\":\"55-70 character finished title\",\"meta_description\":\"145-155 character search description\",\"html\":\"complete HTML body using h1/h2/h3/p/ul/li/a only\",\"faqs\":[{\"question\":\"...\",\"answer\":\"...\"}],\"schema_type\":\"Article or Service\"}. Include practical business-specific details, local context when available, ecommerce/product context when applicable, internal links when useful, and a clear CTA.";
    $model = function_exists('vmo_get_model') ? vmo_get_model('mini') : 'gpt-5.5-mini';
    $body = array_merge([
        'model' => $model,
        'messages' => [
            ['role' => 'system', 'content' => 'You are an expert SEO content strategist writing finished customer-facing website copy.'],
            ['role' => 'user', 'content' => $prompt],
        ],
        'temperature' => 0.55,
    ], function_exists('vmo_get_token_param') ? vmo_get_token_param($model, 3500) : ['max_completion_tokens' => 3500]);
    $response = wp_remote_post('https://api.openai.com/v1/chat/completions', [
        'headers' => ['Authorization' => 'Bearer ' . $api_key, 'Content-Type' => 'application/json'],
        'body' => wp_json_encode($body),
        'timeout' => 45,
    ]);
    if (is_wp_error($response)) return $fallback;
    $raw = json_decode(wp_remote_retrieve_body($response), true);
    $content = (string) ($raw['choices'][0]['message']['content'] ?? '');
    $parsed = function_exists('vmo_extract_json_object_from_text') ? vmo_extract_json_object_from_text($content) : json_decode($content, true);
    if (!is_array($parsed) || empty($parsed['html'])) return $fallback;
    $html = preg_replace('/<\/?(?:html|body|head)[^>]*>/i', '', (string) $parsed['html']);
    $html = wp_kses_post($html);
    if ($html === '' || vmo_seo_content_has_placeholder_language($html)) return $fallback;
    vmo_seo_charge_tokens($user_id, 'VMO_TOKENS_SEO_CONTENT_GENERATION', 'seo_content_generation', 'Generated finished SEO content copy');
    return [
        'title' => sanitize_text_field((string) ($parsed['title'] ?? $title)),
        'meta_description' => substr(sanitize_text_field((string) ($parsed['meta_description'] ?? $fallback['meta_description'])), 0, 155),
        'html' => $html,
        'faqs' => is_array($parsed['faqs'] ?? null) ? $parsed['faqs'] : $fallback['faqs'],
        'source' => 'ai_finished_copy',
    ];
}

function vmo_seo_create_content_asset($user_id, $text, $kind, array $governance) {
    $profile = function_exists('vmo_get_consultation_profile') ? vmo_get_consultation_profile($user_id) : [];
    $business_name = sanitize_text_field((string) ($profile['business_name'] ?? get_bloginfo('name')));
    $industry = sanitize_text_field((string) ($profile['industry'] ?? 'your industry'));
    $location = sanitize_text_field((string) ($profile['location'] ?? ''));
    $services = array_values(array_unique(array_filter(array_merge(
        vmo_seo_value_as_list($profile['services'] ?? []),
        vmo_seo_value_as_list($profile['products_services'] ?? []),
        vmo_seo_value_as_list($profile['offerings'] ?? []),
        vmo_seo_value_as_list($profile['products'] ?? []),
        vmo_seo_value_as_list($profile['product_catalog'] ?? []),
        vmo_seo_value_as_list($profile['ecommerce_products'] ?? [])
    ))));
    $keywords = vmo_seo_target_keywords($user_id, (array) $profile);
    if (empty($keywords) && $industry !== '') $keywords[] = $industry;
    $focus_keyword = vmo_seo_pick_focus_keyword($text, $keywords);
    $related_keywords = array_values(array_slice(array_diff($keywords, [$focus_keyword]), 0, 6));
    $raw_title = wp_strip_all_tags((string) $text);
    $raw_title = preg_replace('/^(create|build|draft|write|publish|add|optimize)\s+/i', '', $raw_title);
    $title = sanitize_text_field($raw_title);
    if ($title === '') $title = $kind === 'page' ? 'SEO Service Page' : 'SEO Blog Draft';
    if (strlen($title) > 95) $title = trim(substr($title, 0, 92)) . '...';
    $post_type = $kind === 'page' ? 'page' : 'post';
    $post_status = sanitize_key((string) ($governance['post_status'] ?? 'draft'));
    $internal_links = vmo_seo_internal_link_suggestions($user_id);
    $copy = vmo_seo_generate_finished_content_copy($user_id, $kind, $text, $title, (array) $profile, $business_name, $industry, $location, $services, $focus_keyword, $related_keywords, $internal_links);
    $title = sanitize_text_field((string) ($copy['title'] ?? $title));
    if (strlen($title) > 95) $title = trim(substr($title, 0, 92)) . '...';
    $meta_description = substr(sanitize_text_field((string) ($copy['meta_description'] ?? '')), 0, 155);
    if ($meta_description === '') {
        $meta_description = substr(sprintf('%s explains %s with clear next steps for %s%s.', $business_name, $focus_keyword, $industry, $location ? ' in ' . $location : ''), 0, 155);
    }
    $content = wp_kses_post((string) ($copy['html'] ?? ''));
    if ($content === '' || vmo_seo_content_has_placeholder_language($content)) {
        $copy = vmo_seo_finished_content_fallback($kind, $title, $business_name, $industry, $location, $services, $focus_keyword, $related_keywords, $internal_links);
        $content = wp_kses_post((string) $copy['html']);
        $meta_description = substr(sanitize_text_field((string) $copy['meta_description']), 0, 155);
    }
    $schema = vmo_seo_content_schema($kind, $title, $meta_description, $business_name, $location, $focus_keyword, $related_keywords, (array) ($copy['faqs'] ?? []));
    $quality_gate = vmo_seo_content_quality_gate(0, $content, $focus_keyword, $kind);
    $ai_quality_review = ['used_ai' => false, 'passed' => true, 'message' => 'AI quality review not required for draft output.'];
    if ($post_status === 'publish' && !empty($quality_gate['passed'])) {
        $ai_quality_review = vmo_seo_ai_quality_review($user_id, $content, $focus_keyword, $kind);
    }
    if ($post_status === 'publish' && empty($quality_gate['passed'])) {
        $post_status = 'draft';
    }
    if ($post_status === 'publish' && empty($ai_quality_review['passed'])) {
        $post_status = 'draft';
    }
    $post_id = wp_insert_post([
        'post_title' => $title,
        'post_name' => sanitize_title($title),
        'post_content' => $content,
        'post_excerpt' => $meta_description,
        'post_status' => $post_status === 'publish' ? 'publish' : 'draft',
        'post_type' => $post_type,
        'post_author' => $user_id,
        'meta_input' => [
            'vmo_generated_blog' => $kind === 'blog_post' ? 1 : 0,
            'vmo_generated_seo_page' => $kind === 'page' ? 1 : 0,
            'vmo_seo_optimized' => 1,
            'vmo_seo_title' => $title,
            'vmo_seo_description' => $meta_description,
            '_vmo_seo_description' => $meta_description,
            'vmo_focus_keyword' => $focus_keyword,
            'vmo_related_keywords' => $related_keywords,
            'vmo_internal_link_suggestions' => $internal_links,
            'vmo_seo_json_ld' => wp_json_encode($schema, JSON_UNESCAPED_SLASHES),
            'vmo_seo_governance' => $governance,
            'vmo_seo_quality_gate' => $quality_gate,
            'vmo_seo_ai_quality_review' => $ai_quality_review,
            'vmo_seo_content_source' => sanitize_key((string) ($copy['source'] ?? 'finished_fallback')),
            'vmo_generation_date' => current_time('mysql'),
        ],
    ], true);
    if (is_wp_error($post_id)) {
        return ['created' => false, 'message' => 'SEO content creation failed', 'error' => $post_id->get_error_message()];
    }
    return [
        'created' => true,
        'post_id' => (int) $post_id,
        'post_type' => $post_type,
        'post_status' => get_post_status($post_id),
        'focus_keyword' => $focus_keyword,
        'message' => empty($quality_gate['passed']) ? $quality_gate['message'] : (empty($ai_quality_review['passed']) ? $ai_quality_review['message'] : ($post_status === 'publish' ? 'SEO content published by auto-execute governance' : 'SEO content draft created for review')),
        'quality_gate' => $quality_gate,
        'ai_quality_review' => $ai_quality_review,
        'url' => get_permalink($post_id),
    ];
}

function vmo_execute_seo_recommendation($emit_json = true) {
    if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['nonce'] ?? $_REQUEST['security'] ?? '')), 'vmo_nonce')) { wp_send_json_error(['message' => 'Security check failed'], 403); }
    if (!current_user_can('read')) {
        if ($emit_json) wp_send_json_error(['message' => 'Unauthorized'], 403);
        return;
    }
    if (!vmo_verify_admin_security()) {
        if ($emit_json) wp_send_json_error(['message' => 'Invalid nonce', 'status' => 'invalid_nonce'], 403);
        return;
    }
    global $wpdb;
    $user_id = get_current_user_id();
    $type = sanitize_key($_POST['type'] ?? '');
    $index = isset($_POST['index']) ? (int) $_POST['index'] : -1;
    $text = sanitize_text_field(wp_unslash($_POST['text'] ?? ''));
    if ($type === '' || $index < 0 || $text === '') {
        if ($emit_json) wp_send_json_error(['message' => 'Invalid input']);
        return;
    }

    $result = ['executed' => true];
    $governance = vmo_seo_governance_decision($user_id, $type, $text);
    $result['governance'] = $governance;
    if ($type === 'keywords') {
        $list = get_user_meta($user_id, 'vmo_seo_target_keywords', true);
        if (!is_array($list)) $list = [];
        if (!in_array($text, $list, true)) $list[] = $text;
        update_user_meta($user_id, 'vmo_seo_target_keywords', $list);
        $result['message'] = 'Keyword saved to SEO targets';
    } elseif ($type === 'blog_topics') {
        $result = array_merge($result, vmo_seo_create_content_asset($user_id, $text, 'blog_post', $governance));
    } else {
        if (($governance['execution_kind'] ?? 'task') === 'page') {
            $result = array_merge($result, vmo_seo_create_content_asset($user_id, $text, 'page', $governance));
        } else {
            $tasks = get_user_meta($user_id, 'vmo_seo_technical_tasks', true);
            if (!is_array($tasks)) $tasks = [];
            $tasks[] = ['text' => $text, 'created_at' => current_time('mysql'), 'governance' => $governance];
            update_user_meta($user_id, 'vmo_seo_technical_tasks', $tasks);
            $result['message'] = 'Task saved';
        }
    }

    // Log execution as a task row
    $table = $wpdb->prefix . 'vmo_approval_queue';
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    $wpdb->insert($table, [
        'user_id' => $user_id,
        'type' => 'seo',
        'title' => 'SEO: Execute',
        'payload' => wp_json_encode(['type' => $type, 'index' => $index, 'text' => $text, 'result' => $result]),
        'status' => 'approved',
        'notes' => 'executed:' . ($governance['execution_kind'] ?? 'task'),
        'created_at' => current_time('mysql'),
        'updated_at' => current_time('mysql'),
    ]);

    if ($emit_json) wp_send_json_success($result);
}

function vmo_get_contacts() {
    vmo_require_crm_ajax_security();
    global $wpdb;
    $user_id = get_current_user_id();
    $type = sanitize_key($_POST['type'] ?? 'lead');
    $limit = isset($_POST['limit']) ? max(1, min(500, (int) $_POST['limit'])) : 200;
    $table = $wpdb->prefix . 'vmo_contacts';
    $has_table = vmo_table_exists($table);
    if (!$has_table) {
        wp_send_json_success(['contacts' => []]);
    }
    $columns = function_exists('vmo_crm_contact_columns') ? vmo_crm_contact_columns($table) : [];
    if (empty($columns) || !in_array('user_id', $columns, true)) {
        wp_send_json_success(['contacts' => []]);
    }
    $where = "user_id=%d";
    $params = [$user_id];
    if ($type === 'customer') {
        $customer_conditions = [];
        if (in_array('lifecycle_stage', $columns, true)) $customer_conditions[] = "lifecycle_stage='customer'";
        if (in_array('primary_category', $columns, true)) $customer_conditions[] = "primary_category='customer'";
        $where .= !empty($customer_conditions) ? " AND (" . implode(' OR ', $customer_conditions) . ")" : " AND 1=0";
    } elseif ($type === 'website_visitor') {
        $where .= in_array('primary_category', $columns, true) ? " AND primary_category='website_visitor'" : " AND 1=0";
    } else {
        if (in_array('lifecycle_stage', $columns, true)) {
            $where .= " AND (lifecycle_stage IS NULL OR lifecycle_stage<>'customer')";
        }
        if (in_array('primary_category', $columns, true)) {
            $where .= " AND (primary_category IS NULL OR primary_category NOT IN ('customer', 'website_visitor'))";
        }
    }
    // ✅ ENHANCED: Now includes company, address, city, job_title for richer lead display
    $order_column = in_array('created_at', $columns, true) ? 'created_at' : (in_array('updated_at', $columns, true) ? 'updated_at' : 'id');
    $sql = $wpdb->prepare(
        "SELECT * FROM {$table} WHERE {$where} ORDER BY {$order_column} DESC LIMIT %d",
        array_merge($params, [$limit])
    );
    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    $rows = $wpdb->get_results($sql, ARRAY_A);
    $contacts = [];
    foreach (($rows ?: []) as $r) {
        $tags = [];
        if (!empty($r['tags'])) {
            $tags = array_values(array_filter(array_map('trim', explode(',', (string) $r['tags']))));
        }
        // Build full address string for display
        $address_parts = array_filter([
            (string) (($r['address'] ?? '') ?: ''),
            (string) (($r['city'] ?? '') ?: ''),
            (string) (($r['state'] ?? '') ?: ''),
            (string) (($r['zip'] ?? '') ?: '')
        ]);
        $full_address = implode(', ', $address_parts);
        
        $contacts[] = [
            'id' => (int) $r['id'],
            'contact_id' => (string) ($r['contact_id'] ?? ''),
            'name' => (string) (($r['full_name'] ?? '') ?: 'Contact'),
            'email' => (string) (($r['email'] ?? '') ?: ''),
            'phone' => (string) (($r['phone'] ?? '') ?: ''),
            'company' => (string) (($r['company'] ?? '') ?: ''),
            'job_title' => (string) (($r['job_title'] ?? '') ?: ''),
            'address' => $full_address,
            'city' => (string) (($r['city'] ?? '') ?: ''),
            'description' => (string) (($r['notes'] ?? '') ?: ''),
            'tags' => $tags,
            'date_added' => (string) (($r['created_at'] ?? '') ?: ''),
            'type' => (($r['lifecycle_stage'] ?? '') === 'customer' || ($r['primary_category'] ?? '') === 'customer') ? 'customer' : 'lead',
            'primary_category' => (string) (($r['primary_category'] ?? '') ?: ''),
            'lifecycle_stage' => (string) (($r['lifecycle_stage'] ?? '') ?: ''),
            'lead_score' => (int) (($r['lead_score'] ?? 0) ?: 0),
            'source' => (string) (($r['source'] ?? '') ?: 'manual_entry'),
        ];
    }
    wp_send_json_success(['contacts' => $contacts]);
}

function vmo_list_tags() {
    vmo_require_crm_ajax_security();
    global $wpdb;
    $user_id = get_current_user_id();
    $table = $wpdb->prefix . 'vmo_contacts';
    $has_table = vmo_table_exists($table);
    if (!$has_table) {
        wp_send_json_success(['tags' => []]);
    }
    $columns = function_exists('vmo_crm_contact_columns') ? vmo_crm_contact_columns($table) : [];
    if (!in_array('user_id', $columns, true) || !in_array('tags', $columns, true)) {
        wp_send_json_success(['tags' => []]);
    }
    $rows = $wpdb->get_col($wpdb->prepare("SELECT tags FROM {$table} WHERE user_id=%d AND tags IS NOT NULL AND tags<>'' LIMIT 500", $user_id));
    $set = [];
    foreach (($rows ?: []) as $raw) {
        foreach (array_filter(array_map('trim', explode(',', (string) $raw))) as $t) {
            $k = strtolower($t);
            $set[$k] = $t;
        }
    }
    $tags = array_values($set);
    sort($tags, SORT_NATURAL | SORT_FLAG_CASE);
    wp_send_json_success(['tags' => array_map(function($t){ return ['name' => $t, 'label' => $t]; }, $tags)]);
}

function vmo_save_crm_contact() {
    vmo_require_crm_ajax_security();

    $user_id = get_current_user_id();
    $contact_type = sanitize_key($_POST['type'] ?? $_POST['contact_type'] ?? 'lead');
    $contact_id = isset($_POST['contact_id']) ? absint($_POST['contact_id']) : 0;
    $email = sanitize_email(wp_unslash($_POST['email'] ?? ''));
    $phone = sanitize_text_field(wp_unslash($_POST['phone'] ?? ''));
    $full_name = sanitize_text_field(wp_unslash($_POST['name'] ?? $_POST['full_name'] ?? ''));
    $company = sanitize_text_field(wp_unslash($_POST['company'] ?? ''));
    if ($email === '' && $phone === '' && $full_name === '' && $company === '') {
        wp_send_json_error(['message' => 'Add a name, email, phone, or company.']);
    }

    $allowed_contact_types = ['lead', 'prospect', 'customer', 'partner', 'vendor', 'support', 'referral_source', 'other'];
    if (!in_array($contact_type, $allowed_contact_types, true)) {
        $contact_type = 'lead';
    }
    $lifecycle_stage = vmo_crm_valid_lifecycle_stage($_POST['lifecycle_stage'] ?? (($contact_type === 'customer') ? 'customer' : 'new'));
    if ($contact_type === 'customer') {
        $lifecycle_stage = 'customer';
    }
    $source = vmo_crm_valid_source($_POST['source'] ?? 'manual_entry');
    $street_address = sanitize_textarea_field(wp_unslash($_POST['address'] ?? $_POST['street_address'] ?? ''));

    $contact = vmo_crm_upsert_contact($user_id, [
        'id' => $contact_id,
        'email' => $email,
        'phone' => $phone,
        'full_name' => $full_name,
        'company' => $company,
    ], [
        'source' => $source,
        'primary_category' => ($contact_type === 'customer') ? 'customer' : $contact_type,
        'lifecycle_stage' => $lifecycle_stage,
        'job_title' => sanitize_text_field(wp_unslash($_POST['job_title'] ?? '')),
        'address' => $street_address,
        'city' => sanitize_text_field(wp_unslash($_POST['city'] ?? '')),
        'state' => sanitize_text_field(wp_unslash($_POST['state'] ?? '')),
        'zip' => sanitize_text_field(wp_unslash($_POST['zip'] ?? '')),
        'country' => sanitize_text_field(wp_unslash($_POST['country'] ?? '')),
        'tags' => wp_unslash($_POST['tags'] ?? ''),
        'notes' => sanitize_textarea_field(wp_unslash($_POST['notes'] ?? '')),
    ]);

    if (!$contact) {
        wp_send_json_error(['message' => 'Unable to save contact.']);
    }

    if ($contact_type === 'customer' || $lifecycle_stage === 'customer') {
        vmo_crm_convert_to_customer($user_id, (string) $contact['contact_id'], [
            'type' => 'manual',
            'summary' => 'Manually added or updated as customer from CRM.',
        ]);
    }

    $follow_up_title = sanitize_text_field(wp_unslash($_POST['follow_up_title'] ?? ''));
    $follow_up_date = sanitize_text_field(wp_unslash($_POST['follow_up_date'] ?? ''));
    if ($follow_up_title !== '') {
        global $wpdb;
        $followups_table = $wpdb->prefix . 'vmo_follow_ups';
        if (vmo_table_exists($followups_table)) {
            $scheduled = $follow_up_date ? date('Y-m-d H:i:s', strtotime($follow_up_date)) : current_time('mysql');
            $wpdb->insert($followups_table, [
                'user_id' => $user_id,
                'customer_id' => (int) $contact['id'],
                'call_id' => null,
                'follow_up_type' => 'manual_task',
                'scheduled_date' => $scheduled,
                'status' => 'pending',
                'notes' => $follow_up_title,
                'created_at' => current_time('mysql'),
                'updated_at' => current_time('mysql'),
            ]);
        }
    }

    vmo_crm_log_interaction($user_id, (string) $contact['contact_id'], [
        'interaction_type' => 'manual_note',
        'subtype' => $contact_id ? 'manual_contact_update' : 'manual_contact_created',
        'subject' => $contact_id ? 'Contact updated' : 'Contact created',
        'summary' => $contact_id ? 'Manual CRM contact update.' : 'Manual CRM contact creation.',
        'outcome' => 'successful',
    ]);

    wp_send_json_success(['contact' => $contact, 'message' => 'Contact saved']);
}

function vmo_mark_as_customer() {
    vmo_require_crm_ajax_security();
    $user_id = get_current_user_id();
    $contact_id = isset($_POST['contact_id']) ? (int) $_POST['contact_id'] : 0;
    if (!$contact_id) {
        wp_send_json_error(['message' => 'Missing contact_id']);
    }
    $ok = vmo_crm_convert_to_customer($user_id, $contact_id, [
        'type' => 'manual',
        'summary' => 'Manually marked as customer from CRM.',
    ]);
    if (!$ok) {
        wp_send_json_error(['message' => 'Failed to update']);
    }
    wp_send_json_success(['updated' => true]);
}

function vmo_get_crm_contact_details() {
    vmo_require_crm_ajax_security();
    global $wpdb;
    $user_id = get_current_user_id();
    $id = isset($_POST['contact_id']) ? (int) $_POST['contact_id'] : 0;
    if (!$id) {
        wp_send_json_error(['message' => 'Missing contact_id']);
    }
    $contacts_table = $wpdb->prefix . 'vmo_contacts';
    $contact = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$contacts_table} WHERE id=%d AND user_id=%d", $id, $user_id), ARRAY_A);
    if (!$contact) {
        wp_send_json_error(['message' => 'Contact not found']);
    }
    $cid = (string) ($contact['contact_id'] ?? '');
    $interactions_table = $wpdb->prefix . 'vmo_contact_interactions';
    $purchases_table = $wpdb->prefix . 'vmo_contact_purchases';

    $numeric_id = (string) $contact['id']; // numeric auto-increment id

    $calls = [];
    if ($cid) {
        // Query by both VARCHAR contact_id (UUID) AND the numeric id cast to string,
        // because link_call_to_contact historically stored the numeric id.
        $calls = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT interaction_type, subject, summary, created_at FROM {$interactions_table} WHERE user_id=%d AND (contact_id=%s OR contact_id=%s) ORDER BY created_at DESC LIMIT 25",
                $user_id,
                $cid,
                $numeric_id
            ),
            ARRAY_A
        );
    }

    $purchases = [];
    $total_spent = 0.0;
    if ($cid) {
        $purchases = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT product_service, amount, currency, status, purchase_date FROM {$purchases_table} WHERE user_id=%d AND contact_id=%s ORDER BY purchase_date DESC LIMIT 25",
                $user_id,
                $cid
            ),
            ARRAY_A
        );
        foreach (($purchases ?: []) as $p) {
            $total_spent += (float) ($p['amount'] ?? 0);
        }
    }

    // Get direct phone call data (transcript, recording, summary) from vmo_phone_calls
    $phone_history = [];
    $contact_phone = $contact['phone'] ?? '';
    if (!empty($contact_phone)) {
        $calls_table = $wpdb->prefix . 'vmo_phone_calls';
        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
        if ($wpdb->get_var("SHOW TABLES LIKE '{$calls_table}'") === $calls_table) {
            $phone_history = $wpdb->get_results($wpdb->prepare(
                "SELECT id, caller_number, direction, status, duration, topic, summary, transcript, recording_url, created_at 
                FROM {$calls_table} 
                WHERE user_id = %d AND (caller_number LIKE %s OR caller_number LIKE %s)
                ORDER BY created_at DESC LIMIT 25",
                $user_id,
                '%' . $wpdb->esc_like(preg_replace('/[^0-9]/', '', $contact_phone)) . '%',
                '%' . $wpdb->esc_like($contact_phone) . '%'
            ), ARRAY_A) ?: [];
            // Convert timestamps so browser displays user's local time
            foreach ($phone_history as &$ph) {
                if (!empty($ph['created_at']) && method_exists('VMO_Twilio_Manager', 'to_iso8601')) {
                    $ph['created_at'] = VMO_Twilio_Manager::to_iso8601($ph['created_at']);
                }
            }
            unset($ph);
        }
    }

    // Fetch tasks (follow-ups) associated with this contact
    $contact_tasks = [];
    $followups_table = $wpdb->prefix . 'vmo_follow_ups';
    if (vmo_table_exists($followups_table)) {
        $contact_tasks = $wpdb->get_results($wpdb->prepare(
            "SELECT id, notes AS title, scheduled_date AS due_date, status FROM {$followups_table} WHERE user_id=%d AND customer_id=%d ORDER BY scheduled_date DESC LIMIT 50",
            $user_id, $id
        ), ARRAY_A) ?: [];
        foreach ($contact_tasks as &$ct) {
            $ct['id'] = (int) $ct['id'];
            $ct['completed'] = ($ct['status'] === 'completed');
        }
        unset($ct);
    }

    $email_history = vmo_get_contact_email_history($user_id, $contact['email'] ?? '');
    $timeline = [];
    foreach (($calls ?: []) as $item) {
        $timeline[] = [
            'type' => (string) ($item['interaction_type'] ?? 'interaction'),
            'label' => ucwords(str_replace('_', ' ', (string) ($item['interaction_type'] ?? 'Interaction'))),
            'subject' => (string) ($item['subject'] ?? ''),
            'summary' => (string) ($item['summary'] ?? ''),
            'datetime' => (string) ($item['created_at'] ?? ''),
            'source' => 'interaction',
        ];
    }
    foreach (($phone_history ?: []) as $item) {
        $timeline[] = [
            'type' => 'phone_call',
            'label' => 'Phone Call',
            'subject' => (string) ($item['topic'] ?? 'Phone call'),
            'summary' => (string) ($item['summary'] ?? ($item['status'] ?? '')),
            'datetime' => (string) ($item['created_at'] ?? ''),
            'source' => 'phone',
        ];
    }
    foreach (($email_history ?: []) as $item) {
        $timeline[] = [
            'type' => 'email',
            'label' => (string) (($item['direction'] ?? '') === 'outbound' ? 'Sent Email' : 'Received Email'),
            'subject' => (string) ($item['subject'] ?? 'Email'),
            'summary' => (string) ($item['preview'] ?? ''),
            'datetime' => (string) ($item['created_at'] ?? ''),
            'source' => 'email',
        ];
    }
    foreach (($purchases ?: []) as $item) {
        $timeline[] = [
            'type' => 'purchase',
            'label' => 'Purchase',
            'subject' => (string) ($item['product_service'] ?? 'Purchase'),
            'summary' => trim((string) ($item['status'] ?? 'completed') . ' ' . (string) ($item['currency'] ?? 'USD') . ' ' . (string) ($item['amount'] ?? '')),
            'datetime' => (string) ($item['purchase_date'] ?? ''),
            'source' => 'purchase',
        ];
    }
    foreach (($contact_tasks ?: []) as $item) {
        $timeline[] = [
            'type' => 'task',
            'label' => 'Task',
            'subject' => (string) ($item['title'] ?? 'Task'),
            'summary' => (string) ($item['status'] ?? 'pending'),
            'datetime' => (string) ($item['due_date'] ?? ''),
            'source' => 'follow_up',
        ];
    }
    usort($timeline, function($a, $b) {
        return strtotime($b['datetime'] ?? '0') - strtotime($a['datetime'] ?? '0');
    });
    $timeline = array_slice($timeline, 0, 100);

    $relationship_name = (string) ($contact['full_name'] ?: ($contact['company'] ?: 'This contact'));
    $relationship_type = (string) ($contact['primary_category'] ?: $contact['lifecycle_stage'] ?: 'relationship');
    $interaction_count = count((array) $calls) + count((array) $phone_history) + count((array) $email_history);
    $purchase_count = count((array) $purchases);
    $relationship_summary = sprintf(
        '%s is tracked as %s from %s with %d communication touchpoint%s, %d purchase%s, and $%s total spend. Last contact: %s.',
        $relationship_name,
        str_replace('_', ' ', $relationship_type),
        str_replace('_', ' ', (string) ($contact['source'] ?: 'unknown source')),
        $interaction_count,
        $interaction_count === 1 ? '' : 's',
        $purchase_count,
        $purchase_count === 1 ? '' : 's',
        number_format((float) $total_spent, 2),
        (string) ($contact['last_contact'] ?: 'not recorded')
    );

    $pattern_insight = 'Not enough similar contact history yet to identify a reliable pattern.';
    $source_for_pattern = sanitize_key($contact['source'] ?? '');
    $category_for_pattern = sanitize_key($contact['primary_category'] ?? '');
    if ($source_for_pattern !== '' || $category_for_pattern !== '') {
        $pattern_stats = $wpdb->get_row($wpdb->prepare(
            "SELECT COUNT(*) AS total_contacts, SUM(CASE WHEN lifecycle_stage = 'customer' OR primary_category = 'customer' THEN 1 ELSE 0 END) AS customers, AVG(lead_score) AS avg_lead_score FROM {$contacts_table} WHERE user_id = %d AND id <> %d AND (source = %s OR primary_category = %s)",
            $user_id,
            $id,
            $source_for_pattern,
            $category_for_pattern
        ), ARRAY_A);
        $similar_total = (int) ($pattern_stats['total_contacts'] ?? 0);
        if ($similar_total >= 3) {
            $similar_customers = (int) ($pattern_stats['customers'] ?? 0);
            $conversion_rate = round(($similar_customers / max(1, $similar_total)) * 100);
            $average_score = round((float) ($pattern_stats['avg_lead_score'] ?? 0));
            $pattern_insight = sprintf(
                'Similar %s/%s contacts show a %d%% customer conversion rate across %d records, with an average lead score of %d. Use that pattern to set follow-up urgency and offer strength.',
                str_replace('_', ' ', $source_for_pattern ?: 'source'),
                str_replace('_', ' ', $category_for_pattern ?: 'category'),
                $conversion_rate,
                $similar_total,
                $average_score
            );
        }
    }

    $tag_list = [];
    if (!empty($contact['tags'])) {
        $tag_list = array_values(array_filter(array_map('trim', explode(',', (string) $contact['tags']))));
    }
    $address_parts = array_filter([
        (string) ($contact['address'] ?? ''),
        (string) ($contact['city'] ?? ''),
        (string) ($contact['state'] ?? ''),
        (string) ($contact['zip'] ?? ''),
        (string) ($contact['country'] ?? ''),
    ]);

    $payload = [
        'contact' => [
            'id' => (int) $contact['id'],
            'contact_id' => $cid,
            'name' => (string) ($contact['full_name'] ?: trim(($contact['first_name'] ?? '') . ' ' . ($contact['last_name'] ?? ''))),
            'email' => (string) ($contact['email'] ?? ''),
            'phone' => (string) ($contact['phone'] ?? ''),
            'company' => (string) ($contact['company'] ?? ''),
            'job_title' => (string) ($contact['job_title'] ?? ''),
            'address' => implode(', ', $address_parts),
            'street_address' => (string) ($contact['address'] ?? ''),
            'city' => (string) ($contact['city'] ?? ''),
            'state' => (string) ($contact['state'] ?? ''),
            'zip' => (string) ($contact['zip'] ?? ''),
            'country' => (string) ($contact['country'] ?? ''),
            'notes' => (string) ($contact['notes'] ?? ''),
            'tags' => $tag_list,
            'created_at' => (string) ($contact['created_at'] ?? ''),
            'date_added' => (string) ($contact['created_at'] ?? ''),
            'lifecycle_stage' => (string) ($contact['lifecycle_stage'] ?? ''),
            'primary_category' => (string) ($contact['primary_category'] ?? ''),
            'source' => (string) ($contact['source'] ?? ''),
            'engagement_level' => (string) ($contact['engagement_level'] ?? ''),
            'lead_score' => (int) ($contact['lead_score'] ?? 0),
            'last_contact' => (string) ($contact['last_contact'] ?? ''),
            'type' => (($contact['lifecycle_stage'] ?? '') === 'customer' || ($contact['primary_category'] ?? '') === 'customer') ? 'customer' : 'lead',
        ],
        'calls' => is_array($calls) ? $calls : [],
        'phone_history' => $phone_history,
        'email_history' => $email_history,
        'purchases' => is_array($purchases) ? $purchases : [],
        'total_spent' => $total_spent,
        'tasks' => $contact_tasks,
        'timeline' => $timeline,
        'relationship_summary' => $relationship_summary,
        'pattern_insight' => $pattern_insight,
        'retention_recommendation' => vmo_crm_generate_retention_recommendation($user_id, $id),
        'ai_recommendation' => vmo_crm_generate_retention_recommendation($user_id, $id),
    ];
    wp_send_json_success($payload);
}

/**
 * Set tags on a CRM contact.
 */
add_action('wp_ajax_vmo_set_contact_tags', function() {
    vmo_require_crm_ajax_security();
    global $wpdb;
    $user_id = get_current_user_id();
    $contact_id = isset($_POST['contact_id']) ? (int) $_POST['contact_id'] : 0;
    if (!$contact_id) {
        wp_send_json_error(['message' => 'Missing contact_id']);
    }
    // Accept either comma-separated string or JSON array
    $tags = '';
    if (!empty($_POST['tags_json'])) {
        $arr = json_decode(wp_unslash($_POST['tags_json']), true);
        if (is_array($arr)) {
            $tags = implode(',', array_map('sanitize_text_field', $arr));
        }
    } elseif (isset($_POST['tags'])) {
        $tags = sanitize_text_field(wp_unslash($_POST['tags']));
    }
    $table = $wpdb->prefix . 'vmo_contacts';
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    $ok = $wpdb->update($table, ['tags' => $tags, 'updated_at' => current_time('mysql')], ['id' => $contact_id, 'user_id' => $user_id]);
    if ($ok === false) {
        wp_send_json_error(['message' => 'Failed to update tags']);
    }
    wp_send_json_success(['message' => 'Tags saved']);
});

/**
 * Add a task (follow-up) for a CRM contact.
 */
add_action('wp_ajax_vmo_add_task', function() {
    vmo_require_crm_ajax_security();
    global $wpdb;
    $user_id = get_current_user_id();
    $contact_id = isset($_POST['contact_id']) ? (int) $_POST['contact_id'] : 0;
    $title = sanitize_text_field($_POST['title'] ?? '');
    $due_date = sanitize_text_field($_POST['due_date'] ?? '');
    if (!$title) {
        wp_send_json_error(['message' => 'Title is required']);
    }
    $table = $wpdb->prefix . 'vmo_follow_ups';
    $exists = vmo_table_exists($table);
    if (!$exists) {
        wp_send_json_error(['message' => 'Follow-ups table not available']);
    }
    $scheduled = $due_date ? date('Y-m-d H:i:s', strtotime($due_date)) : current_time('mysql');
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    $res = $wpdb->insert($table, [
        'user_id' => $user_id,
        'customer_id' => $contact_id,
        'call_id' => null,
        'follow_up_type' => 'manual_task',
        'scheduled_date' => $scheduled,
        'status' => 'pending',
        'notes' => $title,
        'created_at' => current_time('mysql'),
        'updated_at' => current_time('mysql'),
    ]);
    if (!$res) {
        wp_send_json_error(['message' => 'Failed to add task']);
    }
    wp_send_json_success(['message' => 'Task added', 'id' => $wpdb->insert_id]);
});

/**
 * Complete a CRM contact task (follow-up).
 */
add_action('wp_ajax_vmo_complete_task', function() {
    vmo_require_crm_ajax_security();
    global $wpdb;
    $id = isset($_POST['id']) ? (int) $_POST['id'] : 0;
    if ($id <= 0) {
        wp_send_json_error(['message' => 'Invalid task ID']);
    }
    $table = $wpdb->prefix . 'vmo_follow_ups';
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    $res = $wpdb->update($table, ['status' => 'completed', 'updated_at' => current_time('mysql')], ['id' => $id, 'user_id' => get_current_user_id()]);
    if ($res === false) {
        wp_send_json_error(['message' => 'Failed to complete task']);
    }
    wp_send_json_success(['message' => 'Task completed']);
});

/**
 * Delete a CRM contact task (follow-up).
 */
add_action('wp_ajax_vmo_delete_task', function() {
    vmo_require_crm_ajax_security();
    global $wpdb;
    $id = isset($_POST['id']) ? (int) $_POST['id'] : 0;
    if ($id <= 0) {
        wp_send_json_error(['message' => 'Invalid task ID']);
    }
    $table = $wpdb->prefix . 'vmo_follow_ups';
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    $res = $wpdb->delete($table, ['id' => $id, 'user_id' => get_current_user_id()]);
    if ($res === false) {
        wp_send_json_error(['message' => 'Failed to delete task']);
    }
    wp_send_json_success(['message' => 'Task deleted']);
});

/**
 * Refresh document recommendations using AI context.
 */
add_action('wp_ajax_vmo_ai_refresh_document_recs', function() {
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    // Nonce check — this action uses _ajax_nonce with the builder nonce
    $nonce = sanitize_text_field($_POST['_ajax_nonce'] ?? $_POST['security'] ?? $_POST['nonce'] ?? '');
    if ($nonce && !wp_verify_nonce($nonce, 'vmo_builder_nonce') && !wp_verify_nonce($nonce, 'vmo_admin_nonce')) {
        wp_send_json_error(['message' => 'Invalid nonce'], 403);
    }
    // Return recommendation adjustments based on consultation context
    $user_id = get_current_user_id();
    $recs = [];
    // Check for consultation data to inform priority
    $consultation_table = $GLOBALS['wpdb']->prefix . 'vmo_consultations';
    $has_consultation = (bool) $GLOBALS['wpdb']->get_var($GLOBALS['wpdb']->prepare("SHOW TABLES LIKE %s", $GLOBALS['wpdb']->esc_like($consultation_table)));
    $completed = false;
    if ($has_consultation) {
        $completed = (bool) $GLOBALS['wpdb']->get_var($GLOBALS['wpdb']->prepare(
            "SELECT COUNT(*) FROM {$consultation_table} WHERE user_id=%d AND status IN ('completed','implemented','generating_media')", $user_id
        ));
    }
    if ($completed) {
        // Prioritize business-critical documents
        $critical_docs = ['privacy_policy', 'terms_of_service', 'business_plan'];
        foreach ($critical_docs as $doc_id) {
            $recs[] = ['id' => $doc_id, 'priority' => 'critical', 'selected' => true, 'summary' => 'Essential for your launched business.'];
        }
    }
    wp_send_json_success($recs);
});

/**
 * Get email history for a contact by email address.
 */
function vmo_get_contact_email_history($user_id, $email) {
    if (empty($email)) return [];
    global $wpdb;
    $all_emails = [];

    // Inbound / general emails
    $emails_table = $wpdb->prefix . 'vmo_emails';
    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
    if ($wpdb->get_var("SHOW TABLES LIKE '{$emails_table}'") === $emails_table) {
        $rows = $wpdb->get_results($wpdb->prepare(
            "SELECT id, subject, body_preview, direction, from_email, to_email, created_at 
            FROM {$emails_table} 
            WHERE user_id = %d AND (from_email = %s OR to_email = %s)
            ORDER BY created_at DESC LIMIT 50",
            $user_id, $email, $email
        ), ARRAY_A) ?: [];
        foreach ($rows as $em) {
            $all_emails[] = [
                'id' => (int) $em['id'],
                'subject' => $em['subject'] ?: 'Email',
                'preview' => $em['body_preview'] ?: '',
                'direction' => $em['direction'] ?: 'inbound',
                'from' => $em['from_email'] ?? '',
                'to' => $em['to_email'] ?? '',
                'created_at' => $em['created_at'],
                'source' => 'inbox',
            ];
        }
    }

    // Sent emails
    $sent_table = $wpdb->prefix . 'vmo_sent_emails';
    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
    if ($wpdb->get_var("SHOW TABLES LIKE '{$sent_table}'") === $sent_table) {
        $rows = $wpdb->get_results($wpdb->prepare(
            "SELECT id, subject, body, to_email, created_at 
            FROM {$sent_table} 
            WHERE user_id = %d AND to_email = %s
            ORDER BY created_at DESC LIMIT 50",
            $user_id, $email
        ), ARRAY_A) ?: [];
        foreach ($rows as $em) {
            $all_emails[] = [
                'id' => (int) $em['id'],
                'subject' => $em['subject'] ?: 'Sent Email',
                'preview' => wp_strip_all_tags(substr($em['body'] ?? '', 0, 120)),
                'direction' => 'outbound',
                'from' => '',
                'to' => $em['to_email'] ?? '',
                'created_at' => $em['created_at'],
                'source' => 'sent',
            ];
        }
    }

    // Sort by date descending
    usort($all_emails, function($a, $b) {
        return strtotime($b['created_at'] ?? '0') - strtotime($a['created_at'] ?? '0');
    });

    return array_slice($all_emails, 0, 50);
}

/**
 * Get communications history for a contact
 * Includes emails, phone calls, and chat messages
 */
function vmo_get_contact_communications() {
    vmo_require_crm_ajax_security();
    global $wpdb;
    $user_id = get_current_user_id();
    $contact_id = isset($_POST['contact_id']) ? (int) $_POST['contact_id'] : 0;
    $type = isset($_POST['type']) ? sanitize_key($_POST['type']) : 'all';
    
    if (!$contact_id) {
        wp_send_json_error(['message' => 'Missing contact_id']);
    }
    
    // Get contact info to find phone/email
    $contacts_table = $wpdb->prefix . 'vmo_contacts';
    $contact = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$contacts_table} WHERE id=%d AND user_id=%d", $contact_id, $user_id), ARRAY_A);
    if (!$contact) {
        wp_send_json_error(['message' => 'Contact not found']);
    }
    
    $communications = [];
    $email = $contact['email'] ?? '';
    $phone = $contact['phone'] ?? '';
    $cid = $contact['contact_id'] ?? '';
    
    // Get Phone Calls
    if ($type === 'all' || $type === 'phone') {
        $calls_table = $wpdb->prefix . 'vmo_phone_calls';
        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
        if ($wpdb->get_var("SHOW TABLES LIKE '{$calls_table}'") === $calls_table && $phone) {
            $calls = $wpdb->get_results($wpdb->prepare(
                "SELECT id, call_sid, caller_number, status, duration, topic, summary, transcript, recording_url, created_at 
                FROM {$calls_table} 
                WHERE user_id = %d AND (caller_number LIKE %s OR caller_number LIKE %s)
                ORDER BY created_at DESC LIMIT 50",
                $user_id, 
                '%' . $wpdb->esc_like(preg_replace('/[^0-9]/', '', $phone)) . '%',
                '%' . $wpdb->esc_like($phone) . '%'
            ), ARRAY_A);
            
            foreach ($calls ?: [] as $call) {
                $communications[] = [
                    'type' => 'phone',
                    'id' => (int) $call['id'],
                    'subject' => $call['topic'] ?: 'Phone Call',
                    'preview' => $call['summary'] ?: 'Call with ' . $call['caller_number'],
                    'transcript' => $call['transcript'] ?? '',
                    'recording_url' => $call['recording_url'] ?? '',
                    'datetime' => $call['created_at'],
                    'direction' => 'inbound',
                    'duration' => (int) $call['duration'],
                    'status' => $call['status'],
                ];
            }
        }
    }
    
    // Get Emails
    if ($type === 'all' || $type === 'email') {
        $emails_table = $wpdb->prefix . 'vmo_emails';
        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
        if ($wpdb->get_var("SHOW TABLES LIKE '{$emails_table}'") === $emails_table && $email) {
            $emails = $wpdb->get_results($wpdb->prepare(
                "SELECT id, subject, body_preview, direction, created_at 
                FROM {$emails_table} 
                WHERE user_id = %d AND (from_email = %s OR to_email = %s)
                ORDER BY created_at DESC LIMIT 50",
                $user_id, $email, $email
            ), ARRAY_A);
            
            foreach ($emails ?: [] as $em) {
                $communications[] = [
                    'type' => 'email',
                    'id' => (int) $em['id'],
                    'subject' => $em['subject'] ?: 'Email',
                    'preview' => $em['body_preview'] ?: '',
                    'datetime' => $em['created_at'],
                    'direction' => $em['direction'] ?: 'outbound',
                ];
            }
        }
        
        // Also check sent emails table
        $sent_table = $wpdb->prefix . 'vmo_sent_emails';
        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
        if ($wpdb->get_var("SHOW TABLES LIKE '{$sent_table}'") === $sent_table && $email) {
            $sent_emails = $wpdb->get_results($wpdb->prepare(
                "SELECT id, subject, body, created_at 
                FROM {$sent_table} 
                WHERE user_id = %d AND to_email = %s
                ORDER BY created_at DESC LIMIT 50",
                $user_id, $email
            ), ARRAY_A);
            
            foreach ($sent_emails ?: [] as $em) {
                $communications[] = [
                    'type' => 'email',
                    'id' => (int) $em['id'],
                    'subject' => $em['subject'] ?: 'Sent Email',
                    'preview' => wp_strip_all_tags(substr($em['body'] ?? '', 0, 100)),
                    'datetime' => $em['created_at'],
                    'direction' => 'outbound',
                ];
            }
        }
    }
    
    // Get Chat messages
    if ($type === 'all' || $type === 'chat') {
        $chat_table = $wpdb->prefix . 'vmo_chat_history';
        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
        if ($wpdb->get_var("SHOW TABLES LIKE '{$chat_table}'") === $chat_table && $cid) {
            $chats = $wpdb->get_results($wpdb->prepare(
                "SELECT id, message, message_role, created_at 
                FROM {$chat_table} 
                WHERE user_id = %d AND contact_id = %s
                ORDER BY created_at DESC LIMIT 50",
                $user_id, $cid
            ), ARRAY_A);
            
            // Group chats by conversation (same date)
            $chat_groups = [];
            foreach ($chats ?: [] as $chat) {
                $date = date('Y-m-d', strtotime($chat['created_at']));
                if (!isset($chat_groups[$date])) {
                    $chat_groups[$date] = [
                        'type' => 'chat',
                        'id' => (int) $chat['id'],
                        'subject' => 'Chat Conversation',
                        'preview' => $chat['message'],
                        'datetime' => $chat['created_at'],
                        'direction' => '',
                    ];
                }
            }
            foreach ($chat_groups as $cg) {
                $communications[] = $cg;
            }
        }
    }
    
    // Sort by datetime descending
    usort($communications, function($a, $b) {
        return strtotime($b['datetime']) - strtotime($a['datetime']);
    });
    
    wp_send_json_success(['communications' => array_slice($communications, 0, 100)]);
}

/**
 * Get detailed communication content
 */
function vmo_get_communication_detail() {
    vmo_require_crm_ajax_security();
    global $wpdb;
    $user_id = get_current_user_id();
    $type = isset($_POST['type']) ? sanitize_key($_POST['type']) : '';
    $id = isset($_POST['id']) ? (int) $_POST['id'] : 0;
    
    if (!$type || !$id) {
        wp_send_json_error(['message' => 'Missing type or id']);
    }
    
    $data = [];
    
    if ($type === 'phone') {
        $calls_table = $wpdb->prefix . 'vmo_phone_calls';
        $call = $wpdb->get_row($wpdb->prepare(
            "SELECT * FROM {$calls_table} WHERE id = %d AND user_id = %d",
            $id, $user_id
        ), ARRAY_A);
        
        if ($call) {
            $data = [
                'direction' => $call['direction'] ?? 'inbound',
                'duration' => (int) ($call['duration'] ?? 0),
                'datetime' => $call['created_at'],
                'status' => $call['status'],
                'recording_url' => $call['recording_url'] ?? $call['recording_local_path'] ?? '',
                'summary' => $call['summary'] ?? '',
                'transcript' => $call['transcript'] ?? '',
            ];
        }
    } elseif ($type === 'email') {
        // Check both emails tables
        $emails_table = $wpdb->prefix . 'vmo_emails';
        $email = $wpdb->get_row($wpdb->prepare(
            "SELECT * FROM {$emails_table} WHERE id = %d AND user_id = %d",
            $id, $user_id
        ), ARRAY_A);
        
        if ($email) {
            $data = [
                'subject' => $email['subject'] ?? '',
                'from' => $email['from_email'] ?? '',
                'to' => $email['to_email'] ?? '',
                'datetime' => $email['created_at'],
                'body' => $email['body'] ?? '',
                'body_html' => $email['body_html'] ?? $email['body'] ?? '',
            ];
        } else {
            // Check sent emails table
            $sent_table = $wpdb->prefix . 'vmo_sent_emails';
            $sent = $wpdb->get_row($wpdb->prepare(
                "SELECT * FROM {$sent_table} WHERE id = %d AND user_id = %d",
                $id, $user_id
            ), ARRAY_A);
            
            if ($sent) {
                $data = [
                    'subject' => $sent['subject'] ?? '',
                    'from' => get_user_meta($user_id, 'business_email', true) ?: 'You',
                    'to' => $sent['to_email'] ?? '',
                    'datetime' => $sent['created_at'],
                    'body' => $sent['body'] ?? '',
                    'body_html' => $sent['body_html'] ?? $sent['body'] ?? '',
                ];
            }
        }
    } elseif ($type === 'chat') {
        $chat_table = $wpdb->prefix . 'vmo_chat_history';
        // Get the conversation from that day
        $chat = $wpdb->get_row($wpdb->prepare(
            "SELECT created_at FROM {$chat_table} WHERE id = %d AND user_id = %d",
            $id, $user_id
        ), ARRAY_A);
        
        if ($chat) {
            $date = date('Y-m-d', strtotime($chat['created_at']));
            $messages = $wpdb->get_results($wpdb->prepare(
                "SELECT message, message_role, created_at 
                FROM {$chat_table} 
                WHERE user_id = %d AND DATE(created_at) = %s
                ORDER BY created_at ASC LIMIT 100",
                $user_id, $date
            ), ARRAY_A);
            
            $data = [
                'datetime' => $chat['created_at'],
                'messages' => array_map(function($m) {
                    return [
                        'role' => $m['message_role'],
                        'content' => $m['message'],
                        'time' => $m['created_at'],
                    ];
                }, $messages ?: []),
            ];
        }
    }
    
    if (empty($data)) {
        wp_send_json_error(['message' => 'Communication not found']);
    }
    
    wp_send_json_success($data);
}

/**
 * Generate AI recommendations for a contact based on all communications
 */
function vmo_generate_contact_recommendations() {
    vmo_require_crm_ajax_security();
    // Rate limit AI calls
    $rate_check = vmo_check_ai_rate_limit(get_current_user_id(), 'contact_rec');
    if (is_wp_error($rate_check)) { wp_send_json_error(['message' => $rate_check->get_error_message()], 429); }
    global $wpdb;
    $user_id = get_current_user_id();
    $contact_id = isset($_POST['contact_id']) ? (int) $_POST['contact_id'] : 0;
    
    if (!$contact_id) {
        wp_send_json_error(['message' => 'Missing contact_id']);
    }

    // ✅ TOKEN SECURITY CHECK v8.1.0: Prevent negative arbitrage
    $current_balance = (int) get_user_meta($user_id, 'vmo_token_balance', true);
    if ($current_balance < 100) {
        wp_send_json_error(['message' => 'Insufficient tokens. Minimum balance of 100 tokens required for this analysis.']);
    }
    
    // Get contact info
    $contacts_table = $wpdb->prefix . 'vmo_contacts';
    $contact = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$contacts_table} WHERE id=%d AND user_id=%d", $contact_id, $user_id), ARRAY_A);
    if (!$contact) {
        wp_send_json_error(['message' => 'Contact not found']);
    }
    
    $name = $contact['full_name'] ?: trim(($contact['first_name'] ?? '') . ' ' . ($contact['last_name'] ?? ''));
    $email = $contact['email'] ?? '';
    $phone = $contact['phone'] ?? '';
    $stage = $contact['lifecycle_stage'] ?? 'lead';
    
    // Gather communication summary
    $communications_summary = "Contact: {$name}\nEmail: {$email}\nPhone: {$phone}\nLifecycle Stage: {$stage}\n\n";
    
    // Get recent calls (include full transcripts so AI has maximum context)
    $calls_table = $wpdb->prefix . 'vmo_phone_calls';
    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
    if ($wpdb->get_var("SHOW TABLES LIKE '{$calls_table}'") === $calls_table && $phone) {
        $calls = $wpdb->get_results($wpdb->prepare(
            "SELECT topic, summary, transcript, duration, created_at FROM {$calls_table} 
            WHERE user_id = %d AND caller_number LIKE %s 
            ORDER BY created_at DESC LIMIT 5",
            $user_id, '%' . $wpdb->esc_like(preg_replace('/[^0-9]/', '', $phone)) . '%'
        ), ARRAY_A);
        
        if ($calls) {
            $communications_summary .= "Recent Phone Calls:\n";
            foreach ($calls as $c) {
                $communications_summary .= "- {$c['created_at']}: {$c['topic']} ({$c['duration']}s)\n";
                if ($c['summary']) $communications_summary .= "  Summary: {$c['summary']}\n";
                if ($c['transcript']) $communications_summary .= "  Full Transcript:\n{$c['transcript']}\n";
            }
            $communications_summary .= "\n";
        }
    }
    
    // Get purchases
    $purchases_table = $wpdb->prefix . 'vmo_contact_purchases';
    $cid = $contact['contact_id'] ?? '';
    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
    if ($wpdb->get_var("SHOW TABLES LIKE '{$purchases_table}'") === $purchases_table && $cid) {
        $purchases = $wpdb->get_results($wpdb->prepare(
            "SELECT product_service, amount, purchase_date FROM {$purchases_table} 
            WHERE user_id = %d AND contact_id = %s 
            ORDER BY purchase_date DESC LIMIT 5",
            $user_id, $cid
        ), ARRAY_A);
        
        if ($purchases) {
            $communications_summary .= "Purchase History:\n";
            foreach ($purchases as $p) {
                $communications_summary .= "- {$p['purchase_date']}: {$p['product_service']} (\${$p['amount']})\n";
            }
            $communications_summary .= "\n";
        }
    }
    
    // Get business context
    $business_name = get_user_meta($user_id, 'business_name', true) ?: 'the business';
    $business_type = get_user_meta($user_id, 'business_type', true) ?: 'business';
    
    // Generate AI recommendations
    $prompt = "You are an expert business consultant analyzing a contact for {$business_name} ({$business_type}).

Based on the following contact information and communication history, provide specific, actionable recommendations to convert or retain this contact.

{$communications_summary}

Provide 3-5 specific recommendations. Be concise and actionable. Focus on:
1. Best next action to take
2. Timing suggestions
3. Communication channel recommendations
4. Potential upsell/cross-sell opportunities if applicable
5. Relationship strengthening tactics

Format as a bulleted list with clear, actionable items.";

    $api_key = get_option('vmo_openai_api_key') ?: VMO_OPENAI_API_KEY;
    if (!$api_key) {
        wp_send_json_error(['message' => 'AI not configured']);
    }
    
    // ✅ FIX: Use centralized model registry
    // ✅ Guardrail 5A: Adaptive model fallback when balance is low
    $_crm_model = function_exists('vmo_get_model') ? vmo_get_model('nano') : 'gpt-5.5-nano';
    if (function_exists('vmo_resolve_model')) {
        $_crm_model = vmo_resolve_model(get_current_user_id(), $_crm_model)['model'];
    }
    $_crm_body = array_merge([
        'model' => $_crm_model,
        'messages' => [
            ['role' => 'system', 'content' => 'You are a helpful business consultant providing actionable CRM recommendations.'],
            ['role' => 'user', 'content' => $prompt],
        ],
        'temperature' => 0.7,
    ], function_exists('vmo_get_token_param') ? vmo_get_token_param($_crm_model, 500) : ['max_tokens' => 500]);

    $response = wp_remote_post('https://api.openai.com/v1/chat/completions', [
        'timeout' => 60,
        'headers' => [
            'Authorization' => 'Bearer ' . $api_key,
            'Content-Type' => 'application/json',
        ],
        'body' => json_encode($_crm_body),
    ]);
    
    if (is_wp_error($response)) {
        wp_send_json_error(['message' => 'AI request failed: ' . $response->get_error_message()]);
    }
    
    $body = json_decode(wp_remote_retrieve_body($response), true);
    $recommendations = $body['choices'][0]['message']['content'] ?? 'Unable to generate recommendations at this time.';
    
    // ✅ TOKEN LEAK FIX v8.2.1: Use centralized deduction with FIFO, admin bypass & audit trail
    $cost = vmo_get_token_cost('contact_insights');
    vmo_deduct_tokens($user_id, $cost, 'contact_insights', 'AI contact recommendations for contact #' . $contact_id);
    
    wp_send_json_success(['recommendations' => $recommendations]);
}

// ========================================
// QUARTERLY BUSINESS INSIGHTS FUNCTIONS
// ========================================

/**
 * Generate quarterly business insights from all communications
 */
function vmo_generate_quarterly_insights() {
    if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['nonce'] ?? $_REQUEST['security'] ?? '')), 'vmo_nonce')) { wp_send_json_error(['message' => 'Security check failed'], 403); }
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if (!vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Invalid nonce', 'status' => 'invalid_nonce'], 403);
    }
    // Rate limit AI calls
    $rate_check = vmo_check_ai_rate_limit(get_current_user_id(), 'quarterly_insights', 2, 10);
    if (is_wp_error($rate_check)) { wp_send_json_error(['message' => $rate_check->get_error_message()], 429); }
    
    global $wpdb;
    $user_id = get_current_user_id();

    // ✅ TOKEN SECURITY CHECK v8.1.0: Prevent negative arbitrage
    $current_balance = (int) get_user_meta($user_id, 'vmo_token_balance', true);
    if ($current_balance < 200) {
        wp_send_json_error(['message' => 'Insufficient tokens. Minimum balance of 200 tokens required for quarterly insights.']);
    }
    
    // Calculate current quarter
    $current_month = (int) date('n');
    $current_year = (int) date('Y');
    $quarter = ceil($current_month / 3);
    $period = "Q{$quarter} {$current_year}";
    
    // Quarter date range
    $quarter_start_month = (($quarter - 1) * 3) + 1;
    $quarter_start = date('Y-m-d', mktime(0, 0, 0, $quarter_start_month, 1, $current_year));
    $quarter_end = date('Y-m-d', mktime(23, 59, 59, $quarter_start_month + 2, date('t', mktime(0, 0, 0, $quarter_start_month + 2, 1, $current_year)), $current_year));
    
    $metrics = [
        'emails' => 0,
        'calls' => 0,
        'chats' => 0,
    ];
    
    $communications_summary = "Business Communications Summary for {$period}\n\n";
    
    // Get email stats
    $emails_table = $wpdb->prefix . 'vmo_email_sends';
    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
    if ($wpdb->get_var("SHOW TABLES LIKE '{$emails_table}'") === $emails_table) {
        $email_count = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$emails_table} WHERE user_id = %d AND created_at BETWEEN %s AND %s",
            $user_id, $quarter_start, $quarter_end . ' 23:59:59'
        ));
        $metrics['emails'] = $email_count;
        
        // Get top email subjects/campaigns
        $top_emails = $wpdb->get_results($wpdb->prepare(
            "SELECT subject, COUNT(*) as cnt FROM {$emails_table} 
            WHERE user_id = %d AND created_at BETWEEN %s AND %s 
            GROUP BY subject ORDER BY cnt DESC LIMIT 5",
            $user_id, $quarter_start, $quarter_end . ' 23:59:59'
        ), ARRAY_A);
        
        if ($top_emails) {
            $communications_summary .= "EMAIL ACTIVITY ({$email_count} total):\n";
            foreach ($top_emails as $e) {
                $communications_summary .= "- \"{$e['subject']}\" ({$e['cnt']} sends)\n";
            }
            $communications_summary .= "\n";
        }
    }
    
    // Get phone call stats
    $calls_table = $wpdb->prefix . 'vmo_phone_calls';
    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
    if ($wpdb->get_var("SHOW TABLES LIKE '{$calls_table}'") === $calls_table) {
        $call_count = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$calls_table} WHERE user_id = %d AND created_at BETWEEN %s AND %s",
            $user_id, $quarter_start, $quarter_end . ' 23:59:59'
        ));
        $metrics['calls'] = $call_count;
        
        // Get call summaries
        $call_summaries = $wpdb->get_results($wpdb->prepare(
            "SELECT topic, summary, duration, direction FROM {$calls_table} 
            WHERE user_id = %d AND created_at BETWEEN %s AND %s AND summary IS NOT NULL AND summary != ''
            ORDER BY created_at DESC LIMIT 10",
            $user_id, $quarter_start, $quarter_end . ' 23:59:59'
        ), ARRAY_A);
        
        $inbound = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$calls_table} WHERE user_id = %d AND created_at BETWEEN %s AND %s AND direction = 'inbound'",
            $user_id, $quarter_start, $quarter_end . ' 23:59:59'
        ));
        $outbound = $call_count - $inbound;
        
        $communications_summary .= "PHONE CALLS ({$call_count} total - {$inbound} inbound, {$outbound} outbound):\n";
        if ($call_summaries) {
            $communications_summary .= "Recent call topics:\n";
            foreach ($call_summaries as $c) {
                $dir = ucfirst($c['direction'] ?? 'unknown');
                $duration = round(($c['duration'] ?? 0) / 60, 1);
                $communications_summary .= "- [{$dir}] {$c['topic']} ({$duration} min)\n";
            }
        }
        $communications_summary .= "\n";
    }
    
    // Get chat stats
    $chat_table = $wpdb->prefix . 'vmo_chat_sessions';
    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
    if ($wpdb->get_var("SHOW TABLES LIKE '{$chat_table}'") === $chat_table) {
        $chat_count = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$chat_table} WHERE user_id = %d AND created_at BETWEEN %s AND %s",
            $user_id, $quarter_start, $quarter_end . ' 23:59:59'
        ));
        $metrics['chats'] = $chat_count;
        
        // Get recent chat summaries if available
        $chat_summaries = $wpdb->get_results($wpdb->prepare(
            "SELECT summary, visitor_name FROM {$chat_table} 
            WHERE user_id = %d AND created_at BETWEEN %s AND %s AND summary IS NOT NULL AND summary != ''
            ORDER BY created_at DESC LIMIT 5",
            $user_id, $quarter_start, $quarter_end . ' 23:59:59'
        ), ARRAY_A);
        
        $communications_summary .= "CHAT SESSIONS ({$chat_count} total):\n";
        if ($chat_summaries) {
            foreach ($chat_summaries as $c) {
                $visitor = $c['visitor_name'] ?: 'Visitor';
                $summary = substr($c['summary'], 0, 100) . (strlen($c['summary']) > 100 ? '...' : '');
                $communications_summary .= "- {$visitor}: {$summary}\n";
            }
        }
        $communications_summary .= "\n";
    }
    
    // Get contact and conversion stats
    $contacts_table = $wpdb->prefix . 'vmo_contacts';
    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
    if ($wpdb->get_var("SHOW TABLES LIKE '{$contacts_table}'") === $contacts_table) {
        $new_contacts = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$contacts_table} WHERE user_id = %d AND created_at BETWEEN %s AND %s",
            $user_id, $quarter_start, $quarter_end . ' 23:59:59'
        ));
        $conversions = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$contacts_table} 
            WHERE user_id = %d AND created_at BETWEEN %s AND %s AND lifecycle_stage = 'customer'",
            $user_id, $quarter_start, $quarter_end . ' 23:59:59'
        ));
        
        $communications_summary .= "CONTACTS & CONVERSIONS:\n";
        $communications_summary .= "- New contacts added: {$new_contacts}\n";
        $communications_summary .= "- Conversions to customer: {$conversions}\n\n";
    }
    
    // Get business context
    $business_name = get_user_meta($user_id, 'business_name', true) ?: 'the business';
    $business_type = get_user_meta($user_id, 'business_type', true) ?: 'business';
    
    // Generate AI insights
    $prompt = "You are an expert business consultant analyzing quarterly performance for {$business_name} ({$business_type}).

Based on the following quarterly communications data, provide:
1. A brief summary (2-3 sentences) of overall communication performance
2. 4-5 specific, actionable recommendations to improve business outcomes next quarter

{$communications_summary}

Be specific and actionable. Focus on patterns you notice and opportunities for improvement.

Format your response as:
SUMMARY:
[Your 2-3 sentence summary here]

RECOMMENDATIONS:
1. [First recommendation]
2. [Second recommendation]
3. [Third recommendation]
4. [Fourth recommendation]
5. [Fifth recommendation - optional]";

    $api_key = get_option('vmo_openai_api_key') ?: VMO_OPENAI_API_KEY;
    if (!$api_key) {
        wp_send_json_error(['message' => 'AI not configured']);
    }
    
    // ✅ FIX: Use centralized model registry
    // ✅ Guardrail 5A: Adaptive model fallback when balance is low
    $_qi_model = function_exists('vmo_get_model') ? vmo_get_model('nano') : 'gpt-5.5-nano';
    if (function_exists('vmo_resolve_model')) {
        $_qi_model = vmo_resolve_model(get_current_user_id(), $_qi_model)['model'];
    }
    $_qi_body = array_merge([
        'model' => $_qi_model,
        'messages' => [
            ['role' => 'system', 'content' => 'You are a helpful business consultant providing quarterly insights and recommendations.'],
            ['role' => 'user', 'content' => $prompt],
        ],
        'temperature' => 0.7,
    ], function_exists('vmo_get_token_param') ? vmo_get_token_param($_qi_model, 800) : ['max_tokens' => 800]);

    $response = wp_remote_post('https://api.openai.com/v1/chat/completions', [
        'timeout' => 55, // Capped: Kinsta 60s PHP limit
        'headers' => [
            'Authorization' => 'Bearer ' . $api_key,
            'Content-Type' => 'application/json',
        ],
        'body' => json_encode($_qi_body),
    ]);

    if (is_wp_error($response)) {
        wp_send_json_error(['message' => 'AI request failed: ' . $response->get_error_message()]);
    }
    
    $body = json_decode(wp_remote_retrieve_body($response), true);
    $ai_response = $body['choices'][0]['message']['content'] ?? '';
    
    // Parse the response
    $summary = '';
    $recommendations = [];
    
    if (preg_match('/SUMMARY:\s*(.+?)(?=RECOMMENDATIONS:|$)/is', $ai_response, $match)) {
        $summary = trim($match[1]);
    }
    
    if (preg_match('/RECOMMENDATIONS:\s*(.+)/is', $ai_response, $match)) {
        $rec_text = trim($match[1]);
        preg_match_all('/\d+\.\s*(.+?)(?=\d+\.|$)/s', $rec_text, $rec_matches);
        if (!empty($rec_matches[1])) {
            foreach ($rec_matches[1] as $r) {
                $clean = trim($r);
                if ($clean) $recommendations[] = $clean;
            }
        }
    }
    
    // Fallback if parsing failed
    if (!$summary && !$recommendations) {
        $summary = $ai_response;
    }
    
    // Save report to database
    $reports_table = $wpdb->prefix . 'vmo_quarterly_reports';
    vmo_ensure_quarterly_reports_table();
    
    $report_data = [
        'user_id' => $user_id,
        'period' => $period,
        'quarter' => $quarter,
        'year' => $current_year,
        'summary' => $summary,
        'recommendations' => json_encode($recommendations),
        'metrics' => json_encode($metrics),
        'raw_response' => $ai_response,
        'generated_at' => current_time('mysql'),
    ];
    
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    $wpdb->insert($reports_table, $report_data);
    
    // ✅ TOKEN LEAK FIX v8.2.1: Use centralized deduction with FIFO, admin bypass & audit trail
    $cost = vmo_get_token_cost('quarterly_insights');
    vmo_deduct_tokens($user_id, $cost, 'quarterly_insights', 'Quarterly business insights for ' . $period);
    
    wp_send_json_success([
        'period' => $period,
        'summary' => $summary,
        'recommendations' => $recommendations,
        'metrics' => $metrics,
        'generated_at' => date('M j, Y g:i A'),
    ]);
}

/**
 * Get quarterly reports history
 */
function vmo_get_quarterly_history() {
    if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['nonce'] ?? $_REQUEST['security'] ?? '')), 'vmo_nonce')) { wp_send_json_error(['message' => 'Security check failed'], 403); }
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if (!vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Invalid nonce', 'status' => 'invalid_nonce'], 403);
    }
    
    global $wpdb;
    $user_id = get_current_user_id();
    
    $reports_table = $wpdb->prefix . 'vmo_quarterly_reports';
    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
    if ($wpdb->get_var("SHOW TABLES LIKE '{$reports_table}'") !== $reports_table) {
        wp_send_json_success(['reports' => []]);
    }
    
    $reports = $wpdb->get_results($wpdb->prepare(
        "SELECT id, period, quarter, year, generated_at, metrics FROM {$reports_table} 
        WHERE user_id = %d ORDER BY year DESC, quarter DESC LIMIT 12",
        $user_id
    ), ARRAY_A);
    
    $formatted = [];
    foreach ($reports as $r) {
        $metrics = json_decode($r['metrics'] ?: '{}', true);
        $formatted[] = [
            'id' => (int) $r['id'],
            'period' => $r['period'],
            'generated_at' => date('M j, Y', strtotime($r['generated_at'])),
            'emails' => $metrics['emails'] ?? 0,
            'calls' => $metrics['calls'] ?? 0,
            'chats' => $metrics['chats'] ?? 0,
        ];
    }
    
    wp_send_json_success(['reports' => $formatted]);
}

/**
 * Get a specific quarterly report
 */
function vmo_get_quarterly_report() {
    if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['nonce'] ?? $_REQUEST['security'] ?? '')), 'vmo_nonce')) { wp_send_json_error(['message' => 'Security check failed'], 403); }
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if (!vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Invalid nonce', 'status' => 'invalid_nonce'], 403);
    }
    
    global $wpdb;
    $user_id = get_current_user_id();
    $report_id = isset($_POST['report_id']) ? (int) $_POST['report_id'] : 0;
    
    if (!$report_id) {
        wp_send_json_error(['message' => 'Missing report_id']);
    }
    
    $reports_table = $wpdb->prefix . 'vmo_quarterly_reports';
    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
    if ($wpdb->get_var("SHOW TABLES LIKE '{$reports_table}'") !== $reports_table) {
        wp_send_json_error(['message' => 'No reports found']);
    }
    
    $report = $wpdb->get_row($wpdb->prepare(
        "SELECT * FROM {$reports_table} WHERE id = %d AND user_id = %d",
        $report_id, $user_id
    ), ARRAY_A);
    
    if (!$report) {
        wp_send_json_error(['message' => 'Report not found']);
    }
    
    $recommendations = json_decode($report['recommendations'] ?: '[]', true);
    $metrics = json_decode($report['metrics'] ?: '{}', true);
    
    wp_send_json_success([
        'period' => $report['period'],
        'summary' => $report['summary'],
        'recommendations' => is_array($recommendations) ? $recommendations : [],
        'metrics' => $metrics,
        'generated_at' => date('M j, Y g:i A', strtotime($report['generated_at'])),
    ]);
}

/**
 * Ensure quarterly reports table exists
 */
function vmo_ensure_quarterly_reports_table() {
    global $wpdb;
    $table = $wpdb->prefix . 'vmo_quarterly_reports';
    
    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
    if ($wpdb->get_var("SHOW TABLES LIKE '{$table}'") === $table) {
        return;
    }
    
    $charset_collate = $wpdb->get_charset_collate();
    
    $sql = "CREATE TABLE {$table} (
        id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
        user_id BIGINT UNSIGNED NOT NULL,
        period VARCHAR(20) NOT NULL,
        quarter TINYINT NOT NULL,
        year SMALLINT NOT NULL,
        summary TEXT,
        recommendations LONGTEXT,
        metrics TEXT,
        raw_response LONGTEXT,
        generated_at DATETIME NOT NULL,
        PRIMARY KEY (id),
        KEY user_quarter (user_id, year, quarter)
    ) {$charset_collate};";
    
    require_once ABSPATH . 'wp-admin/includes/upgrade.php';
    dbDelta($sql);
}

/**
 * Generate quarterly insights for a specific user (used by cron)
 * Does not require nonce verification - for internal use only
 */
function vmo_generate_quarterly_insights_for_user($user_id) {
    global $wpdb;

    // ✅ TOKEN SECURITY CHECK v8.1.0: Prevent negative arbitrage
    $current_balance = (int) get_user_meta($user_id, 'vmo_token_balance', true);
    if ($current_balance < 200) {
        // If this runs via cron, we just abort silently (or with log) rather than sending JSON
        return false;
    }
    
    // Calculate current quarter (generate for PREVIOUS quarter)
    $current_month = (int) date('n');
    $current_year = (int) date('Y');
    
    // We generate for the previous quarter
    $prev_quarter = ceil($current_month / 3) - 1;
    $prev_year = $current_year;
    if ($prev_quarter < 1) {
        $prev_quarter = 4;
        $prev_year--;
    }
    
    $period = "Q{$prev_quarter} {$prev_year}";
    
    // Quarter date range
    $quarter_start_month = (($prev_quarter - 1) * 3) + 1;
    $quarter_start = date('Y-m-d', mktime(0, 0, 0, $quarter_start_month, 1, $prev_year));
    $quarter_end = date('Y-m-d', mktime(23, 59, 59, $quarter_start_month + 2, date('t', mktime(0, 0, 0, $quarter_start_month + 2, 1, $prev_year)), $prev_year));
    
    $metrics = [
        'emails' => 0,
        'calls' => 0,
        'chats' => 0,
    ];
    
    $communications_summary = "Business Communications Summary for {$period}\n\n";
    
    // Get email stats
    $emails_table = $wpdb->prefix . 'vmo_email_sends';
    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
    if ($wpdb->get_var("SHOW TABLES LIKE '{$emails_table}'") === $emails_table) {
        $email_count = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$emails_table} WHERE user_id = %d AND created_at BETWEEN %s AND %s",
            $user_id, $quarter_start, $quarter_end . ' 23:59:59'
        ));
        $metrics['emails'] = $email_count;
        $communications_summary .= "EMAILS: {$email_count} sent\n\n";
    }
    
    // Get phone call stats
    $calls_table = $wpdb->prefix . 'vmo_phone_calls';
    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
    if ($wpdb->get_var("SHOW TABLES LIKE '{$calls_table}'") === $calls_table) {
        $call_count = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$calls_table} WHERE user_id = %d AND created_at BETWEEN %s AND %s",
            $user_id, $quarter_start, $quarter_end . ' 23:59:59'
        ));
        $metrics['calls'] = $call_count;
        $communications_summary .= "PHONE CALLS: {$call_count} total\n\n";
    }
    
    // Get chat stats
    $chat_table = $wpdb->prefix . 'vmo_chat_sessions';
    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
    if ($wpdb->get_var("SHOW TABLES LIKE '{$chat_table}'") === $chat_table) {
        $chat_count = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$chat_table} WHERE user_id = %d AND created_at BETWEEN %s AND %s",
            $user_id, $quarter_start, $quarter_end . ' 23:59:59'
        ));
        $metrics['chats'] = $chat_count;
        $communications_summary .= "CHAT SESSIONS: {$chat_count} total\n\n";
    }
    
    // Check if there's any activity worth reporting
    $total_activity = $metrics['emails'] + $metrics['calls'] + $metrics['chats'];
    if ($total_activity < 5) {
        // Not enough activity to generate meaningful insights
        return false;
    }
    
    // Get business context
    $business_name = get_user_meta($user_id, 'business_name', true) ?: 'the business';
    $business_type = get_user_meta($user_id, 'business_type', true) ?: 'business';
    
    // Generate AI insights
    $prompt = "You are an expert business consultant analyzing quarterly performance for {$business_name} ({$business_type}).

Based on the following quarterly communications data, provide:
1. A brief summary (2-3 sentences) of overall communication performance
2. 4-5 specific, actionable recommendations to improve business outcomes next quarter

{$communications_summary}

Format your response as:
SUMMARY:
[Your 2-3 sentence summary here]

RECOMMENDATIONS:
1. [First recommendation]
2. [Second recommendation]
3. [Third recommendation]
4. [Fourth recommendation]";

    $api_key = get_option('vmo_openai_api_key') ?: VMO_OPENAI_API_KEY;
    if (!$api_key) {
        return false;
    }
    
    // ✅ FIX: Use centralized model registry
    // ✅ Guardrail 5A: Adaptive model fallback when balance is low
    $_qi2_model = function_exists('vmo_get_model') ? vmo_get_model('nano') : 'gpt-5.5-nano';
    if (function_exists('vmo_resolve_model')) {
        $_qi2_model = vmo_resolve_model(get_current_user_id(), $_qi2_model)['model'];
    }
    $_qi2_body = array_merge([
        'model' => $_qi2_model,
        'messages' => [
            ['role' => 'system', 'content' => 'You are a helpful business consultant providing quarterly insights and recommendations.'],
            ['role' => 'user', 'content' => $prompt],
        ],
        'temperature' => 0.7,
    ], function_exists('vmo_get_token_param') ? vmo_get_token_param($_qi2_model, 600) : ['max_tokens' => 600]);

    $response = wp_remote_post('https://api.openai.com/v1/chat/completions', [
        'timeout' => 55, // Capped: Kinsta 60s PHP limit
        'headers' => [
            'Authorization' => 'Bearer ' . $api_key,
            'Content-Type' => 'application/json',
        ],
        'body' => json_encode($_qi2_body),
    ]);
    
    if (is_wp_error($response)) {
        return false;
    }
    
    $body = json_decode(wp_remote_retrieve_body($response), true);
    $ai_response = $body['choices'][0]['message']['content'] ?? '';
    
    // Parse the response
    $summary = '';
    $recommendations = [];
    
    if (preg_match('/SUMMARY:\s*(.+?)(?=RECOMMENDATIONS:|$)/is', $ai_response, $match)) {
        $summary = trim($match[1]);
    }
    
    if (preg_match('/RECOMMENDATIONS:\s*(.+)/is', $ai_response, $match)) {
        $rec_text = trim($match[1]);
        preg_match_all('/\d+\.\s*(.+?)(?=\d+\.|$)/s', $rec_text, $rec_matches);
        if (!empty($rec_matches[1])) {
            foreach ($rec_matches[1] as $r) {
                $clean = trim($r);
                if ($clean) $recommendations[] = $clean;
            }
        }
    }
    
    if (!$summary) {
        $summary = $ai_response;
    }
    
    // Save report to database
    $reports_table = $wpdb->prefix . 'vmo_quarterly_reports';
    vmo_ensure_quarterly_reports_table();
    
    $report_data = [
        'user_id' => $user_id,
        'period' => $period,
        'quarter' => $prev_quarter,
        'year' => $prev_year,
        'summary' => $summary,
        'recommendations' => json_encode($recommendations),
        'metrics' => json_encode($metrics),
        'raw_response' => $ai_response,
        'generated_at' => current_time('mysql'),
    ];
    
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    $wpdb->insert($reports_table, $report_data);
    
    // ✅ TOKEN LEAK FIX v8.2.1: Use centralized deduction with FIFO, admin bypass & audit trail
    $cost = vmo_get_token_cost('quarterly_insights');
    vmo_deduct_tokens($user_id, $cost, 'quarterly_insights', 'Automated quarterly insights for ' . $period);
    
    return true;
}

// ========================================
// END QUARTERLY BUSINESS INSIGHTS
// ========================================

function vmo_import_contacts() {
    vmo_require_crm_ajax_security();
    global $wpdb;
    $user_id = get_current_user_id();
    $rows_json = wp_unslash($_POST['rows'] ?? '');
    $type = sanitize_key($_POST['type'] ?? 'lead');
    $detect_duplicates = !isset($_POST['detect_duplicates']) || sanitize_text_field(wp_unslash($_POST['detect_duplicates'])) !== '0';
    $rows = json_decode($rows_json, true);
    if (!is_array($rows) || !count($rows)) {
        wp_send_json_error(['message' => 'No rows provided']);
    }
    $contacts_table = $wpdb->prefix . 'vmo_contacts';
    $has_table = vmo_table_exists($contacts_table);
    if (!$has_table) {
        wp_send_json_error(['message' => 'Contacts table missing']);
    }


    $inserted = 0;
    $created = 0;
    $updated = 0;
    $skipped = 0;
    $duplicates = 0;
    $possible_duplicates = 0;
    $possible_duplicate_rows = [];
    foreach ($rows as $r) {
        if (!is_array($r)) { $skipped++; continue; }
        $email = sanitize_email($r['email'] ?? '');
        $phone = sanitize_text_field($r['phone'] ?? '');
        $name = sanitize_text_field($r['name'] ?? '');
        $notes = sanitize_textarea_field($r['description'] ?? ($r['notes'] ?? ''));
        $tags = vmo_crm_normalize_tags($r['tags'] ?? '');
        $company = sanitize_text_field($r['company'] ?? '');
        $city = sanitize_text_field($r['city'] ?? '');
        $state = sanitize_text_field($r['state'] ?? '');
        $row_type = sanitize_key($r['type'] ?? $type);
        if (!in_array($row_type, ['lead', 'prospect', 'customer', 'partner', 'vendor', 'support', 'referral_source', 'other'], true)) {
            $row_type = 'lead';
        }
        if ($name === '' && $email === '' && $phone === '') { $skipped++; continue; }
        $weak_matches = [];
        $existing = vmo_crm_find_contact($user_id, ['email' => $email, 'phone' => $phone]);
        if ($detect_duplicates && $existing) {
            $duplicates++;
        } elseif ($detect_duplicates) {
            $weak_matches = vmo_crm_find_possible_duplicates($user_id, [
                'full_name' => $name,
                'company' => $company,
                'city' => $city,
                'state' => $state,
            ], 3);
            if (!empty($weak_matches)) {
                $possible_duplicates++;
            }
        }
        $contact = vmo_crm_upsert_contact($user_id, [
            'email' => $email,
            'phone' => $phone,
            'full_name' => $name,
            'company' => $company,
        ], [
            'source' => 'manual_entry',
            'primary_category' => ($row_type === 'customer') ? 'customer' : $row_type,
            'lifecycle_stage' => ($row_type === 'customer') ? 'customer' : vmo_crm_valid_lifecycle_stage($r['lifecycle_stage'] ?? 'new'),
            'job_title' => sanitize_text_field($r['job_title'] ?? ''),
            'address' => sanitize_textarea_field($r['address'] ?? ''),
            'city' => $city,
            'state' => $state,
            'zip' => sanitize_text_field($r['zip'] ?? ''),
            'country' => sanitize_text_field($r['country'] ?? ''),
            'tags' => $tags,
            'notes' => $notes,
            'acquisition_details' => [
                'source' => 'csv_import',
                'duplicate_detection_requested' => $detect_duplicates,
                'exact_duplicate_updated' => (bool) $existing,
                'possible_duplicate_flagged' => !$existing && !empty($weak_matches),
            ],
        ]);
        if ($contact) {
            if ($row_type === 'customer') {
                vmo_crm_convert_to_customer($user_id, (string) $contact['contact_id'], [
                    'type' => 'manual',
                    'summary' => 'Imported as customer from CRM CSV.',
                ]);
            }
            if ($existing) {
                $updated++;
            } else {
                $created++;
            }
            if (!$existing && !empty($weak_matches)) {
                if (count($possible_duplicate_rows) < 25) {
                    $possible_duplicate_rows[] = [
                        'imported_id' => (int) ($contact['id'] ?? 0),
                        'imported_contact_id' => (string) ($contact['contact_id'] ?? ''),
                        'import_name' => $name,
                        'import_company' => $company,
                        'import_email' => $email,
                        'import_phone' => $phone,
                        'matches' => array_map(function($match) {
                            return [
                                'id' => (int) ($match['id'] ?? 0),
                                'contact_id' => (string) ($match['contact_id'] ?? ''),
                                'name' => (string) ($match['full_name'] ?? ''),
                                'company' => (string) ($match['company'] ?? ''),
                                'email' => (string) ($match['email'] ?? ''),
                                'phone' => (string) ($match['phone'] ?? ''),
                                'lifecycle_stage' => (string) ($match['lifecycle_stage'] ?? ''),
                                'primary_category' => (string) ($match['primary_category'] ?? ''),
                            ];
                        }, $weak_matches),
                    ];
                }
                vmo_crm_log_interaction($user_id, (string) $contact['contact_id'], [
                    'interaction_type' => 'manual_note',
                    'subtype' => 'possible_duplicate_review',
                    'subject' => 'Possible duplicate flagged during CSV import',
                    'summary' => 'Weak name/company/location match found. Review before merging this contact with any existing record.',
                    'outcome' => 'needs_follow_up',
                    'metadata' => ['matches' => $weak_matches],
                ]);
            }
            $inserted++;
        } elseif ($existing) {
            $updated++;
            $inserted++;
        } else {
            $skipped++;
        }
    }

    wp_send_json_success([
        'inserted' => $inserted,
        'created' => $created,
        'updated' => $updated,
        'skipped' => $skipped,
        'duplicates' => $duplicates,
        'exact_duplicates_updated' => $duplicates,
        'possible_duplicates' => $possible_duplicates,
        'possible_duplicate_rows' => $possible_duplicate_rows,
        'duplicate_detection' => $detect_duplicates,
    ]);
}

function vmo_crm_resolve_duplicate_contacts($user_id, $decision, $imported_id, $match_id = 0) {
    global $wpdb;

    $user_id = absint($user_id);
    $decision = sanitize_key($decision);
    $imported_id = absint($imported_id);
    $match_id = absint($match_id);
    $valid_decisions = ['merge', 'update_existing', 'keep_separate', 'ignore'];

    if (!$imported_id || !in_array($decision, $valid_decisions, true)) {
        return new WP_Error('invalid_duplicate_review', 'Invalid duplicate review request.');
    }

    $contacts_table = $wpdb->prefix . 'vmo_contacts';
    if (!vmo_table_exists($contacts_table)) {
        return new WP_Error('contacts_table_missing', 'Contacts table missing.');
    }

    $imported = $wpdb->get_row($wpdb->prepare(
        "SELECT * FROM {$contacts_table} WHERE id = %d AND user_id = %d",
        $imported_id,
        $user_id
    ), ARRAY_A);
    if (!$imported) {
        return new WP_Error('imported_contact_missing', 'Imported contact not found.');
    }

    if (in_array($decision, ['keep_separate', 'ignore'], true)) {
        if (function_exists('vmo_crm_log_interaction')) {
            vmo_crm_log_interaction($user_id, (string) $imported['contact_id'], [
                'interaction_type' => 'manual_note',
                'subtype' => 'duplicate_review_' . $decision,
                'subject' => 'Possible duplicate reviewed',
                'summary' => $decision === 'keep_separate' ? 'Owner reviewed a possible duplicate and kept this contact separate.' : 'Owner dismissed a possible duplicate suggestion.',
                'outcome' => 'successful',
            ]);
        }
        return ['message' => $decision === 'keep_separate' ? 'Kept as a separate contact.' : 'Duplicate suggestion ignored.'];
    }

    if (!$match_id || $match_id === $imported_id) {
        return new WP_Error('existing_contact_required', 'Choose an existing contact to merge or update.');
    }

    $existing = $wpdb->get_row($wpdb->prepare(
        "SELECT * FROM {$contacts_table} WHERE id = %d AND user_id = %d",
        $match_id,
        $user_id
    ), ARRAY_A);
    if (!$existing) {
        return new WP_Error('existing_contact_missing', 'Existing contact not found.');
    }

    $overwrite = $decision === 'update_existing';
    $merge_fields = ['first_name', 'last_name', 'full_name', 'email', 'phone', 'company', 'job_title', 'address', 'city', 'state', 'zip', 'country'];
    $update_row = ['updated_at' => current_time('mysql')];
    foreach ($merge_fields as $field_name) {
        $imported_value = $imported[$field_name] ?? null;
        $existing_value = $existing[$field_name] ?? null;
        if ($imported_value !== null && $imported_value !== '' && ($overwrite || $existing_value === null || $existing_value === '')) {
            $update_row[$field_name] = $imported_value;
        }
    }

    $merged_tags = vmo_crm_normalize_tags(array_merge(
        explode(',', (string) ($existing['tags'] ?? '')),
        explode(',', (string) ($imported['tags'] ?? ''))
    ));
    if ($merged_tags !== '') {
        $update_row['tags'] = $merged_tags;
    }

    $imported_notes = trim((string) ($imported['notes'] ?? ''));
    $existing_notes = trim((string) ($existing['notes'] ?? ''));
    if ($imported_notes !== '') {
        $update_row['notes'] = $existing_notes !== ''
            ? $existing_notes . "\n\nMerged duplicate note: " . $imported_notes
            : $imported_notes;
    }

    if (($imported['lifecycle_stage'] ?? '') === 'customer' || ($imported['primary_category'] ?? '') === 'customer') {
        $update_row['primary_category'] = 'customer';
        $update_row['lifecycle_stage'] = 'customer';
        $update_row['lead_score'] = 100;
        $update_row['engagement_level'] = 'high';
    }

    $updated = $wpdb->update($contacts_table, $update_row, ['id' => $match_id, 'user_id' => $user_id]);
    if ($updated === false) {
        return new WP_Error('duplicate_update_failed', 'Unable to update existing contact.');
    }

    $imported_contact_id = (string) ($imported['contact_id'] ?? '');
    $existing_contact_id = (string) ($existing['contact_id'] ?? '');

    if ($imported_contact_id !== '' && $existing_contact_id !== '') {
        $interactions_table = $wpdb->prefix . 'vmo_contact_interactions';
        if (vmo_table_exists($interactions_table)) {
            $wpdb->update($interactions_table, ['contact_id' => $existing_contact_id, 'updated_at' => current_time('mysql')], ['user_id' => $user_id, 'contact_id' => $imported_contact_id]);
        }

        $purchases_table = $wpdb->prefix . 'vmo_contact_purchases';
        if (vmo_table_exists($purchases_table)) {
            $wpdb->update($purchases_table, ['contact_id' => $existing_contact_id, 'updated_at' => current_time('mysql')], ['user_id' => $user_id, 'contact_id' => $imported_contact_id]);
        }
    }

    $followups_table = $wpdb->prefix . 'vmo_follow_ups';
    if (vmo_table_exists($followups_table)) {
        $wpdb->update($followups_table, ['customer_id' => $match_id, 'updated_at' => current_time('mysql')], ['user_id' => $user_id, 'customer_id' => $imported_id]);
    }

    $wpdb->delete($contacts_table, ['id' => $imported_id, 'user_id' => $user_id]);

    if (function_exists('vmo_crm_log_interaction')) {
        vmo_crm_log_interaction($user_id, $existing_contact_id, [
            'interaction_type' => 'manual_note',
            'subtype' => 'duplicate_review_' . $decision,
            'subject' => 'Duplicate contact resolved',
            'summary' => $decision === 'update_existing' ? 'Imported duplicate updated this existing contact and the duplicate row was removed.' : 'Imported duplicate was merged into this existing contact.',
            'outcome' => 'successful',
            'metadata' => [
                'removed_contact_id' => $imported_contact_id,
                'decision' => $decision,
            ],
        ]);
    }

    if (function_exists('vmo_crm_convert_to_customer') && (($imported['lifecycle_stage'] ?? '') === 'customer' || ($imported['primary_category'] ?? '') === 'customer')) {
        vmo_crm_convert_to_customer($user_id, $existing_contact_id, [
            'type' => 'manual',
            'summary' => 'Customer status preserved during duplicate merge/update.',
        ]);
    }

    return [
        'message' => $decision === 'update_existing' ? 'Existing contact updated and duplicate removed.' : 'Contacts merged and duplicate removed.',
        'existing_id' => $match_id,
        'removed_id' => $imported_id,
    ];
}

function vmo_resolve_contact_duplicate() {
    vmo_require_crm_ajax_security();

    $result = vmo_crm_resolve_duplicate_contacts(
        get_current_user_id(),
        $_POST['decision'] ?? '',
        $_POST['imported_id'] ?? 0,
        $_POST['match_id'] ?? 0
    );

    if (is_wp_error($result)) {
        wp_send_json_error(['message' => $result->get_error_message()]);
    }

    wp_send_json_success($result);
}

function vmo_get_tax_settings() {
    if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['nonce'] ?? $_REQUEST['security'] ?? '')), 'vmo_nonce')) { wp_send_json_error(['message' => 'Security check failed'], 403); }
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if (!vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Invalid nonce', 'status' => 'invalid_nonce'], 403);
    }
    $user_id = get_current_user_id();
    $settings = get_user_meta($user_id, 'vmo_tax_settings', true);
    if (!is_array($settings)) $settings = [];
    wp_send_json_success(['settings' => $settings]);
}

function vmo_save_tax_settings() {
    if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['nonce'] ?? $_REQUEST['security'] ?? '')), 'vmo_nonce')) { wp_send_json_error(['message' => 'Security check failed'], 403); }
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if (!vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Invalid nonce', 'status' => 'invalid_nonce'], 403);
    }
    $user_id = get_current_user_id();
    $country = sanitize_text_field(wp_unslash($_POST['country'] ?? ''));
    $state = sanitize_text_field(wp_unslash($_POST['state'] ?? ''));

    // Merge with existing settings to avoid overwriting REST-saved fields
    $existing = get_user_meta($user_id, 'vmo_tax_settings', true);
    if (!is_array($existing)) $existing = [];

    $settings = array_merge($existing, [
        'country' => $country,
        'state' => $state,
        'updated_at' => current_time('mysql'),
    ]);

    // Only set legacy fields if explicitly provided
    if (isset($_POST['profit_margin'])) {
        $settings['profit_margin'] = max(0.0, min(100.0, floatval($_POST['profit_margin'])));
    }
    if (isset($_POST['sales_tax_rate'])) {
        $settings['sales_tax_rate'] = max(0.0, min(100.0, floatval($_POST['sales_tax_rate'])));
    }
    if (isset($_POST['income_tax_rate'])) {
        $settings['income_tax_rate'] = max(0.0, min(100.0, floatval($_POST['income_tax_rate'])));
    }

    update_user_meta($user_id, 'vmo_tax_settings', $settings);
    wp_send_json_success(['settings' => $settings]);
}

function vmo_get_competitors_data() {
    if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['nonce'] ?? $_REQUEST['security'] ?? '')), 'vmo_nonce')) { wp_send_json_error(['message' => 'Security check failed'], 403); }
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if ((isset($_POST['security']) || isset($_POST['nonce'])) && !vmo_verify_admin_security()) {
        // Soften invalid nonce to avoid HTTP 400 noise in console
        wp_send_json_error(['message' => 'Invalid nonce', 'status' => 'invalid_nonce']);
    }
    // With no external data, approximate "market share" as your orders vs top competitors from connected accounts (if any)
    global $wpdb;
    $user_id = get_current_user_id();
    $from = isset($_POST['from']) ? sanitize_text_field(wp_unslash($_POST['from'])) : '';
    $to   = isset($_POST['to'])   ? sanitize_text_field(wp_unslash($_POST['to']))   : '';
    $days = isset($_POST['days']) ? (int) $_POST['days'] : 30;
    $now  = current_time('mysql');
    if (!$to) $to = $now;
    if (!$from) $from = date('Y-m-d H:i:s', strtotime('-' . max(1,$days) . ' days', strtotime($to)));
    $orders_table = $wpdb->prefix . 'vmo_store_orders';
    $connected_accounts_table = $wpdb->prefix . 'vmo_connected_accounts';
    $has_orders = vmo_table_exists($orders_table);
    $has_accounts = vmo_table_exists($connected_accounts_table);

    $your_orders = $has_orders ? (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$orders_table} WHERE user_id=%d AND status='paid' AND created_at BETWEEN %s AND %s", $user_id, $from, $to)) : 0;
    $competitors = [];
    if ($has_accounts) {
        // take up to 5 competitor accounts as placeholders
        $rows = $wpdb->get_results($wpdb->prepare("SELECT account_name FROM {$connected_accounts_table} WHERE user_id=%d AND provider LIKE '%%competitor%%' LIMIT 5", $user_id));
        foreach (($rows?:[]) as $r) {
            $competitors[] = ['name' => (string) ($r->account_name ?: 'Competitor'), 'share' => 0];
        }
    }
    // Calculate shares only if real competitor data exists
    $comp_count = count($competitors);
    if ($comp_count > 0 && $your_orders > 0) {
        // Real competitor accounts exist — but we don't have their actual order data
        // Report only what we know: your orders and that competitors are monitored
        $your_share = 0; // Cannot calculate real share without competitor order data
        $leader_share = 0;
        $your_rank = 0;
    } else {
        $your_share = 0;
        $leader_share = 0;
        $your_rank = 0;
    }
    $monitored = $comp_count;
    // chart: trend of your orders over last 5 weeks (actual weekly counts)
    $labels = [];$chart_data = [];
    if ($has_orders) {
        for ($i=4; $i>=0; $i--) {
            $week_end = date('Y-m-d H:i:s', strtotime('-' . ($i * 7) . ' days', strtotime($to)));
            $week_start = date('Y-m-d H:i:s', strtotime('-7 days', strtotime($week_end)));
            $labels[] = 'W'.(5-$i);
            $chart_data[] = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$orders_table} WHERE user_id=%d AND status='paid' AND created_at BETWEEN %s AND %s", $user_id, $week_start, $week_end));
        }
    } else {
        for ($i=4; $i>=0; $i--) {
            $labels[] = 'W'.(5-$i);
            $chart_data[] = 0;
        }
    }
    wp_send_json_success([
        'your_share' => $your_share,
        'leader_share' => $leader_share,
        'your_rank' => $your_rank,
        'monitored' => $monitored,
        'competitors' => $competitors,
        'chart' => ['labels' => $labels, 'data' => $chart_data]
    ]);
}


if (!function_exists('vmo_client_direct_master_voice_proxy_config')) {
    function vmo_client_direct_master_voice_proxy_config() {
        $is_client_mode = ((defined('VMO_CLIENT_MODE') && VMO_CLIENT_MODE === true)
            || get_option('vmo_quicklaunch_hosted_client') === '1'
            || get_option('vmo_client_distribution_channel') === 'quicklaunch_hosted');
        $master_proxy_url = defined('VMO_MASTER_PROXY_URL') && VMO_MASTER_PROXY_URL
            ? (string) VMO_MASTER_PROXY_URL
            : (string) get_option('vmo_master_proxy_url', 'https://getvmo.com/wp-json/vmo-builder/v1/proxy');
        $master_proxy_url = rtrim(trim($master_proxy_url), '/');
        $master_api_url = preg_replace('#/proxy/?$#', '', $master_proxy_url);
        $license_key = trim((string) get_option('vmo_license_key', ''));
        $installation_id = trim((string) get_option('vmo_installation_id', ''));
        $enabled = $is_client_mode && $master_proxy_url !== '' && strpos($master_proxy_url, '__VMO_MASTER_' . 'PROXY_URL__') === false && $license_key !== '' && $installation_id !== '';
        return array(
            'enabled' => (bool) $enabled,
            'client_mode' => (bool) $is_client_mode,
            'direct_master_voice' => (bool) $enabled,
            'master_proxy_url' => esc_url_raw($master_proxy_url),
            'master_api_url' => esc_url_raw($master_api_url),
            'license_key' => $license_key,
            'installation_id' => $installation_id,
            'site_url' => esc_url_raw(home_url('/')),
            'hosted_client' => (bool) (get_option('vmo_quicklaunch_hosted_client') === '1'),
        );
    }
}

/* --------------------------------------------------------------
   Enqueue Scripts & Styles  (Admin only – VMO screens) - UNIFIED VERSION
---------------------------------------------------------------- */
add_action( 'admin_enqueue_scripts', function ( $hook ) {
    // ⚡ PERFORMANCE: Only load VMO assets on VMO admin pages
    // Non-VMO pages (Posts, Media, Users, Plugins, etc.) skip this entirely.
    $hook = (string) ($hook ?? '');
    if ( strpos( $hook, 'vmo' ) === false ) {
        return;
    }

    
if (!function_exists('vmo_client_direct_master_voice_proxy_config')) {
    function vmo_client_direct_master_voice_proxy_config() {
        $is_client_mode = ((defined('VMO_CLIENT_MODE') && VMO_CLIENT_MODE === true) || get_option('vmo_quicklaunch_hosted_client') === '1');
        $master_proxy_url = defined('VMO_MASTER_PROXY_URL') && VMO_MASTER_PROXY_URL
            ? (string) VMO_MASTER_PROXY_URL
            : (string) get_option('vmo_master_proxy_url', 'https://getvmo.com/wp-json/vmo-builder/v1/proxy');
        $license_key = (string) get_option('vmo_license_key', '');
        $installation_id = (string) get_option('vmo_installation_id', '');
        $enabled = $is_client_mode && $master_proxy_url !== '' && $license_key !== '' && $installation_id !== '';
        return array(
            'enabled' => (bool) $enabled,
            'direct_master_voice' => (bool) $enabled,
            'master_proxy_url' => esc_url_raw(rtrim($master_proxy_url, '/')),
            'master_api_url' => esc_url_raw(preg_replace('#/proxy/?$#', '', rtrim($master_proxy_url, '/'))),
            'license_key' => $license_key,
            'installation_id' => $installation_id,
            'site_url' => esc_url_raw(home_url('/')),
            'hosted_client' => (bool) (get_option('vmo_quicklaunch_hosted_client') === '1'),
        );
    }
}

/* 1️⃣  make absolutely sure jQuery ships first */
    wp_enqueue_script( 'jquery' );   // WP-bundled copy

    /* 2️⃣  core stylesheet + template styles */
    wp_enqueue_style(
        'vmo-builder-css',
        VMO_AI_BUILDER_URL . 'assets/builder.css',
        [],
        '8.0.0-' . time() // Force cache bust with timestamp - UNIFIED BUSINESS PLATFORM THEME
    );
    // Provide a datepicker fallback for Safari/older browsers
    wp_enqueue_script('jquery-ui-datepicker');
    // Minimal datepicker CSS to ensure visibility (WordPress may not ship UI theme by default)
    $inline_css = '.ui-datepicker{z-index:999999 !important;background:#0b0f1a;border:1px solid rgba(255,255,255,.15);color:#e6f3ff;border-radius:8px;padding:6px}.ui-datepicker .ui-datepicker-header{background:rgba(255,255,255,.06);border:0;border-bottom:1px solid rgba(255,255,255,.1);border-radius:8px 8px 0 0}.ui-datepicker td a{color:#e6f3ff}.ui-datepicker td a.ui-state-active{background:#0fe1ff;color:#04121a;border-radius:4px}';
    wp_add_inline_style('vmo-builder-css', $inline_css);
    
    wp_enqueue_style(
        'vmo-templates-css',
        VMO_AI_BUILDER_URL . 'assets/vmo-templates.css',
        ['vmo-builder-css'],
        '1.0.0-' . time()
    );

    // Business Wizard UI fixes (text overflow, button sizing, spacing)
    wp_enqueue_style(
        'vmo-wizard-ui-fixes',
        VMO_AI_BUILDER_URL . 'assets/vmo-wizard-ui-fixes.css',
        ['vmo-builder-css'],
        '1.0.0-' . time()
    );

    // Jarvis-style floating voice panel (text chat only, ready for OpenAI TTS HD)
    wp_enqueue_style(
        'vmo-voice-panel-css',
        VMO_AI_BUILDER_URL . 'assets/vmo-voice-panel.css',
        ['vmo-builder-css'],
        '1.0.0-' . time()
    );

    /* 3️⃣  base state (everything else hangs off this) */
    wp_enqueue_script(
        'vmo-state',
        VMO_AI_BUILDER_URL . 'assets/vmo-state.js',
        [ 'jquery' ],
        vmo_safe_filemtime( VMO_AI_BUILDER_DIR . 'assets/vmo-state.js' ),
        true
    );

    /* 4️⃣  functional modules (order matters only via dependencies) */

    // Pre-register OpenAI TTS HD client so other scripts can depend on it
    $tts_hd_js_path = VMO_AI_BUILDER_DIR . 'assets/vmo-tts-hd.js';
    if (file_exists($tts_hd_js_path)) {
        $tts_version = '9.0.1-FIXED-' . time() . '-' . filemtime($tts_hd_js_path);
        // Ensure the audio coordinator loads before the TTS client
        wp_register_script('vmo-tts-hd', VMO_AI_BUILDER_URL . 'assets/vmo-tts-hd.js', ['vmo-audio-coordinator'], $tts_version, true);
        // Localize REST API URL and nonce
        wp_localize_script('vmo-tts-hd', 'wpApiSettings', [
            'root' => esc_url_raw(rest_url()),
            'nonce' => wp_create_nonce('wp_rest')
        ]);
    }

    $modules = [
        'vmo-audio-coordinator' => [], // NEW: One-mic guard across modules
        'vmo-realtime' => ['vmo-audio-coordinator'], // Premium Live Voice WebRTC client; must load before Jarvis voice panel
        'vmo-init'     => [],              // v8.0.0 initialization
        'vmo-ui'       => [],
        'vmo-tokens'   => [],
        'vmo-undo'     => [],
        'vmo-render'   => [],
        'vmo-prompts'  => [],
        'vmo-menu'     => [],
        'vmo-media'    => [],
        'vmo-media-generator' => [], // NEW: Auto-generate missing media
        'vmo-media-integration' => [], // NEW: AI-powered backend media integration
        'vmo-iframe-wrapper' => [], // NEW: High-quality iframe wrapper with GSAP & modern libraries
        'vmo-iframe-placeholder' => [], // Blank preview until generated website content exists
        'vmo-refine'   => [],
        'vmo-element-refiner' => ['vmo-refine'], // NEW: Element-specific refinement system
        'vmo-ai-agents' => [],         // NEW: Dedicated AI agents for different functions
        'vmo-projects' => [],
        'vmo-publish'  => ['vmo-ui'], // Needs showToast function
        'vmo-theme' => ['vmo-main'], // Replace button system - needs vmo-main for vmoOpenMediaPicker
        // 'vmo-voice'    => [],          // DISABLED: Legacy voice system (replaced by v8)
        // ✅ v9.0.0 CLEAN ARCHITECTURE: Two dedicated AI files only
        'vmo-tts-hd' => [], // ✅ OpenAI TTS Client (Must be loaded before dependents)
        'vmo-voice-panel' => ['vmo-tts-hd', 'vmo-realtime'], // ✅ JARVIS AI depends on TTS + Premium Realtime client
        'vmo-intelligent-consultation' => ['vmo-audio-coordinator', 'vmo-tts-hd', 'vmo-realtime'], // ✅ V2 CONSULTATION SYSTEM with persistent memory (uses new V2 file)
        'vmo-refinement-agents' => ['vmo-ai-agents'], // Multi-agent refinement system
        // 'vmo-conversational' => [], // ❌ DISABLED: Using vmo-intelligent-consultation instead
        // 'vmo-enhanced-business-wizard' => [], // REMOVED: Duplicate Business Consultant handler
        'vmo-forms'    => [],          // NEW: Enhanced forms for business wizard
        'vmo-templates' => [],         // NEW: Template system
        'vmo-template-integration' => ['vmo-templates'], // NEW: Template integration
        // 'vmo-business-dashboard' => [], // Removed frontend overlay; use WP Admin dashboard page instead
        'vmo-seo'      => [],          // NEW: SEO optimization features
        'vmo-main'     => ['media-upload', 'media-views'], // Add WordPress media dependencies
        'vmo-plans'    => [],          // plan picker / Stripe
        'vmo-payments' => [],          // product buy buttons (Stripe Connect)
        'vmo-queue'    => [],          // approval queue admin UI
        'vmo-video-generator' => [],   // NEW: Sora video generation UI
        'vmo-360-spin-viewer' => [],   // NEW: 360° product spin viewer
        'vmo-animated-overlays' => [], // NEW: Animated overlay effects (clouds, smoke, particles)
    ];

    foreach ( $modules as $handle => $extraDeps ) {
        $file_path = VMO_AI_BUILDER_DIR . "assets/{$handle}.js";
        
        // Special case for media integration - use backend version
        if ($handle === 'vmo-media-integration') {
            $file_path = VMO_AI_BUILDER_DIR . "assets/vmo-media-integration-backend.js";
        }
        
        if (file_exists($file_path)) {
            // ✅ FORCE CACHE BUST: Multiple strategies to force new files to load
            // Strategy 1: Manual version + timestamp
            // Strategy 2: Use current time for critical files
            // Strategy 3: Add unique query string
            $base_version = filemtime($file_path);
            
            // For critical fixed files, add current timestamp to force reload
            $version = ($handle === 'vmo-voice-panel' || $handle === 'vmo-intelligent-consultation' || $handle === 'vmo-tts-hd') 
                ? '9.0.1-FIXED-' . time() . '-' . $base_version  // FORCE RELOAD - V2 consultation system
                : $base_version;
            
            wp_enqueue_script(
                $handle,
                VMO_AI_BUILDER_URL . ($handle === 'vmo-media-integration' ? "assets/vmo-media-integration-backend.js" : "assets/{$handle}.js"),
                array_merge( [ 'jquery', 'vmo-state' ], $extraDeps ),
                $version,
                true
            );

            // Localize TTS settings immediately
            if ($handle === 'vmo-tts-hd') {
                wp_localize_script('vmo-tts-hd', 'wpApiSettings', [
                    'root' => esc_url_raw(rest_url()),
                    'nonce' => wp_create_nonce('wp_rest')
                ]);
                wp_localize_script('vmo-tts-hd', 'vmoClientProxy', vmo_client_direct_master_voice_proxy_config());
            }

            // Localize Premium Live Voice / Realtime early. The first enqueue
            // path was loading Jarvis without vmo-realtime, so Premium could be
            // selected but would still fall back to Default TTS. Keep this data
            // on the same global used by the Realtime client.
            if ($handle === 'vmo-realtime') {
                wp_localize_script('vmo-realtime', 'vmo_ajax', [
                    'ajaxurl' => admin_url('admin-ajax.php'),
                    'nonce'   => wp_create_nonce('vmo_builder_nonce'),
                    'security'=> wp_create_nonce('vmo_builder_nonce'),
                    'is_client_mode' => ((defined('VMO_CLIENT_MODE') && VMO_CLIENT_MODE === true) || get_option('vmo_quicklaunch_hosted_client') === '1'),
                ]);
                wp_localize_script('vmo-realtime', 'vmoClientProxy', vmo_client_direct_master_voice_proxy_config());
            }
        }
    }

    if (wp_script_is('vmo-forms', 'enqueued')) {
        wp_localize_script('vmo-forms', 'vmo_form_config', [
            'ajaxurl' => admin_url('admin-ajax.php'),
            'site_owner' => get_current_user_id(),
            'security' => wp_create_nonce('vmo_builder_nonce'),
            'nonce' => wp_create_nonce('vmo_builder_nonce'),
        ]);
    }


    /* 6️⃣  GSAP and ScrollTrigger for advanced animations (bundled locally) */
    wp_enqueue_script( 'gsap-core', VMO_AI_BUILDER_URL . 'assets/vendor/gsap.min.js', [], '3.12.2', true );
    wp_enqueue_script( 'gsap-scrolltrigger', VMO_AI_BUILDER_URL . 'assets/vendor/ScrollTrigger.min.js', ['gsap-core'], '3.12.2', true );
    
    /* 7️⃣  AOS animation library (bundled locally) */
    wp_enqueue_style(  'aos-css', VMO_AI_BUILDER_URL . 'assets/vendor/aos.css', [], '2.3.1' );
    wp_enqueue_script( 'aos-js',  VMO_AI_BUILDER_URL . 'assets/vendor/aos.min.js',  [], '2.3.1', true );

    /* 8️⃣  WordPress media uploader (image / video uploads) */
    wp_enqueue_media();

    /* 9️⃣  localisation for AJAX + user state */
        wp_localize_script( 'vmo-main', 'vmoAjax', [
        'ajax_url' => admin_url( 'admin-ajax.php' ),
        'nonce' => wp_create_nonce( 'vmo_nonce' ),
    'pluginUrl' => VMO_AI_BUILDER_URL,  // Add plugin URL for templates
    // Used by Jarvis-style greeting logic in assets/vmo-main.js
    'has_published_site' => vmo_user_has_published_site(get_current_user_id()),
        'builder_reset_mode' => function_exists('vmo_builder_reset_mode_active') ? (bool) vmo_builder_reset_mode_active(get_current_user_id()) : false,
    ]);

    // 🔧 Compatibility: Also localize as vmo_ajax for legacy JavaScript files
    $consultation_profile = function_exists('vmo_get_consultation_profile') ? vmo_get_consultation_profile(get_current_user_id()) : [];
    $profile_system = !empty($consultation_profile) ? "BUSINESS CONSULTATION PROFILE (authoritative context):\n" . substr(wp_json_encode($consultation_profile, JSON_PRETTY_PRINT), 0, 6000) : '';
    
    // Build upgrade URL with user context (used by credit enforcement system)
    $installation_id_early = get_option('vmo_installation_id', '');
    $upgrade_url_early = 'https://jerremejosephson.com/vmo-upgrade/' . (!empty($installation_id_early) ? '?installation=' . urlencode($installation_id_early) : '');
    $topup_url_early = 'https://jerremejosephson.com/vmo-topup/' . (!empty($installation_id_early) ? '?installation=' . urlencode($installation_id_early) : '');
    // In-plugin "My Plan" page deep links: credit notices point users straight
    // to the relevant section (plan upgrade vs one-time booster) so the option
    // they chose is the first thing they see.
    $my_plan_url_early = admin_url('admin.php?page=vmo-my-plan');
    $my_plan_plans_url_early = $my_plan_url_early . '#vmo-plans-section';
    $my_plan_boosters_url_early = $my_plan_url_early . '#vmo-boosters-section';
    $credit_config_early = function_exists('vmo_get_frontend_credit_config')
        ? vmo_get_frontend_credit_config(get_current_user_id())
        : ['token_balance' => (int) get_user_meta(get_current_user_id(), 'vmo_token_balance', true)];
    
    wp_localize_script( 'vmo-main', 'vmo_ajax', [
        'ajaxurl'       => admin_url( 'admin-ajax.php' ),
        'nonce'         => wp_create_nonce( 'vmo_builder_nonce' ),
        // CLIENT VERSION: Require an active subscription from the master server
        'current_plan'  => get_user_meta( get_current_user_id(), 'vmo_subscription_status', true ),
        'token_balance' => (int) ($credit_config_early['token_balance'] ?? get_user_meta( get_current_user_id(), 'vmo_token_balance', true )),
        'plan_tokens' => (int) ($credit_config_early['plan_tokens'] ?? 0),
        'credit_remaining_percent' => (float) ($credit_config_early['credit_remaining_percent'] ?? 100),
        'credit_usage_percent' => (float) ($credit_config_early['credit_usage_percent'] ?? 0),
        'low_credit_remaining_percent' => (float) ($credit_config_early['low_credit_remaining_percent'] ?? 40),
        'critical_credit_remaining_percent' => (float) ($credit_config_early['critical_credit_remaining_percent'] ?? 20),
        'low_credit' => (bool) ($credit_config_early['low_credit'] ?? false),
        'critical_credit' => (bool) ($credit_config_early['critical_credit'] ?? false),
        'credit_warning_level' => (string) ($credit_config_early['credit_warning_level'] ?? 'none'),
        'no_credit' => (bool) ($credit_config_early['no_credit'] ?? false),
        'next_plan' => (string) ($credit_config_early['next_plan'] ?? 'Starter'),
        'has_selected_plan' => (bool) ($credit_config_early['has_selected_plan'] ?? false),
        'entitlement_active' => (bool) ($credit_config_early['entitlement_active'] ?? false),
        'free_access_active' => (bool) ($credit_config_early['free_access_active'] ?? false),
        'free_access_expired' => (bool) ($credit_config_early['free_access_expired'] ?? false),
        'can_use_cost_bearing_services' => (bool) ($credit_config_early['can_use_cost_bearing_services'] ?? false),
        'service_locked' => (bool) ($credit_config_early['service_locked'] ?? false),
        'service_lock_reason' => (string) ($credit_config_early['service_lock_reason'] ?? ''),
        'subscription_payment_status' => (string) ($credit_config_early['subscription_payment_status'] ?? ''),
        'upgradeUrl'    => $upgrade_url_early,
        'topupUrl'      => $topup_url_early,
        'myPlanUrl'        => $my_plan_url_early,
        'myPlanPlansUrl'   => $my_plan_plans_url_early,
        'myPlanBoostersUrl'=> $my_plan_boosters_url_early,
        // ── Guardrail 5A: Expose model downgrade state to frontend ──
        'model_downgraded' => function_exists('vmo_resolve_model')
            ? vmo_resolve_model(get_current_user_id())['downgraded']
            : false,
        // ── Guardrail 5B: Token costs for pre-confirmation dialogs ──
        'token_costs' => [
            'full_page'          => defined('VMO_TOKENS_FULL_PAGE') ? VMO_TOKENS_FULL_PAGE : 2500,
            'section'            => defined('VMO_TOKENS_SECTION') ? VMO_TOKENS_SECTION : 500,
            'refinement'         => defined('VMO_TOKENS_REFINEMENT') ? VMO_TOKENS_REFINEMENT : 225,
            'image_generation'   => defined('VMO_TOKENS_IMAGE_GENERATION') ? VMO_TOKENS_IMAGE_GENERATION : 1080,
            'generate_website'   => defined('VMO_TOKENS_GENERATE_WEBSITE') ? VMO_TOKENS_GENERATE_WEBSITE : 9450,
            'deep_research'      => defined('VMO_TOKENS_DEEP_RESEARCH') ? VMO_TOKENS_DEEP_RESEARCH : 13500,
            'business_wizard'    => defined('VMO_TOKENS_BUSINESS_WIZARD') ? VMO_TOKENS_BUSINESS_WIZARD : 2500,
            'media_integration'  => defined('VMO_TOKENS_MEDIA_INTEGRATION') ? VMO_TOKENS_MEDIA_INTEGRATION : 900,
            'email_campaign'     => defined('VMO_TOKENS_EMAIL_CAMPAIGN_GENERATION') ? VMO_TOKENS_EMAIL_CAMPAIGN_GENERATION : 945,
            'roadmap'            => defined('VMO_TOKENS_ROADMAP_GENERATION') ? VMO_TOKENS_ROADMAP_GENERATION : 2025,
            'consultation'       => defined('VMO_TOKENS_CONSULTATION_MESSAGE') ? VMO_TOKENS_CONSULTATION_MESSAGE : 405,
            'vision'             => defined('VMO_TOKENS_VISION_ANALYSIS') ? VMO_TOKENS_VISION_ANALYSIS : 270,
            'tts_response'       => defined('VMO_TOKENS_TTS_RESPONSE') ? VMO_TOKENS_TTS_RESPONSE : 81,
            'publish'            => function_exists('vmo_get_publish_cost') ? vmo_get_publish_cost() : 500,
        ],
        // ── Guardrail 5B: Threshold above which we show a confirmation dialog ──
        'high_cost_threshold' => 1000,
        // ── Guardrail 5C: Low-balance proactive nudge threshold ──
        'low_balance_threshold' => defined('VMO_LOW_BALANCE_MODEL_THRESHOLD') ? VMO_LOW_BALANCE_MODEL_THRESHOLD : 5000,
        // Welcome bonus notification for new Starter plan users
        'pending_notification' => get_user_meta( get_current_user_id(), 'vmo_pending_notification', true ),
        'quicklaunch_welcome' => function_exists('vmo_quicklaunch_get_welcome_client_data') ? vmo_quicklaunch_get_welcome_client_data(get_current_user_id()) : array(),
        // Background ops upgrade prompts (Tester plan missed operations)
        'upgrade_prompts' => get_user_meta( get_current_user_id(), 'vmo_upgrade_prompts', true ) ?: [],
    'payments_enabled' => (bool) VMO_PAYMENTS_ENABLED,
        'stripe' => [
            'pk' => get_option('vmo_stripe_publishable_key', ''),
            'platform_fee_bps' => (int) get_option('vmo_stripe_platform_fee_bps', 50)
        ],
        // OpenAI TTS HD Voice Settings
        'voice' => [
            'enabled' => true,
            'provider' => 'openai',
            'voice_id' => get_option('vmo_voice_id', 'alloy'),
            'speed' => (float) get_option('vmo_voice_speed', 1.0),
            'hd' => true
        ],
        'business_wizard' => [
            'enabled' => true,
            'cost_per_session' => VMO_TOKENS_BUSINESS_WIZARD,
            'generation_cost' => VMO_TOKENS_GENERATE_WEBSITE
        ],
        'pluginUrl' => plugin_dir_url( __FILE__ ),
        'userId' => get_current_user_id(),
        // Builder gating flags and niceties
        'generation_unlocked' => function_exists('vmo_is_consultation_ready') ? (bool) vmo_is_consultation_ready(get_current_user_id()) : false,
        'jarvis_onboarding_ready' => function_exists('vmo_jarvis_has_connected_consultation_website') ? (bool) vmo_jarvis_has_connected_consultation_website(get_current_user_id()) : false,
        'user_display_name' => vmo_get_greeting_display_name(),
        'builder_reset_mode' => function_exists('vmo_builder_reset_mode_active') ? (bool) vmo_builder_reset_mode_active(get_current_user_id()) : false
    ] );

    // Expose consultation system text globally for prompts
    wp_add_inline_script('vmo-main', 'window.vmoConsultationProfileSystem = '. wp_json_encode($profile_system) .';', 'before');

    // ✅ v9.0.0: Localize AJAX data for both AI systems
    // Build upgrade URL with user context
    $installation_id = get_option('vmo_installation_id', '');
    $upgrade_url = 'https://jerremejosephson.com/vmo-upgrade/' . (!empty($installation_id) ? '?installation=' . urlencode($installation_id) : '');
    $topup_url = 'https://jerremejosephson.com/vmo-topup/' . (!empty($installation_id) ? '?installation=' . urlencode($installation_id) : '');
    // In-plugin "My Plan" deep links to the relevant section.
    $my_plan_url = admin_url('admin.php?page=vmo-my-plan');
    $my_plan_plans_url = $my_plan_url . '#vmo-plans-section';
    $my_plan_boosters_url = $my_plan_url . '#vmo-boosters-section';
    $credit_config = function_exists('vmo_get_frontend_credit_config')
        ? vmo_get_frontend_credit_config(get_current_user_id())
        : ['token_balance' => (int) get_user_meta(get_current_user_id(), 'vmo_token_balance', true)];
    
    $ajax_data = [
        'ajaxurl'       => admin_url('admin-ajax.php'),
        'nonce'         => wp_create_nonce('vmo_builder_nonce'),
        'security'      => wp_create_nonce('vmo_builder_nonce'),
        // CLIENT VERSION: Require an active subscription from the master server
        'current_plan'  => get_user_meta(get_current_user_id(), 'vmo_subscription_status', true),
        'token_balance' => (int) ($credit_config['token_balance'] ?? get_user_meta(get_current_user_id(), 'vmo_token_balance', true)),
        'plan_tokens' => (int) ($credit_config['plan_tokens'] ?? 0),
        'credit_remaining_percent' => (float) ($credit_config['credit_remaining_percent'] ?? 100),
        'credit_usage_percent' => (float) ($credit_config['credit_usage_percent'] ?? 0),
        'low_credit_remaining_percent' => (float) ($credit_config['low_credit_remaining_percent'] ?? 40),
        'critical_credit_remaining_percent' => (float) ($credit_config['critical_credit_remaining_percent'] ?? 20),
        'low_credit' => (bool) ($credit_config['low_credit'] ?? false),
        'critical_credit' => (bool) ($credit_config['critical_credit'] ?? false),
        'credit_warning_level' => (string) ($credit_config['credit_warning_level'] ?? 'none'),
        'no_credit' => (bool) ($credit_config['no_credit'] ?? false),
        'next_plan' => (string) ($credit_config['next_plan'] ?? 'Starter'),
        'has_selected_plan' => (bool) ($credit_config['has_selected_plan'] ?? false),
        'entitlement_active' => (bool) ($credit_config['entitlement_active'] ?? false),
        'free_access_active' => (bool) ($credit_config['free_access_active'] ?? false),
        'free_access_expired' => (bool) ($credit_config['free_access_expired'] ?? false),
        'can_use_cost_bearing_services' => (bool) ($credit_config['can_use_cost_bearing_services'] ?? false),
        'service_locked' => (bool) ($credit_config['service_locked'] ?? false),
        'service_lock_reason' => (string) ($credit_config['service_lock_reason'] ?? ''),
        'subscription_payment_status' => (string) ($credit_config['subscription_payment_status'] ?? ''),
        'upgradeUrl'    => $upgrade_url,
        'topupUrl'      => $topup_url,
        'myPlanUrl'        => $my_plan_url,
        'myPlanPlansUrl'   => $my_plan_plans_url,
        'myPlanBoostersUrl'=> $my_plan_boosters_url,
        'business_wizard' => [
            'enabled' => true,
            'cost_per_session' => defined('VMO_TOKENS_BUSINESS_WIZARD') ? VMO_TOKENS_BUSINESS_WIZARD : 1500,
            'generation_cost' => defined('VMO_TOKENS_GENERATE_WEBSITE') ? VMO_TOKENS_GENERATE_WEBSITE : 2500
        ],
        'pluginUrl' => plugin_dir_url(__FILE__),
        'userId' => get_current_user_id(),
        'user_display_name' => vmo_get_greeting_display_name(),
        'pending_notification' => get_user_meta(get_current_user_id(), 'vmo_pending_notification', true),
        'quicklaunch_welcome' => function_exists('vmo_quicklaunch_get_welcome_client_data') ? vmo_quicklaunch_get_welcome_client_data(get_current_user_id()) : array(),
        'is_client_mode' => ((defined('VMO_CLIENT_MODE') && VMO_CLIENT_MODE === true) || get_option('vmo_quicklaunch_hosted_client') === '1'),
        'builder_reset_mode' => function_exists('vmo_builder_reset_mode_active') ? (bool) vmo_builder_reset_mode_active(get_current_user_id()) : false,
        // Background ops upgrade prompts (Tester plan missed operations)
        'upgrade_prompts' => get_user_meta(get_current_user_id(), 'vmo_upgrade_prompts', true) ?: [],
    ];

    if (wp_script_is('vmo-init', 'enqueued')) {
        wp_localize_script('vmo-init', 'vmo_ajax', $ajax_data);
    }
    
    // Token system (needs vmo_ajax early)
    if (wp_script_is('vmo-tokens', 'enqueued')) {
        wp_localize_script('vmo-tokens', 'vmo_ajax', $ajax_data);
    }
    
    // Jarvis AI (Voice Panel)
    if (wp_script_is('vmo-voice-panel', 'enqueued')) {
        wp_localize_script('vmo-voice-panel', 'vmoAjax', $ajax_data);
        wp_localize_script('vmo-voice-panel', 'vmo_ajax', $ajax_data);
    }
    
    // Business Consultant AI - V2 (Persistent Memory System)
    if (wp_script_is('vmo-intelligent-consultation', 'enqueued')) {
        wp_localize_script('vmo-intelligent-consultation', 'vmoAjax', $ajax_data);
        wp_localize_script('vmo-intelligent-consultation', 'vmo_ajax', $ajax_data);

        // Belt-and-suspenders: ensure wpApiSettings is also available via consultation script
        // (normally provided by vmo-tts-hd dependency, but adding here for resilience)
        wp_add_inline_script('vmo-intelligent-consultation',
            'if(typeof window.wpApiSettings==="undefined"){window.wpApiSettings=' . wp_json_encode([
                'root'  => esc_url_raw(rest_url()),
                'nonce' => wp_create_nonce('wp_rest'),
            ]) . ';}',
            'before'
        );

        $greeting_text = function_exists('vmo_get_consultation_greeting_text') ? vmo_get_consultation_greeting_text() : '';
        $greeting_user_name = vmo_get_greeting_display_name();
        $greeting_bootstrap = [
            'text' => $greeting_text,
            'userName' => $greeting_user_name,
            'audioUrl' => '',
            'generatedAt' => null,
            'expiresAt' => null,
            'voice' => apply_filters('vmo_consultation_greeting_voice', 'ash'),
            'restEndpoint' => esc_url_raw(rest_url('vmo/v1/greeting-audio')),
        ];

        if (class_exists('VMO_Greeting_Audio_Manager')) {
            $greeting_manager = VMO_Greeting_Audio_Manager::get_instance();
            $audio_meta = $greeting_manager->get_audio_metadata(true);
            if (!is_wp_error($audio_meta) && !empty($audio_meta['url'])) {
                $greeting_bootstrap['audioUrl'] = esc_url_raw($audio_meta['url']);
                $greeting_bootstrap['generatedAt'] = isset($audio_meta['generated_at']) ? (int) $audio_meta['generated_at'] : null;
                $greeting_bootstrap['expiresAt'] = isset($audio_meta['expires_at']) ? (int) $audio_meta['expires_at'] : null;
                if (!empty($audio_meta['voice'])) {
                    $greeting_bootstrap['voice'] = $audio_meta['voice'];
                }
            }
        }

        $inline_greeting_bootstrap = 'window.vmoGreetingBootstrap = ' . wp_json_encode($greeting_bootstrap) . ';'
            . 'window.vmoGreetingText = window.vmoGreetingBootstrap?.text || window.vmoGreetingText;'
            . 'window.vmoGreetingAudioUrl = window.vmoGreetingBootstrap?.audioUrl || window.vmoGreetingAudioUrl;';
        wp_add_inline_script('vmo-intelligent-consultation', $inline_greeting_bootstrap, 'before');
    }

    // VOICE SYSTEM REMOVED: Voice system v8.0 localization removed for No Voice version
    // Voice system localization was here - removed because vmo-voice-system-v8 script doesn't exist

    // Shared incoming-call bridge for all VMO admin pages. The bridge itself
    // detects the strategic dashboard and disables itself there, but on builder
    // and other pages it keeps incoming calls visible even if browser-audio
    // registration is unavailable. It must not depend on the Twilio SDK just
    // to poll/ring/show controls.
    $page = isset($_GET['page']) ? sanitize_text_field(wp_unslash($_GET['page'])) : '';

    $twilio_sdk_path = VMO_AI_BUILDER_DIR . 'assets/vendor/twilio-voice-sdk.min.js';
    if (file_exists($twilio_sdk_path)) {
        wp_enqueue_script(
            'vmo-twilio-voice-sdk',
            VMO_AI_BUILDER_URL . 'assets/vendor/twilio-voice-sdk.min.js',
            [],
            vmo_safe_filemtime($twilio_sdk_path),
            true
        );
    }

    $phone_bridge_path = VMO_AI_BUILDER_DIR . 'assets/vmo-phone-bridge.js';
    if (file_exists($phone_bridge_path)) {
        wp_enqueue_script(
            'vmo-phone-bridge',
            VMO_AI_BUILDER_URL . 'assets/vmo-phone-bridge.js',
            ['jquery', 'vmo-state'],
            vmo_safe_filemtime($phone_bridge_path),
            true
        );
        wp_localize_script('vmo-phone-bridge', 'vmoPhoneBridge', [
            'ajaxurl' => admin_url('admin-ajax.php'),
            'security' => wp_create_nonce('vmo_builder_nonce'),
            'vmo_nonce' => wp_create_nonce('vmo_nonce'),
            'page' => $page,
            'dialerUrl' => admin_url('admin.php?page=vmo-ai-dashboard&section=phone'),
            'userId' => get_current_user_id(),
            'userDisplayName' => vmo_get_greeting_display_name(),
            'twilioSdkLoaded' => wp_script_is('vmo-twilio-voice-sdk', 'enqueued') || wp_script_is('vmo-twilio-voice-sdk', 'done'),
            'ringEnabled' => (int) (get_user_meta(get_current_user_id(), 'vmo_ring_enabled', true) !== '0'),
            'ringVolume' => (float) (get_user_meta(get_current_user_id(), 'vmo_ring_volume', true) ?: 0.6),
        ]);
    }

    // Enhanced Business Phone Admin assets
    $screen = get_current_screen();
    if ($screen && isset($screen->id) && strpos($screen->id, 'vmo-phone') !== false) {
        // CSS
        if (file_exists(VMO_AI_BUILDER_DIR . 'assets/vmo-phone.css')) {
            wp_enqueue_style('vmo-phone', VMO_AI_BUILDER_URL . 'assets/vmo-phone.css', [], vmo_safe_filemtime(VMO_AI_BUILDER_DIR . 'assets/vmo-phone.css'));
        }
        // Enhanced Business Phone Admin JS
        if (file_exists(VMO_AI_BUILDER_DIR . 'assets/vmo-business-phone-admin.js')) {
            wp_enqueue_script('vmo-business-phone-admin', VMO_AI_BUILDER_URL . 'assets/vmo-business-phone-admin.js', ['jquery'], vmo_safe_filemtime(VMO_AI_BUILDER_DIR . 'assets/vmo-business-phone-admin.js'), true);
            wp_localize_script('vmo-business-phone-admin', 'vmo_admin_ajax', [
                'ajaxurl' => admin_url('admin-ajax.php'),
                'nonce'   => wp_create_nonce('vmo_admin_nonce'),
                'costs'   => [
                    'twilio_per_minute' => 0.10, // Actual AI voice cost (not basic PSTN)
                    'vmo_tokens_per_minute' => 300, // 2x markup for profitability
                    'openai_per_1k_tokens' => 0.03
                ],
                'markup'  => get_option('vmo_phone_markup_multiplier', 2), // 2x markup (was 9x)
                'strings' => [
                    'activating' => 'Activating...',
                    'deactivating' => 'Deactivating...',
                    'processing' => 'Processing...',
                    'confirmDeactivate' => 'Are you sure you want to deactivate your phone service?',
                    'networkError' => 'Network error occurred'
                ]
            ]);
        }
    }
} );

// Ensure business dashboard JS is loaded on our Admin Dashboard page
add_action('admin_enqueue_scripts', function() {
    $page = isset($_GET['page']) ? sanitize_text_field(wp_unslash($_GET['page'])) : '';
    // Allow dashboard JS on both the main dashboard AND the master admin
    if ($page !== 'vmo-ai-dashboard' && $page !== 'vmo-master-admin') return;
    $path = VMO_AI_BUILDER_DIR . 'assets/vmo-business-dashboard.js';
    if (file_exists($path)) {
        wp_enqueue_script('vmo-business-dashboard', VMO_AI_BUILDER_URL . 'assets/vmo-business-dashboard.js', ['jquery', 'vmo-state'], filemtime($path), true);
        
        // Build robust AJAX data for dashboard (must include plan status for gatekeeper)
        $installation_id = get_option('vmo_installation_id', '');
        $upgrade_url = 'https://jerremejosephson.com/vmo-upgrade/' . (!empty($installation_id) ? '?installation=' . urlencode($installation_id) : '');
        $topup_url = 'https://jerremejosephson.com/vmo-topup/' . (!empty($installation_id) ? '?installation=' . urlencode($installation_id) : '');
        $my_plan_url = admin_url('admin.php?page=vmo-my-plan');
        $my_plan_plans_url = $my_plan_url . '#vmo-plans-section';
        $my_plan_boosters_url = $my_plan_url . '#vmo-boosters-section';
        $dashboard_credit_config = function_exists('vmo_get_frontend_credit_config')
            ? vmo_get_frontend_credit_config(get_current_user_id())
            : ['token_balance' => (int) get_user_meta(get_current_user_id(), 'vmo_token_balance', true)];
        
        $dashboard_ajax_data = [
            'ajaxurl'       => admin_url('admin-ajax.php'),
            'rest_url'      => get_rest_url(null, 'vmo/v1'),
            'nonce'         => wp_create_nonce('vmo_builder_nonce'),
            'vmo_nonce'     => wp_create_nonce('vmo_nonce'), // Added standard nonce for Twilio/Phone operations
            'security'      => wp_create_nonce('vmo_builder_nonce'),
            'current_plan'  => get_user_meta(get_current_user_id(), 'vmo_subscription_status', true),
            'has_pack'      => get_user_meta(get_current_user_id(), 'vmo_has_phone_pack', true) === 'true',
            'token_balance' => (int) ($dashboard_credit_config['token_balance'] ?? get_user_meta(get_current_user_id(), 'vmo_token_balance', true)),
            'plan_tokens' => (int) ($dashboard_credit_config['plan_tokens'] ?? 0),
            'credit_remaining_percent' => (float) ($dashboard_credit_config['credit_remaining_percent'] ?? 100),
            'credit_usage_percent' => (float) ($dashboard_credit_config['credit_usage_percent'] ?? 0),
            'low_credit_remaining_percent' => (float) ($dashboard_credit_config['low_credit_remaining_percent'] ?? 40),
            'critical_credit_remaining_percent' => (float) ($dashboard_credit_config['critical_credit_remaining_percent'] ?? 20),
            'low_credit' => (bool) ($dashboard_credit_config['low_credit'] ?? false),
            'critical_credit' => (bool) ($dashboard_credit_config['critical_credit'] ?? false),
            'credit_warning_level' => (string) ($dashboard_credit_config['credit_warning_level'] ?? 'none'),
            'no_credit' => (bool) ($dashboard_credit_config['no_credit'] ?? false),
            'next_plan' => (string) ($dashboard_credit_config['next_plan'] ?? 'Starter'),
            'has_selected_plan' => (bool) ($dashboard_credit_config['has_selected_plan'] ?? false),
            'entitlement_active' => (bool) ($dashboard_credit_config['entitlement_active'] ?? false),
            'free_access_active' => (bool) ($dashboard_credit_config['free_access_active'] ?? false),
            'free_access_expired' => (bool) ($dashboard_credit_config['free_access_expired'] ?? false),
            'can_use_cost_bearing_services' => (bool) ($dashboard_credit_config['can_use_cost_bearing_services'] ?? false),
            'payments_enabled' => (bool) defined('VMO_PAYMENTS_ENABLED') ? VMO_PAYMENTS_ENABLED : true,
            'upgradeUrl'    => $upgrade_url,
            'topupUrl'      => $topup_url,
            'myPlanUrl'        => $my_plan_url,
            'myPlanPlansUrl'   => $my_plan_plans_url,
            'myPlanBoostersUrl'=> $my_plan_boosters_url,
            'stripe' => [
                'pk' => get_option('vmo_stripe_publishable_key', ''),
            ],
            'userId' => get_current_user_id(),
            'user_display_name' => vmo_get_greeting_display_name(),
            'plugin_url' => VMO_AI_BUILDER_URL
        ];
        
        wp_localize_script('vmo-business-dashboard', 'vmo_ajax', $dashboard_ajax_data);
    }
    
    // ═══════════════════════════════════════════════════════════════════════════════
    // ✅ JARVIS AI VOICE PANEL - Same panel as in the builder, with same chat history
    // This allows users to continue Jarvis conversations inside the dashboard
    // ═══════════════════════════════════════════════════════════════════════════════
    
    // Audio Coordinator (must load first)
    $audio_coord_path = VMO_AI_BUILDER_DIR . 'assets/vmo-audio-coordinator.js';
    if (file_exists($audio_coord_path)) {
        wp_enqueue_script('vmo-audio-coordinator', VMO_AI_BUILDER_URL . 'assets/vmo-audio-coordinator.js', ['jquery'], filemtime($audio_coord_path), true);
    }
    
    // Voice Panel CSS
    $voice_panel_css_path = VMO_AI_BUILDER_DIR . 'assets/vmo-voice-panel.css';
    if (file_exists($voice_panel_css_path)) {
        wp_enqueue_style('vmo-voice-panel-css', VMO_AI_BUILDER_URL . 'assets/vmo-voice-panel.css', [], filemtime($voice_panel_css_path));
    }
    
    // OpenAI TTS HD Voice Selector
    $voice_selector_js_path = VMO_AI_BUILDER_DIR . 'assets/vmo-voice-selector.js';
    $voice_selector_css_path = VMO_AI_BUILDER_DIR . 'assets/vmo-voice-selector.css';
    
    if (file_exists($voice_selector_js_path)) {
        wp_enqueue_script('vmo-voice-selector', VMO_AI_BUILDER_URL . 'assets/vmo-voice-selector.js', ['jquery'], filemtime($voice_selector_js_path), true);
    }
    
    if (file_exists($voice_selector_css_path)) {
        wp_enqueue_style('vmo-voice-selector', VMO_AI_BUILDER_URL . 'assets/vmo-voice-selector.css', [], filemtime($voice_selector_css_path));
    }
    
    // OpenAI TTS HD REST API Client (No API key exposure on frontend)
    $tts_hd_js_path = VMO_AI_BUILDER_DIR . 'assets/vmo-tts-hd.js';
    if (file_exists($tts_hd_js_path)) {
        // FORCE CACHE BUST - Oct 30 2025 critical fix
        $tts_version = '9.0.1-FIXED-' . time() . '-' . filemtime($tts_hd_js_path);
        wp_enqueue_script('vmo-tts-hd', VMO_AI_BUILDER_URL . 'assets/vmo-tts-hd.js', ['vmo-audio-coordinator'], $tts_version, true);
        // Localize REST API URL and nonce
        wp_localize_script('vmo-tts-hd', 'wpApiSettings', [
            'root' => esc_url_raw(rest_url()),
            'nonce' => wp_create_nonce('wp_rest')
        ]);
        wp_localize_script('vmo-tts-hd', 'vmoClientProxy', vmo_client_direct_master_voice_proxy_config());
        if (defined('WP_DEBUG') && WP_DEBUG) error_log('VMO: TTS HD client enqueued successfully with version ' . $tts_version);
    } else {
        if (defined('WP_DEBUG') && WP_DEBUG) error_log('VMO: TTS HD client file missing - voice features will not work');
    }
    
    // ✅ JARVIS AI VOICE PANEL - Main voice panel script
    $voice_panel_js_path = VMO_AI_BUILDER_DIR . 'assets/vmo-voice-panel.js';
    if (file_exists($voice_panel_js_path)) {
        // Realtime (WebRTC) module — must load BEFORE the voice panel so the
        // panel can detect VMORealtimeSession on construction.
        $realtime_js_path = VMO_AI_BUILDER_DIR . 'assets/vmo-realtime.js';
        if (file_exists($realtime_js_path)) {
            $rt_version = '1.0.0-REALTIME-' . filemtime($realtime_js_path);
            wp_enqueue_script('vmo-realtime', VMO_AI_BUILDER_URL . 'assets/vmo-realtime.js', [], $rt_version, true);
            wp_localize_script('vmo-realtime', 'vmo_ajax', [
                'ajaxurl' => admin_url('admin-ajax.php'),
                'nonce'   => wp_create_nonce('vmo_builder_nonce'),
                'security'=> wp_create_nonce('vmo_builder_nonce'),
                'is_client_mode' => ((defined('VMO_CLIENT_MODE') && VMO_CLIENT_MODE === true) || get_option('vmo_quicklaunch_hosted_client') === '1'),
            ]);
            wp_localize_script('vmo-realtime', 'vmoClientProxy', vmo_client_direct_master_voice_proxy_config());
        }
        $voice_panel_version = '9.0.0-DASHBOARD-' . time() . '-' . filemtime($voice_panel_js_path);
        wp_enqueue_script('vmo-voice-panel', VMO_AI_BUILDER_URL . 'assets/vmo-voice-panel.js', ['jquery', 'vmo-tts-hd', 'vmo-audio-coordinator', 'vmo-realtime'], $voice_panel_version, true);
        $voice_credit_config = function_exists('vmo_get_frontend_credit_config')
            ? vmo_get_frontend_credit_config(get_current_user_id())
            : ['token_balance' => (int) get_user_meta(get_current_user_id(), 'vmo_token_balance', true)];
        $voice_ajax_data = array_merge([
            'ajaxurl' => admin_url('admin-ajax.php'),
            'nonce'   => wp_create_nonce('vmo_builder_nonce'),
            'upgradeUrl' => $upgrade_url ?? '',
            'topupUrl' => $topup_url ?? '',
            'myPlanUrl'         => admin_url('admin.php?page=vmo-my-plan'),
            'myPlanPlansUrl'    => admin_url('admin.php?page=vmo-my-plan') . '#vmo-plans-section',
            'myPlanBoostersUrl' => admin_url('admin.php?page=vmo-my-plan') . '#vmo-boosters-section',
            'is_client_mode' => ((defined('VMO_CLIENT_MODE') && VMO_CLIENT_MODE === true) || get_option('vmo_quicklaunch_hosted_client') === '1'),
        ], $voice_credit_config);
        // Localize AJAX globals for Jarvis
        wp_localize_script('vmo-voice-panel', 'vmo_ajax', $voice_ajax_data);
        wp_localize_script('vmo-voice-panel', 'vmoAjax', $voice_ajax_data);
        if (defined('WP_DEBUG') && WP_DEBUG) error_log('VMO: Voice Panel enqueued for dashboard with version ' . $voice_panel_version);
    }
    
    // ✅ NEW V2 CONSULTATION SYSTEM - Clean, persistent, works like MVP
    $v2_js_path = VMO_AI_BUILDER_DIR . 'assets/vmo-business-consultation-v2.js';
    if (file_exists($v2_js_path)) {
        $v2_version = '2.0.0-PERSISTENT-' . time() . '-' . filemtime($v2_js_path);
        wp_enqueue_script('vmo-consultation-v2', VMO_AI_BUILDER_URL . 'assets/vmo-business-consultation-v2.js', ['jquery'], $v2_version, true);
        if (defined('WP_DEBUG') && WP_DEBUG) error_log('VMO: Consultation V2 enqueued with version ' . $v2_version);
    } else {
        if (defined('WP_DEBUG') && WP_DEBUG) error_log('VMO: Consultation V2 file missing');
    }
});

// === Admin AJAX: DB status check and repair (for dashboard badge) ===
function vmo_db_required_tables() {
    global $wpdb;
    return [
        $wpdb->prefix . 'vmo_projects',
        $wpdb->prefix . 'vmo_project_history',
        $wpdb->prefix . 'vmo_consultations',
        $wpdb->prefix . 'vmo_approval_queue',
        $wpdb->prefix . 'vmo_contacts',
        $wpdb->prefix . 'vmo_contact_interactions',
        $wpdb->prefix . 'vmo_contact_purchases',
        $wpdb->prefix . 'vmo_contact_preferences',
        $wpdb->prefix . 'vmo_voice_conversations',
        $wpdb->prefix . 'vmo_voice_conversation_turns',
    ];
}

function vmo_db_tables_status() {
    global $wpdb;
    $tables = vmo_db_required_tables();
    $status = [];
    foreach ($tables as $t) {
        $exists = (bool) $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $wpdb->esc_like($t)));
        $status[$t] = $exists;
    }
    return $status;
}

add_action('wp_ajax_vmo_db_status_check', function() {
    // flexible nonce: accept 'security' or 'nonce'
    $nonce = isset($_POST['security']) ? $_POST['security'] : (isset($_POST['nonce']) ? $_POST['nonce'] : '');
    if (!wp_verify_nonce($nonce, 'vmo_builder_nonce')) {
        wp_send_json_error(['message' => 'Invalid nonce'], 403);
    }
    $status = vmo_db_tables_status();
    $ok = !in_array(false, array_values($status), true);
    $last_scan = (int) get_option('vmo_last_deep_scan_ts', 0);
    $last_update = (int) get_option('vmo_dashboard_last_update_ts', 0);
    wp_send_json_success([
        'ok' => $ok,
        'status' => $status,
        'missing' => array_keys(array_filter($status, function($v){ return !$v; })),
        'last_scan_ts' => $last_scan,
        'last_update_ts' => $last_update,
    ]);
});

add_action('wp_ajax_vmo_db_status_repair', function() {
    $nonce = isset($_POST['security']) ? $_POST['security'] : (isset($_POST['nonce']) ? $_POST['nonce'] : '');
    if (!wp_verify_nonce($nonce, 'vmo_builder_nonce')) {
        wp_send_json_error(['message' => 'Invalid nonce'], 403);
    }
    try {
        vmo_create_plugin_tables();
    } catch (Throwable $e) {
        // continue to re-check status; if still missing, report error
    }
    $status = vmo_db_tables_status();
    $ok = !in_array(false, array_values($status), true);
    wp_send_json_success([
        'ok' => $ok,
        'status' => $status,
        'missing' => array_keys(array_filter($status, function($v){ return !$v; })),
    ]);
});

// === Subscription Checkout Handler ===
// MOVED TO: includes/modules/subscription/class-vmo-subscription-manager.php
// add_action('wp_ajax_vmo_subscription_checkout', function() {
//     check_ajax_referer('vmo_builder_nonce', 'security');
//     ...
// });

// === Token Top-Up Checkout Handler ===
add_action('wp_ajax_vmo_topup_checkout', function() {
    check_ajax_referer('vmo_builder_nonce', 'security');

    $topup = sanitize_text_field($_POST['topup'] ?? '');
    $user_id = get_current_user_id();
    $user = get_userdata($user_id);

    if (!$user || empty($user->user_email)) {
        wp_send_json_error(['message' => 'User email is missing.']);
    }

    // ── Client mode: route top-up through master checkout proxy ──
    if (defined('VMO_CLIENT_MODE') && VMO_CLIENT_MODE === true) {
        // Map top-up tiers to plan-style names the master recognizes
        $topup_to_plan = [
            'phone_m' => 'phone_m', 'phone_l' => 'phone_l',
            'power_dialer' => 'power_dialer', 'phone_xl' => 'phone_xl',
        ];
        $proxy_plan = $topup_to_plan[$topup] ?? '';
        if (!$proxy_plan) {
            // General top-ups: send raw topup tier + amount to master
            $proxy_plan = 'topup_' . $topup;
        }

        $proxy_base = defined('VMO_MASTER_PROXY_URL')
            ? preg_replace('#/proxy$#', '', VMO_MASTER_PROXY_URL)
            : rtrim(get_option('vmo_master_proxy_url', 'https://getvmo.com/wp-json/vmo-builder/v1/proxy'), '/proxy');

        $response = wp_remote_post($proxy_base . '/stripe-checkout/create', [
            'headers' => [
                'Content-Type'       => 'application/json',
                'X-VMO-Installation' => get_option('vmo_installation_id', ''),
                'X-VMO-License'      => get_option('vmo_license_key', ''),
                'X-VMO-Plan'         => get_user_meta($user_id, 'vmo_subscription_status', true),
            ],
            'body'    => wp_json_encode([
                'plan'           => $proxy_plan,
                'billing_cycle'  => 'monthly',
                'customer_email' => $user->user_email,
                'success_url'    => admin_url('admin.php?page=vmo-my-plan&payment=success&purchase=addon&session_id={CHECKOUT_SESSION_ID}'),
                'cancel_url'     => admin_url('admin.php?page=vmo-my-plan'),
            ]),
            'timeout' => 30,
        ]);

        if (is_wp_error($response)) {
            wp_send_json_error(['message' => 'Could not connect to payment server.']);
            return;
        }
        $result = json_decode(wp_remote_retrieve_body($response), true);
        if (!empty($result['checkout_url'])) {
            wp_send_json_success(['checkout_url' => $result['checkout_url']]);
        } else {
            wp_send_json_error(['message' => $result['message'] ?? 'Payment initialization failed.']);
        }
        return;
    }
    
    // Phone Packs require an active subscription plan (bypassed in master version)
    $phone_packs = ['phone_s', 'phone_m', 'phone_l', 'power_dialer'];
    if (in_array($topup, $phone_packs) && !defined('VMO_MASTER_VERSION') || (defined('VMO_MASTER_VERSION') && !VMO_MASTER_VERSION)) {
        $plan = get_user_meta($user_id, 'vmo_subscription_status', true);
        $active_plans = ['tester', 'starter', 'pro', 'elite', 'enterprise', 'starter_pack', 'pro_pack', 'business_pack', 'enterprise_pack', 'business'];
        
        if (empty($plan) || !in_array(strtolower($plan), $active_plans)) {
            wp_send_json_error([
                'message' => 'Phone Packs require an active subscription plan. Please upgrade your plan first.',
                'requires_plan' => true
            ]);
        }
    }

    // Validate top-up tier and map to configured Stripe Price IDs.
    // Source of truth: VMO_Booster_Catalog (includes/orchestration/class-vmo-booster-catalog.php).
    // Change a price/grant/Stripe option key there and every UI surface + this
    // handler + the webhook credit logic pick it up automatically.
    $booster = class_exists('VMO_Booster_Catalog') ? VMO_Booster_Catalog::get($topup) : null;
    if (!$booster) {
        wp_send_json_error(['message' => 'Invalid selection.']);
    }

    $price_id = class_exists('VMO_Booster_Catalog') ? VMO_Booster_Catalog::get_price_id($topup) : '';
    if (!$price_id) {
        wp_send_json_error(['message' => 'This add-on is not configured. Please contact support.']);
    }

    $booster_token_grant = (int) ($booster['tokens'] ?? 0);

    $secret = get_option('vmo_stripe_secret_key');
    if (!$secret) {
        wp_send_json_error(['message' => 'Stripe is not configured. Please contact support.']);
    }

    // Load Stripe SDK
    $stripe_init_file = VMO_AI_BUILDER_DIR . 'stripe-sdk/init.php';
    if (!file_exists($stripe_init_file) || !is_readable($stripe_init_file)) {
        wp_send_json_error(['message' => 'Payment system not available.']);
    }

    try {
        require_once $stripe_init_file;
        \Stripe\Stripe::setApiKey($secret);

        // Create one-time payment checkout session
        $session = \Stripe\Checkout\Session::create([
            'payment_method_types' => ['card'],
            'mode' => 'payment',  // One-time payment, not subscription
            'client_reference_id' => (string) $user_id,
            'customer_email' => $user->user_email,
            'line_items' => [[
                'price' => $price_id,
                'quantity' => 1
            ]],
            'success_url' => admin_url('admin.php?page=vmo-my-plan&payment=success&purchase=addon&session_id={CHECKOUT_SESSION_ID}'),
            'cancel_url' => admin_url('admin.php?page=vmo-my-plan'),
            'metadata' => [
                'type' => 'token_topup',
                'topup_tier' => $topup,
                'user_id' => $user_id,
                'tokens' => $booster_token_grant,
            ]
        ]);

        wp_send_json_success(['checkout_url' => $session->url]);
    } catch (Exception $e) {
        if (defined('WP_DEBUG') && WP_DEBUG) error_log('VMO Top-Up Checkout Error: ' . $e->getMessage());
        wp_send_json_error(['message' => 'Unable to create checkout session. Please try again.']);
    }
});

if (!function_exists('vmo_is_valid_openai_realtime_project_id')) {
    function vmo_is_valid_openai_realtime_project_id($project_id): bool {
        $project_id = trim((string) $project_id);
        return $project_id !== '' && preg_match('/^proj_[A-Za-z0-9_-]+$/', $project_id) === 1;
    }
}

if (!function_exists('vmo_sanitize_openai_realtime_project_id')) {
    function vmo_sanitize_openai_realtime_project_id($value): string {
        $value = trim((string) $value);
        if ($value === '') {
            return '';
        }
        if (vmo_is_valid_openai_realtime_project_id($value)) {
            return $value;
        }
        add_settings_error(
            'vmo_openai_project_id',
            'vmo_openai_project_id_invalid',
            'OpenAI Realtime Project ID must be the exact value from OpenAI Project > General and must begin with proj_.',
            'error'
        );
        return (string) get_option('vmo_openai_project_id', '');
    }
}

if (!function_exists('vmo_check_openai_project_api_access')) {
    function vmo_check_openai_project_api_access($api_key, $project_id): array {
        $api_key = trim((string) $api_key);
        $project_id = trim((string) $project_id);

        if ($api_key === '') {
            return array(
                'valid' => false,
                'message' => 'Skipped — OpenAI API key is missing.',
                'status_code' => 0,
                'openai_request_id' => '',
                'client_request_id' => '',
                'response_body' => '',
            );
        }
        if (!vmo_is_valid_openai_realtime_project_id($project_id)) {
            return array(
                'valid' => false,
                'message' => 'Skipped — OpenAI Realtime Project ID is missing or invalid.',
                'status_code' => 0,
                'openai_request_id' => '',
                'client_request_id' => '',
                'response_body' => '',
            );
        }

        $client_request_id = 'vmo-project-check-' . wp_generate_uuid4();
        $resp = wp_remote_get('https://api.openai.com/v1/models?limit=1', array(
            'timeout' => 10,
            'headers' => array(
                'Authorization' => 'Bearer ' . $api_key,
                'OpenAI-Project' => $project_id,
                'X-Client-Request-Id' => $client_request_id,
            ),
        ));

        if (is_wp_error($resp)) {
            return array(
                'valid' => false,
                'message' => 'Network error while checking API key access to the configured OpenAI project: ' . $resp->get_error_message(),
                'status_code' => 0,
                'openai_request_id' => '',
                'client_request_id' => $client_request_id,
                'response_body' => '',
            );
        }

        $status_code = (int) wp_remote_retrieve_response_code($resp);
        $response_body = (string) wp_remote_retrieve_body($resp);
        $openai_request_id = (string) wp_remote_retrieve_header($resp, 'x-request-id');
        $valid = ($status_code === 200);
        $message = $valid
            ? 'The configured OpenAI API key can access the configured project.'
            : ('HTTP ' . $status_code . ' while targeting project ' . $project_id . ' with the configured API key — this usually means the API key, project id, or organization context do not match.');
        if (!$valid && $openai_request_id !== '') {
            $message .= ' OpenAI request id: ' . $openai_request_id . '.';
        }

        return array(
            'valid' => $valid,
            'message' => $message,
            'status_code' => $status_code,
            'openai_request_id' => $openai_request_id,
            'client_request_id' => $client_request_id,
            'response_body' => substr($response_body, 0, 300),
        );
    }
}

// === Register Settings ===
add_action('admin_init', function () {
    $vmo_bridge_url_sanitizer = function ($value) {
        $value = trim((string) $value);
        $value = preg_replace('/\s+/', '', $value);
        if ($value === '') {
            return '';
        }
        return preg_match('#^(wss?|https?)://#i', $value) ? rtrim($value, '/') : '';
    };

    foreach ([
        'vmo_openai_api_key',
        'vmo_openai_project_id',
        'vmo_openai_webhook_secret',
        'vmo_phone_bridge_url',
        'vmo_media_secret',
        'vmo_listen_secret',
        'vmo_enable_legacy_realtime_sip',
        'vmo_stripe_secret_key',
    'vmo_stripe_publishable_key',
        'vmo_stripe_webhook_secret',
    'vmo_payments_enabled',
    'vmo_stripe_platform_fee_bps',
    // Legacy price IDs (kept for back-compat; not shown in UI)
    'vmo_price_starter',
    'vmo_price_pro',
    'vmo_price_elite',
    'vmo_price_topup',
    // Top-up (one-time add-on) Stripe Price IDs
    'vmo_price_topup_small',
    'vmo_price_topup_medium',
    'vmo_price_topup_large',
    // Current plan product/price IDs (5 tiers)
    'vmo_plan_tester_product',      // $49.99/month - Tester plan
    'vmo_plan_starter_product',     // $99.99/month - Starter plan
    'vmo_plan_pro_product',         // $199.99/month - Pro plan
    'vmo_plan_elite_product',       // $499.99/month - Elite plan (formerly business)
    'vmo_plan_business_product',    // Legacy: maps to elite
    'vmo_plan_enterprise_product',  // $999.99/month - Enterprise plan
    'vmo_price_tester',             // Stripe price ID for Tester
    'vmo_price_enterprise',         // Stripe price ID for Enterprise
    // Annual Price IDs
    'vmo_price_tester_annual',
    'vmo_price_starter_annual',
    'vmo_price_pro_annual',
    'vmo_price_elite_annual',
    'vmo_price_enterprise_annual',
    // QuickLaunch/Vultr hosted subscription Price IDs
    'vmo_quicklaunch_price_tester_monthly',
    'vmo_quicklaunch_price_tester_annual',
    'vmo_quicklaunch_price_starter_monthly',
    'vmo_quicklaunch_price_starter_annual',
    'vmo_quicklaunch_price_pro_monthly',
    'vmo_quicklaunch_price_pro_annual',
    'vmo_quicklaunch_price_elite_monthly',
    'vmo_quicklaunch_price_elite_annual',
    'vmo_quicklaunch_price_enterprise_monthly',
    'vmo_quicklaunch_price_enterprise_annual',
        'vmo_enable_whisper',
        'vmo_enable_voice_chat',
        // VOICE SYSTEM REMOVED: Voice settings removed for No Voice version
        // Twilio integration
        'vmo_twilio_account_sid',
        'vmo_twilio_auth_token',
        'vmo_twilio_api_key_sid',
        'vmo_twilio_api_key_secret',
        'vmo_twilio_voice_webhook_url',
        'vmo_twilio_status_callback_url',
        'vmo_twilio_phone_number',
    'vmo_enable_phone_assistant',
    // Node voice service + integration
    'vmo_node_service_url',
    'vmo_node_service_token',
    // Owner/alert preferences used by realtime manager
    'vmo_owner_phone_number',
    'vmo_auto_dial_owner',
    'vmo_owner_dial_delay',
    'vmo_enable_sms_alerts',
    'vmo_enable_email_alerts',
    'vmo_receptionist_language',
    // QuickLaunch hosting (Vultr + Mailcow + Cloudflare)
    'vmo_quicklaunch_from_email',
    'vmo_quicklaunch_reply_to_email',
    'vmo_vultr_api_key',
    'vmo_vultr_quicklaunch_app_id',
    'vmo_vultr_quicklaunch_image_id',
    'vmo_vultr_quicklaunch_snapshot_id',
    'vmo_vultr_quicklaunch_iso_id',
    'vmo_vultr_quicklaunch_os_id',
    'vmo_quicklaunch_email_provider',
    'vmo_mailcow_api_base',
    'vmo_mailcow_api_key',
    'vmo_mailcow_mx_host_template',
    'vmo_cloudflare_api_token',
    'vmo_namecheap_api_user',
    'vmo_namecheap_username',
    'vmo_namecheap_api_key',
    'vmo_namecheap_client_ip',
    'vmo_namecheap_use_sandbox',
    'vmo_namecheap_default_years',
    'vmo_namecheap_enable_whoisguard',
    'vmo_namecheap_contact_first_name',
    'vmo_namecheap_contact_last_name',
    'vmo_namecheap_contact_org',
    'vmo_namecheap_contact_job_title',
    'vmo_namecheap_contact_address1',
    'vmo_namecheap_contact_address2',
    'vmo_namecheap_contact_city',
    'vmo_namecheap_contact_state',
    'vmo_namecheap_contact_postal_code',
    'vmo_namecheap_contact_country',
    'vmo_namecheap_contact_phone',
    'vmo_namecheap_contact_email',
    'vmo_stripe_webhook_secret'
    ] as $key) {
        if ($key === 'vmo_openai_project_id') {
            register_setting('vmo_settings_group', $key, array(
                'sanitize_callback' => 'vmo_sanitize_openai_realtime_project_id',
            ));
            continue;
        }
        if ($key === 'vmo_phone_bridge_url') {
            register_setting('vmo_settings_group', $key, array(
                'sanitize_callback' => $vmo_bridge_url_sanitizer,
            ));
            continue;
        }
        if (in_array($key, array('vmo_media_secret', 'vmo_listen_secret'), true)) {
            register_setting('vmo_settings_group', $key, array(
                'sanitize_callback' => 'sanitize_text_field',
            ));
            continue;
        }
        if ($key === 'vmo_enable_legacy_realtime_sip') {
            register_setting('vmo_settings_group', $key, array(
                'sanitize_callback' => function ($value) {
                    return '0';
                },
            ));
            continue;
        }
        register_setting('vmo_settings_group', $key);
    }
});

// === Frontend: Enqueue buy button handler when payments enabled ===
add_action('wp_enqueue_scripts', function() {
    if (!defined('VMO_PAYMENTS_ENABLED') || !VMO_PAYMENTS_ENABLED) return;
    $file = VMO_AI_BUILDER_DIR . 'assets/vmo-payments.js';
    if (file_exists($file)) {
        wp_enqueue_script('vmo-payments', VMO_AI_BUILDER_URL . 'assets/vmo-payments.js', ['jquery'], filemtime($file), true);
        wp_localize_script('vmo-payments', 'vmo_ajax', [
            'payments_enabled' => (bool) VMO_PAYMENTS_ENABLED,
            'pluginUrl' => plugin_dir_url(__FILE__),
            'root' => esc_url_raw(rest_url())
        ]);
    }
});

// Shortcode: [vmo_buy product_id="123" label="Buy Now"]
// Legacy still supported: [vmo_buy amount="1999" user="123" name="Product" desc="Description" currency="usd" label="Buy Now"]
add_shortcode('vmo_buy', function($atts) {
    $a = shortcode_atts([
        'product_id' => '',
        'amount' => '',
        'user' => '',
        'name' => 'Product',
        'desc' => '',
        'currency' => 'usd',
        'label' => 'Buy Now'
    ], $atts, 'vmo_buy');
    // Preferred: product_id
    if (!empty($a['product_id'])) {
        $attrs = sprintf('data-vmo-buy data-product-id="%d"', intval($a['product_id']));
        return '<button class="button vmo-buy-btn" '.$attrs.'>'.esc_html($a['label']).'</button>';
    }
    // Legacy fallback
    if (empty($a['amount']) || empty($a['user'])) return '';
    $attrs = sprintf('data-vmo-buy data-merchant="%d" data-amount="%d" data-name="%s" data-desc="%s" data-currency="%s"',
        intval($a['user']), intval($a['amount']), esc_attr($a['name']), esc_attr($a['desc']), esc_attr($a['currency'])
    );
    return '<button class="button vmo-buy-btn" '.$attrs.'>'.esc_html($a['label']).'</button>';
});

// AJAX: phone status for dashboard
add_action('wp_ajax_vmo_get_phone_status', function() {
    if (!current_user_can('read')) {
        wp_send_json_error(array('message' => 'Forbidden'), 403);
    }
    if (!function_exists('vmo_verify_admin_security') || !vmo_verify_admin_security()) {
        wp_send_json_error(array('message' => 'Security check failed'), 403);
    }

    // Ensure Twilio Manager is available
    if (!class_exists('VMO_Twilio_Manager')) {
        $path = plugin_dir_path(__FILE__) . 'includes/class-vmo-twilio-manager.php';
        if (file_exists($path)) require_once $path;
    }

    $user_id = get_current_user_id();
    $phone_number = '';
    if (class_exists('VMO_Twilio_Manager')) {
        try {
            $manager = VMO_Twilio_Manager::get_instance();
            $phone_number = (string) ($manager->get_user_phone($user_id) ?: '');

            // Auto-sync webhook URLs (throttled to once per 12 hours).
            // Fixes inbound "application error" caused by stale webhook URLs
            // after code updates, option changes, or domain migrations.
            $manager->sync_phone_webhooks($user_id);
        } catch (Throwable $e) {
            if (defined('WP_DEBUG') && WP_DEBUG) error_log('[VMO Phone Status] Error: ' . $e->getMessage());
            $phone_number = (string) (get_user_meta($user_id, 'vmo_phone_number', true) ?: '');
        }
    } else {
        $phone_number = (string) (get_user_meta($user_id, 'vmo_phone_number', true) ?: '');
    }

    wp_send_json_success(array(
        'phone_number' => $phone_number,
        'ai_greeting' => (string) (get_user_meta($user_id, 'vmo_ai_greeting', true) ?: ''),
        'external_phone' => (string) (get_user_meta($user_id, 'vmo_external_phone', true) ?: ''),
        'call_recording_enabled' => (int) get_user_meta($user_id, 'call_recording_enabled', true),
        'ring_url' => (string) (get_user_meta($user_id, 'vmo_ring_url', true) ?: ''),
        'ring_attachment_id' => (int) get_user_meta($user_id, 'vmo_ring_attachment_id', true),
        'ring_enabled' => (int) (get_user_meta($user_id, 'vmo_ring_enabled', true) === '' ? 1 : get_user_meta($user_id, 'vmo_ring_enabled', true)),
        'ring_volume' => (float) (get_user_meta($user_id, 'vmo_ring_volume', true) === '' ? 0.6 : get_user_meta($user_id, 'vmo_ring_volume', true)),
        'ring_preset' => (string) (get_user_meta($user_id, 'vmo_ring_preset', true) ?: 'classic'),
    ));
});

// AJAX: Legacy SIP setup endpoint retained as a blocked compatibility stub.
// Premium now uses the WebSocket bridge; this action must not create trunks or
// reroute numbers away from the normal Voice URL webhook.
add_action('wp_ajax_vmo_setup_realtime_sip_trunk', function() {
    if (!current_user_can('manage_options')) {
        wp_send_json_error(array('message' => 'Admin only'), 403);
    }
    if (!function_exists('vmo_verify_admin_security') || !vmo_verify_admin_security()) {
        wp_send_json_error(array('message' => 'Security check failed'), 403);
    }
    if (!class_exists('VMO_Twilio_Manager')) {
        wp_send_json_error(array('message' => 'Twilio Manager not loaded'), 500);
    }
    update_option('vmo_enable_legacy_realtime_sip', '0', false);
    wp_send_json_error(array(
        'message' => 'Legacy SIP routing has been removed from the active phone setup. Premium calls use the VMO WebSocket bridge, and Standard calls use the Voice URL webhook.',
    ), 400);
});

// AJAX: EMERGENCY ROLLBACK — remove every number from the realtime trunk so
// they revert to using their VoiceUrl webhook (legacy pipeline). Use when
// OpenAI is rejecting SIP INVITEs and inbound calls fail with carrier
// "cannot complete" errors.
add_action('wp_ajax_vmo_remove_realtime_sip_trunk', function() {
    if (!current_user_can('manage_options')) {
        wp_send_json_error(array('message' => 'Admin only'), 403);
    }
    if (!function_exists('vmo_verify_admin_security') || !vmo_verify_admin_security()) {
        wp_send_json_error(array('message' => 'Security check failed'), 403);
    }
    if (!class_exists('VMO_Twilio_Manager')) {
        wp_send_json_error(array('message' => 'Twilio Manager not loaded'), 500);
    }
    try {
        $mgr  = VMO_Twilio_Manager::get_instance();
        $stats = $mgr->remove_all_numbers_from_realtime_trunk();
        wp_send_json_success(array(
            'stats'   => $stats,
            'message' => sprintf(
                'Removed %d / %d number(s) from the legacy SIP trunk. Inbound calls now route through the VMO VoiceUrl webhook, which is required for the WebSocket bridge path.',
                $stats['removed'], $stats['total']
            ),
        ));
    } catch (Throwable $e) {
        wp_send_json_error(array('message' => $e->getMessage()), 500);
    }
});

if (!function_exists('vmo_phone_bridge_dns_status')) {
    function vmo_phone_bridge_dns_status($bridge_url) {
        $bridge_url = trim((string) $bridge_url);
        $host = '';
        if ($bridge_url !== '') {
            $parsed = wp_parse_url($bridge_url);
            if (is_array($parsed) && !empty($parsed['host'])) {
                $host = strtolower(trim((string) $parsed['host']));
            }
        }

        if ($host === '') {
            return array(
                'ok' => false,
                'host' => '',
                'addresses' => array(),
                'message' => 'Bridge URL is missing a host',
            );
        }

        if (filter_var($host, FILTER_VALIDATE_IP)) {
            return array(
                'ok' => true,
                'host' => $host,
                'addresses' => array($host),
                'message' => 'Bridge URL uses an IP literal',
            );
        }

        $cache_key = 'vmo_bridge_dns_' . md5($host);
        $cached = get_transient($cache_key);
        if (is_array($cached) && array_key_exists('ok', $cached)) {
            return $cached;
        }

        $addresses = array();
        if (function_exists('dns_get_record')) {
            $records = @dns_get_record($host, DNS_A + DNS_AAAA);
            if (is_array($records)) {
                foreach ($records as $record) {
                    if (!empty($record['ip'])) {
                        $addresses[] = (string) $record['ip'];
                    }
                    if (!empty($record['ipv6'])) {
                        $addresses[] = (string) $record['ipv6'];
                    }
                }
            }
        }
        if (empty($addresses) && function_exists('gethostbynamel')) {
            $resolved = @gethostbynamel($host);
            if (is_array($resolved)) {
                foreach ($resolved as $address) {
                    if (is_string($address) && $address !== '') {
                        $addresses[] = $address;
                    }
                }
            }
        }

        $addresses = array_values(array_unique(array_filter($addresses)));
        $ok = !empty($addresses);
        $status = array(
            'ok' => $ok,
            'host' => $host,
            'addresses' => $addresses,
            'message' => $ok ? ('Resolves to ' . implode(', ', array_slice($addresses, 0, 3))) : 'DNS lookup failed',
            'checked_at' => time(),
        );
        set_transient($cache_key, $status, $ok ? 300 : 60);

        return $status;
    }
}

if (!function_exists('vmo_client_bridge_signer_status')) {
    function vmo_client_bridge_signer_status() {
        $client_mode = (defined('VMO_CLIENT_MODE') && VMO_CLIENT_MODE === true);
        $proxy_base = defined('VMO_MASTER_PROXY_URL')
            ? (string) VMO_MASTER_PROXY_URL
            : (string) get_option('vmo_master_proxy_url', '');
        $proxy_base = trim($proxy_base);
        if ($proxy_base === '' && $client_mode) {
            $proxy_base = 'https://getvmo.com/wp-json/vmo-builder/v1/proxy';
        }

        $sign_url = '';
    $proxy_placeholder = '__VMO_' . 'MASTER_PROXY_URL__';
    if ($proxy_base !== '' && strpos($proxy_base, $proxy_placeholder) === false) {
            $needle = '/wp-json/vmo-builder/v1/proxy';
            $pos = strpos($proxy_base, $needle);
            if ($pos !== false) {
                $sign_url = substr($proxy_base, 0, $pos) . '/wp-json/vmo-builder/v1/bridge/sign';
            } else {
                $sign_url = preg_replace('#/proxy/?$#', '/bridge/sign', $proxy_base);
            }
            $sign_url = esc_url_raw((string) $sign_url);
        }

        $license_present = trim((string) get_option('vmo_license_key', '')) !== '';
        $installation_present = trim((string) get_option('vmo_installation_id', '')) !== '';
        $sign_url_valid = $sign_url !== '' && filter_var($sign_url, FILTER_VALIDATE_URL);
        $ready = $client_mode && $sign_url_valid && $license_present && $installation_present;

        if (!$client_mode) {
            $message = 'not_applicable — master runtime signs locally';
        } elseif ($ready) {
            $message = 'ok — client will ask the Kinsta master bridge signer';
        } elseif (!$sign_url_valid) {
            $message = 'missing — VMO_MASTER_PROXY_URL / vmo_master_proxy_url does not produce a valid bridge signer URL';
        } elseif (!$license_present || !$installation_present) {
            $message = 'missing — client license key or installation id is not configured';
        } else {
            $message = 'missing — client bridge signer is not ready';
        }

        return array(
            'client_mode' => $client_mode,
            'ready' => $ready,
            'message' => $message,
            'sign_url' => $sign_url_valid ? $sign_url : '',
            'license_present' => $license_present,
            'installation_present' => $installation_present,
        );
    }
}

if (!function_exists('vmo_twilio_call_resource_summary')) {
    function vmo_twilio_call_resource_summary($account_sid, $auth_token, $call_sid) {
        $account_sid = trim((string) $account_sid);
        $auth_token = trim((string) $auth_token);
        $call_sid = trim((string) $call_sid);

        if ($account_sid === '' || $auth_token === '' || $call_sid === '') {
            return array(
                'ok' => false,
                'message' => 'skipped — missing Twilio credentials or CallSid',
            );
        }

        $response = wp_remote_get(
            'https://api.twilio.com/2010-04-01/Accounts/' . rawurlencode($account_sid) . '/Calls/' . rawurlencode($call_sid) . '.json',
            array(
                'timeout' => 10,
                'headers' => array('Authorization' => 'Basic ' . base64_encode($account_sid . ':' . $auth_token)),
            )
        );

        if (is_wp_error($response)) {
            return array(
                'ok' => false,
                'message' => 'Twilio lookup network error — ' . $response->get_error_message(),
            );
        }

        $code = (int) wp_remote_retrieve_response_code($response);
        $body_text = (string) wp_remote_retrieve_body($response);
        $body = json_decode($body_text, true);
        if ($code !== 200 || !is_array($body)) {
            $api_message = is_array($body) && !empty($body['message']) ? (string) $body['message'] : substr($body_text, 0, 180);
            return array(
                'ok' => false,
                'message' => 'Twilio lookup failed — HTTP ' . $code . ($api_message !== '' ? ' ' . $api_message : ''),
            );
        }

        $summary_parts = array();
        foreach (array('status', 'duration', 'start_time', 'end_time', 'direction', 'answered_by', 'error_code', 'error_message') as $field) {
            if (array_key_exists($field, $body) && $body[$field] !== null && $body[$field] !== '') {
                $summary_parts[] = $field . '=' . (string) $body[$field];
            }
        }
        if (empty($summary_parts)) {
            $summary_parts[] = 'Twilio returned call resource with no status fields';
        }

        return array(
            'ok' => true,
            'message' => implode(' ', $summary_parts),
            'body' => $body,
        );
    }
}

// AJAX: Live "Listen Mode" diagnostics — runs every check end-to-end so the
// user can press one button on the dashboard and see exactly where the chain
// is broken. Mirrors the QuickLaunch API connection tester pattern.
add_action('wp_ajax_vmo_phone_listen_test', function() {
    if (!current_user_can('read')) {
        wp_send_json_error(array('message' => 'Forbidden'), 403);
    }
    if (!function_exists('vmo_verify_admin_security') || !vmo_verify_admin_security()) {
        wp_send_json_error(array('message' => 'Security check failed'), 403);
    }

    $user_id = get_current_user_id();
    $services = array();
    $details  = array();
    $legacy_sip_enabled = false;

    // ── 1. Twilio credentials present ──
    $sid   = defined('VMO_TWILIO_ACCOUNT_SID') ? VMO_TWILIO_ACCOUNT_SID : (string) get_option('vmo_twilio_account_sid', '');
    $token = defined('VMO_TWILIO_AUTH_TOKEN') ? VMO_TWILIO_AUTH_TOKEN : (string) get_option('vmo_twilio_auth_token', '');
    $services['twilio_credentials'] = array(
        'label'   => 'Twilio Credentials',
        'valid'   => (!empty($sid) && !empty($token)),
        'message' => (!empty($sid) && !empty($token)) ? 'Account SID + Auth Token present' : 'Missing — set in VMO Twilio Settings',
    );

    // ── 2. Twilio API auth — actually call Twilio /Accounts/{sid}.json ──
    if ($services['twilio_credentials']['valid']) {
        $resp = wp_remote_get(
            "https://api.twilio.com/2010-04-01/Accounts/{$sid}.json",
            array(
                'timeout' => 10,
                'headers' => array('Authorization' => 'Basic ' . base64_encode($sid . ':' . $token)),
            )
        );
        if (is_wp_error($resp)) {
            $services['twilio_api_auth'] = array('label' => 'Twilio API Auth', 'valid' => false, 'message' => 'Network error: ' . $resp->get_error_message());
        } else {
            $code = wp_remote_retrieve_response_code($resp);
            $services['twilio_api_auth'] = array(
                'label'   => 'Twilio API Auth',
                'valid'   => ($code === 200),
                'message' => ($code === 200) ? 'Authenticated' : ('HTTP ' . $code . ' — credentials rejected'),
            );
        }
    } else {
        $services['twilio_api_auth'] = array('label' => 'Twilio API Auth', 'valid' => false, 'message' => 'Skipped (no credentials)');
    }

    // ── 3. Phone number assigned to this user ──
    $phone_number = '';
    $twilio_phone_sid = '';
    if (class_exists('VMO_Twilio_Manager')) {
        try {
            $mgr = VMO_Twilio_Manager::get_instance();
            $phone_number = (string) ($mgr->get_user_phone($user_id) ?: '');
        } catch (Throwable $e) {}
    }
    global $wpdb;
    $row = $wpdb->get_row($wpdb->prepare(
        "SELECT * FROM {$wpdb->prefix}vmo_phone_numbers WHERE user_id = %d AND status IN ('active','pending_port') ORDER BY created_at DESC LIMIT 1",
        $user_id
    ));
    if ($row) {
        $twilio_phone_sid = isset($row->twilio_sid) ? (string) $row->twilio_sid : '';
        if (empty($phone_number) && isset($row->phone_number)) $phone_number = (string) $row->phone_number;
    }
    $services['phone_assigned'] = array(
        'label'   => 'Phone Number Assigned',
        'valid'   => !empty($phone_number),
        'message' => !empty($phone_number) ? $phone_number : 'No phone number found for this user',
    );

    $voice_quality_tier = strtolower(trim((string) get_user_meta($user_id, 'vmo_voice_quality_tier', true)));
    $voice_quality_tier = ($voice_quality_tier === 'premium') ? 'premium' : 'standard';
    $services['premium_voice_tier'] = array(
        'label'   => 'Premium Voice Mode',
        'valid'   => ($voice_quality_tier === 'premium'),
        'message' => ($voice_quality_tier === 'premium')
            ? 'Premium mode is selected in ringer settings; inbound calls can use the VMO live audio bridge.'
            : 'Standard mode is selected in ringer settings. Save Phone Settings with Premium selected to route inbound calls to the VMO live audio bridge.',
    );
    $details['voice_quality_tier'] = $voice_quality_tier;

    // ── 4. Twilio number's Voice URL matches our webhook URL ──
    $expected_voice_url = '';
    if (class_exists('VMO_Twilio_Manager')) {
        try {
            $mgr = VMO_Twilio_Manager::get_instance();
            $expected_voice_url = $mgr->phone_gateway_url('voice', array('uid' => $user_id));
        } catch (Throwable $e) {}
    }

    // Fallback: if DB row has no twilio_sid, look it up via Twilio API by phone number
    if (empty($twilio_phone_sid) && !empty($phone_number) && $services['twilio_api_auth']['valid']) {
        $lookup = wp_remote_get(
            "https://api.twilio.com/2010-04-01/Accounts/{$sid}/IncomingPhoneNumbers.json?PhoneNumber=" . rawurlencode($phone_number),
            array(
                'timeout' => 10,
                'headers' => array('Authorization' => 'Basic ' . base64_encode($sid . ':' . $token)),
            )
        );
        if (!is_wp_error($lookup)) {
            $lookup_body = json_decode(wp_remote_retrieve_body($lookup), true);
            if (!empty($lookup_body['incoming_phone_numbers'][0]['sid'])) {
                $twilio_phone_sid = (string) $lookup_body['incoming_phone_numbers'][0]['sid'];
                $details['twilio_phone_sid_source'] = 'looked up via Twilio API (DB row was missing it)';
                // Heal the DB so future runs don't need the lookup
                if ($row && isset($row->id)) {
                    $wpdb->update(
                        $wpdb->prefix . 'vmo_phone_numbers',
                        array('twilio_sid' => $twilio_phone_sid),
                        array('id' => $row->id)
                    );
                }
            }
        }
    }

    if (!empty($twilio_phone_sid) && $services['twilio_api_auth']['valid']) {
        $resp = wp_remote_get(
            "https://api.twilio.com/2010-04-01/Accounts/{$sid}/IncomingPhoneNumbers/{$twilio_phone_sid}.json",
            array(
                'timeout' => 10,
                'headers' => array('Authorization' => 'Basic ' . base64_encode($sid . ':' . $token)),
            )
        );
        if (is_wp_error($resp)) {
            $services['twilio_voice_url'] = array('label' => 'Twilio Number Voice URL', 'valid' => false, 'message' => 'Could not fetch from Twilio: ' . $resp->get_error_message());
        } else {
            $body_arr = json_decode(wp_remote_retrieve_body($resp), true);
            $live_voice_url = isset($body_arr['voice_url']) ? (string) $body_arr['voice_url'] : '';

            // Tolerant comparison: the `k=` token authenticates the request, and `uid` is
            // optional metadata. Two URLs match if they have the same scheme+host+path AND
            // the same `k` token AND the same `kind`/`vmo_gw`/`action` core params.
            $matches = false;
            if ($live_voice_url !== '' && $expected_voice_url !== '') {
                $live_p = wp_parse_url($live_voice_url);
                $exp_p  = wp_parse_url($expected_voice_url);
                parse_str($live_p['query'] ?? '', $live_q);
                parse_str($exp_p['query']  ?? '', $exp_q);
                $core_keys = array('action', 'vmo_gw', 'kind', 'k');
                $core_match = true;
                foreach ($core_keys as $ck) {
                    if (($live_q[$ck] ?? '') !== ($exp_q[$ck] ?? '')) { $core_match = false; break; }
                }
                $host_match = (
                    ($live_p['host'] ?? '') === ($exp_p['host'] ?? '') &&
                    ($live_p['path'] ?? '') === ($exp_p['path'] ?? '')
                );
                $matches = $core_match && $host_match;
            }
            $services['twilio_voice_url'] = array(
                'label'   => 'Twilio Voice Webhook URL',
                'valid'   => $matches,
                'message' => $matches ? 'Matches our endpoint (token verified)' : ('MISMATCH — Twilio has: ' . ($live_voice_url ?: '(empty)')),
            );
            $details['twilio_voice_url_live']     = $live_voice_url;
            $details['twilio_voice_url_expected'] = $expected_voice_url;
        }
    } else {
        $services['twilio_voice_url'] = array('label' => 'Twilio Voice Webhook URL', 'valid' => false, 'message' => 'Skipped (no phone SID or auth)');
    }

    // ── 5. Premium WebSocket bridge config ──
    $bridge_url = '';
    $bridge_candidates = array(
        defined('VMO_PHONE_BRIDGE_URL') ? (string) VMO_PHONE_BRIDGE_URL : '',
        (string) get_option('vmo_phone_bridge_url', ''),
        getenv('VMO_PHONE_BRIDGE_URL') ?: '',
        'wss://bridge.getvmo.com',
    );
    foreach ($bridge_candidates as $candidate) {
        $candidate = trim((string) $candidate);
        if ($candidate !== '') {
            $bridge_url = rtrim($candidate, '/');
            break;
        }
    }
    $bridge_url_valid = (bool) preg_match('#^(wss?|https?)://#i', $bridge_url);
    $services['premium_bridge_url'] = array(
        'label'   => 'Premium Bridge URL',
        'valid'   => $bridge_url_valid,
        'message' => $bridge_url_valid ? ($bridge_url . ' (/media + /listen)') : 'Missing or invalid — set Bridge WebSocket URL in VMO Settings',
    );
    $bridge_dns_status = vmo_phone_bridge_dns_status($bridge_url_valid ? $bridge_url : '');
    $bridge_dns_ok = $bridge_url_valid && !empty($bridge_dns_status['ok']);
    $services['premium_bridge_dns'] = array(
        'label'   => 'Premium Bridge DNS',
        'valid'   => $bridge_dns_ok,
        'message' => $bridge_dns_ok
            ? ((string) ($bridge_dns_status['host'] ?? '') . ' — ' . (string) ($bridge_dns_status['message'] ?? 'resolves'))
            : ((string) ($bridge_dns_status['host'] ?? $bridge_url) . ' — DNS lookup failed. Add/fix the public DNS record before using Premium mode.'),
    );
    $details['premium_bridge_dns'] = $bridge_dns_status;
    $client_bridge_signer = vmo_client_bridge_signer_status();
    $client_mode = !empty($client_bridge_signer['client_mode']);
    $client_bridge_signer_ready = !empty($client_bridge_signer['ready']);
    $services['premium_bridge_signer'] = array(
        'label'   => 'Premium Bridge Signer',
        'valid'   => !$client_mode || $client_bridge_signer_ready,
        'message' => (string) ($client_bridge_signer['message'] ?? ''),
    );
    $details['premium_bridge_signer'] = array(
        'client_mode' => $client_mode,
        'ready' => $client_bridge_signer_ready,
        'sign_url' => (string) ($client_bridge_signer['sign_url'] ?? ''),
        'license_present' => !empty($client_bridge_signer['license_present']),
        'installation_present' => !empty($client_bridge_signer['installation_present']),
    );

    $media_secret = '';
    foreach (array(
        defined('VMO_MEDIA_SECRET') ? (string) VMO_MEDIA_SECRET : '',
        (string) get_option('vmo_media_secret', ''),
        getenv('VMO_MEDIA_SECRET') ?: '',
        defined('VMO_BRIDGE_MEDIA_SECRET') ? (string) VMO_BRIDGE_MEDIA_SECRET : '',
        (string) get_option('vmo_bridge_media_secret', ''),
        getenv('VMO_BRIDGE_MEDIA_SECRET') ?: '',
        defined('VMO_LISTEN_SECRET') ? (string) VMO_LISTEN_SECRET : '',
        (string) get_option('vmo_listen_secret', ''),
        getenv('VMO_LISTEN_SECRET') ?: '',
    ) as $candidate) {
        $candidate = trim((string) $candidate);
        if ($candidate !== '') {
            $media_secret = $candidate;
            break;
        }
    }
    $services['premium_bridge_media_secret'] = array(
        'label'   => 'Bridge Media Secret',
        'valid'   => $client_mode ? $client_bridge_signer_ready : ($media_secret !== ''),
        'message' => $client_mode
            ? ($client_bridge_signer_ready ? 'Client mode — local secret intentionally absent; master signer is ready' : 'Client mode — master bridge signer is not ready')
            : (($media_secret !== '') ? ('Set (' . strlen($media_secret) . ' chars)') : 'Missing — must match VMO_MEDIA_SECRET on the bridge server'),
    );

    $listen_secret = '';
    foreach (array(
        defined('VMO_LISTEN_SECRET') ? (string) VMO_LISTEN_SECRET : '',
        (string) get_option('vmo_listen_secret', ''),
        getenv('VMO_LISTEN_SECRET') ?: '',
        defined('VMO_BRIDGE_LISTEN_SECRET') ? (string) VMO_BRIDGE_LISTEN_SECRET : '',
        (string) get_option('vmo_bridge_listen_secret', ''),
        getenv('VMO_BRIDGE_LISTEN_SECRET') ?: '',
    ) as $candidate) {
        $candidate = trim((string) $candidate);
        if ($candidate !== '') {
            $listen_secret = $candidate;
            break;
        }
    }
    $services['premium_bridge_listen_secret'] = array(
        'label'   => 'Bridge Listen Secret',
        'valid'   => $client_mode ? $client_bridge_signer_ready : ($listen_secret !== ''),
        'message' => $client_mode
            ? ($client_bridge_signer_ready ? 'Client mode — local secret intentionally absent; master signer returns signed listen URLs' : 'Client mode — master bridge signer is not ready')
            : (($listen_secret !== '') ? ('Set (' . strlen($listen_secret) . ' chars)') : 'Missing — must match VMO_LISTEN_SECRET on the bridge server'),
    );
    $services['phone_runtime_mode'] = array(
        'label'   => 'Phone Runtime Mode',
        'valid'   => true,
        'message' => 'WebSocket bridge mode. Legacy OpenAI SIP routing is removed and cannot auto-configure trunks.',
    );
    $details['premium_bridge_media_url'] = rtrim(preg_replace('/^https:/i', 'wss:', preg_replace('/^http:/i', 'ws:', $bridge_url)), '/') . '/media';
    $details['premium_bridge_listen_url'] = rtrim(preg_replace('/^https:/i', 'wss:', preg_replace('/^http:/i', 'ws:', $bridge_url)), '/') . '/listen';

    $latest_premium_bridge_verified = false;
    $latest_call = null;
    $calls_table = $wpdb->prefix . 'vmo_phone_calls';
    if (function_exists('vmo_table_exists') && vmo_table_exists($calls_table)) {
        $latest_call = $wpdb->get_row($wpdb->prepare(
            "SELECT call_sid, topic, status, created_at FROM {$calls_table} WHERE user_id = %d AND direction = 'inbound' ORDER BY id DESC LIMIT 1",
            $user_id
        ));
    }
    if ($latest_call && !empty($latest_call->call_sid)) {
        $latest_trace = get_transient('vmo_voice_trace_' . sanitize_key((string) $latest_call->call_sid));
        $trace_text = is_array($latest_trace) ? implode(' | ', array_slice($latest_trace, -8)) : '';
        $topic_hit = ((string) ($latest_call->topic ?? '') === 'premium_realtime_bridge');
        $trace_hit = (strpos($trace_text, 'premium_bridge_twiml_emitted_exit') !== false);
        $latest_premium_bridge_verified = ($topic_hit || $trace_hit);
        $services['latest_premium_bridge_call'] = array(
            'label'   => 'Latest Inbound Premium Bridge Hit',
            'valid'   => $latest_premium_bridge_verified,
            'message' => $latest_premium_bridge_verified
                ? ('Latest inbound call ' . (string) $latest_call->call_sid . ' emitted Premium bridge TwiML.')
                : ('Latest inbound call ' . (string) $latest_call->call_sid . ' did not emit Premium bridge TwiML; topic=' . ((string) ($latest_call->topic ?? '') ?: '(empty)')),
        );
        $details['latest_inbound_call_sid'] = (string) $latest_call->call_sid;
        $details['latest_inbound_call_topic'] = (string) ($latest_call->topic ?? '');
        $details['latest_premium_bridge_trace'] = $trace_text !== '' ? $trace_text : '(no transient trace found)';
        $latest_twilio_call = vmo_twilio_call_resource_summary($sid, $token, (string) $latest_call->call_sid);
        $details['latest_inbound_twilio_status'] = (string) ($latest_twilio_call['message'] ?? 'Twilio lookup unavailable');
        $latest_premium_bridge_stream = get_transient('vmo_premium_bridge_last_stream_' . $user_id);
        $details['latest_premium_bridge_stream'] = is_array($latest_premium_bridge_stream) && !empty($latest_premium_bridge_stream['message'])
            ? ((string) $latest_premium_bridge_stream['message'] . ' (' . human_time_diff((int) ($latest_premium_bridge_stream['time'] ?? time())) . ' ago)')
            : 'none recorded yet';
        $latest_premium_bridge_error = get_transient('vmo_premium_bridge_last_error_' . $user_id);
        $details['latest_premium_bridge_error'] = is_array($latest_premium_bridge_error) && !empty($latest_premium_bridge_error['message'])
            ? ((string) $latest_premium_bridge_error['message'] . ' (' . human_time_diff((int) ($latest_premium_bridge_error['time'] ?? time())) . ' ago)')
            : 'none recorded yet';
    } else {
        $services['latest_premium_bridge_call'] = array(
            'label'   => 'Latest Inbound Premium Bridge Hit',
            'valid'   => false,
            'message' => 'No inbound call record found yet. Place a fresh inbound test call after saving Premium mode.',
        );
    }

    if ($legacy_sip_enabled) {

    // ── 6. OpenAI API key valid (legacy SIP only) ──
    $oai_key = (string) (get_option('vmo_openai_api_key', '') ?: get_option('vmo_manager_openai_api_key', ''));
    if (!empty($oai_key)) {
        $resp = wp_remote_get('https://api.openai.com/v1/models?limit=1', array(
            'timeout' => 10,
            'headers' => array('Authorization' => 'Bearer ' . $oai_key),
        ));
        if (is_wp_error($resp)) {
            $services['openai_api_key'] = array('label' => 'OpenAI API Key', 'valid' => false, 'message' => 'Network error: ' . $resp->get_error_message());
        } else {
            $code = wp_remote_retrieve_response_code($resp);
            $services['openai_api_key'] = array(
                'label'   => 'OpenAI API Key',
                'valid'   => ($code === 200),
                'message' => ($code === 200) ? 'Authenticated' : ('HTTP ' . $code . ' — ' . substr(wp_remote_retrieve_body($resp), 0, 200)),
            );
        }
    } else {
        $services['openai_api_key'] = array('label' => 'OpenAI API Key', 'valid' => false, 'message' => 'Missing — set in VMO Settings');
    }

    // ── 6. OpenAI Realtime project ID present ──
    $rt_project = (string) get_option('vmo_openai_project_id', '');
    if ($rt_project === '' && defined('VMO_OPENAI_PROJECT_ID')) $rt_project = (string) VMO_OPENAI_PROJECT_ID;
    $rt_project_valid = vmo_is_valid_openai_realtime_project_id($rt_project);
    $services['openai_project_id'] = array(
        'label'   => 'OpenAI Realtime Project ID',
        'valid'   => $rt_project_valid,
        'message' => empty($rt_project)
            ? 'Missing — required for Live Audio Listen'
            : ($rt_project_valid
                ? ('Set: ' . substr($rt_project, 0, 12) . '…')
                : ('Invalid format: ' . substr($rt_project, 0, 24) . '… — must be the exact proj_... value from OpenAI Project > General.')),
    );
    $expected_openai_sip = $rt_project_valid ? ('sip:' . $rt_project . '@sip.api.openai.com;transport=tls') : '';

    $project_access = vmo_check_openai_project_api_access($oai_key, $rt_project);
    $services['openai_project_access'] = array(
        'label'   => 'OpenAI API Key -> Project Access',
        'valid'   => !empty($project_access['valid']),
        'message' => (string) ($project_access['message'] ?? 'Unknown project access state'),
    );
    if (!empty($project_access['openai_request_id'])) {
        $details['openai_project_access_request_id'] = $project_access['openai_request_id'];
    }
    if (!empty($project_access['client_request_id'])) {
        $details['openai_project_access_client_request_id'] = $project_access['client_request_id'];
    }
    if (!empty($project_access['response_body'])) {
        $details['openai_project_access_response'] = $project_access['response_body'];
    }

    // ── 7. OpenAI webhook secret present ──
    $rt_secret = (string) get_option('vmo_openai_webhook_secret', '');
    if ($rt_secret === '' && defined('VMO_OPENAI_WEBHOOK_SECRET')) $rt_secret = (string) VMO_OPENAI_WEBHOOK_SECRET;
    $services['openai_webhook_secret'] = array(
        'label'   => 'OpenAI Webhook Signing Secret',
        'valid'   => !empty($rt_secret),
        'message' => !empty($rt_secret) ? ('Set (' . strlen($rt_secret) . ' chars)') : 'Missing — required to verify OpenAI webhooks',
    );

    // ── 8. Our OpenAI webhook endpoint is publicly reachable ──
    $our_oa_webhook = '';
    if (class_exists('VMO_Twilio_Manager')) {
        try {
            $our_oa_webhook = VMO_Twilio_Manager::get_instance()->openai_realtime_webhook_url();
        } catch (Throwable $e) {
            $our_oa_webhook = '';
        }
    }
    if ($our_oa_webhook === '') {
        $our_oa_webhook = rest_url('vmo/v1/openai-realtime-webhook');
    }
    $resp = wp_remote_post($our_oa_webhook, array(
        'timeout' => 10,
        'headers' => array('Content-Type' => 'application/json'),
        'body' => '{}',
    ));
    if (is_wp_error($resp)) {
        $err_msg = $resp->get_error_message();
        $loopback_timeout = (stripos($err_msg, 'cURL error 28') !== false || stripos($err_msg, 'Operation timed out') !== false);
        if ($loopback_timeout) {
            $services['webhook_reachable'] = array(
                'label'   => 'OpenAI Webhook Endpoint Reachable',
                'valid'   => true,
                'message' => 'This host timed out while self-fetching its own public webhook URL. That loopback failure does not prove the public endpoint is down.',
            );
            $details['openai_webhook_loopback_warning'] = $err_msg;
        } else {
            $services['webhook_reachable'] = array(
                'label'   => 'OpenAI Webhook Endpoint Reachable',
                'valid'   => false,
                'message' => 'Self-fetch failed: ' . $err_msg,
            );
        }
    } else {
        $code = wp_remote_retrieve_response_code($resp);
        // The realtime webhook rejects unsigned probe requests with 400.
        // That still proves the endpoint is publicly reachable.
        $reachable = in_array($code, array(200, 400, 401, 403, 405), true);
        $services['webhook_reachable'] = array(
            'label'   => 'OpenAI Webhook Endpoint Reachable',
            'valid'   => $reachable,
            'message' => $reachable ? ('Endpoint exists at ' . $our_oa_webhook) : ('HTTP ' . $code . ' — route may not be registered'),
        );
    }
    $details['openai_webhook_url'] = $our_oa_webhook;

    // ── 9. Did OpenAI actually call our webhook recently? ──
    $last_verified_hit = get_transient('vmo_openai_webhook_lastverified');
    $last_hit = get_transient('vmo_openai_webhook_lasthit');
    if (is_array($last_verified_hit) && !empty($last_verified_hit['time'])) {
        $age = time() - intval($last_verified_hit['time']);
        $event_type = (string) ($last_verified_hit['type'] ?? 'unknown');
        $services['openai_webhook_last_hit'] = array(
            'label'   => 'OpenAI Webhook Last Hit',
            'valid'   => ($age < 86400), // within 24h
            'message' => 'Last verified OpenAI webhook (' . $event_type . ') ' . human_time_diff($last_verified_hit['time']) . ' ago (' . date('H:i:s', $last_verified_hit['time']) . ')',
        );
    } elseif (is_array($last_hit) && !empty($last_hit['time'])) {
        $services['openai_webhook_last_hit'] = array(
            'label'   => 'OpenAI Webhook Last Hit',
            'valid'   => false,
            'message' => 'Only an unsigned probe hit this endpoint ' . human_time_diff($last_hit['time']) . ' ago — no verified OpenAI realtime webhook has been received yet.',
        );
    } else {
        $services['openai_webhook_last_hit'] = array(
            'label'   => 'OpenAI Webhook Last Hit',
            'valid'   => false,
            'message' => 'NEVER — OpenAI has not called your webhook. Verify project enrollment in Realtime SIP beta and webhook subscription to "realtime.call.incoming" event.',
        );
    }

    // ── 10. Last realtime accept failure (if any) ──
    $accept_fail = get_transient('vmo_openai_accept_lastfail');
    if (is_array($accept_fail) && !empty($accept_fail['time'])) {
        $services['openai_accept_status'] = array(
            'label'   => 'OpenAI /accept Endpoint',
            'valid'   => false,
            'message' => 'Last accept FAILED: ' . ($accept_fail['reason'] ?? 'unknown') . ' — ' . substr((string)($accept_fail['response_body'] ?? $accept_fail['message'] ?? ''), 0, 200),
        );
    } else {
        $services['openai_accept_status'] = array('label' => 'OpenAI /accept Endpoint', 'valid' => true, 'message' => 'No recent failures');
    }

    // ── 11. Last realtime SIP dial result ──
    $rt_err = get_transient('vmo_realtime_last_error_' . $user_id);
    if (is_array($rt_err) && !empty($rt_err['time'])) {
        $when = human_time_diff($rt_err['time']) . ' ago';
        $services['last_sip_dial'] = array(
            'label'   => 'Most Recent Realtime Call',
            'valid'   => false,
            'message' => '[' . $when . '] Failed: ' . substr((string)($rt_err['message'] ?? ''), 0, 240),
        );
    } else {
        $services['last_sip_dial'] = array('label' => 'Most Recent Realtime Call', 'valid' => true, 'message' => 'No recent failures recorded');
    }

    // ── 12. Platform-wide SIP trunk configured (the SaaS-scale path to OpenAI) ──
    $trunk_sid = (string) get_option('vmo_twilio_sip_trunk_sid', '');
    if (empty($trunk_sid)) {
        $services['sip_trunk_configured'] = array(
            'label'   => 'Platform SIP Trunk',
            'valid'   => false,
            'message' => 'NOT configured. Click "Auto-Configure Realtime SIP Trunk" below — required for OpenAI to accept SIP calls at scale.',
        );
    } else {
        $services['sip_trunk_configured'] = array(
            'label'   => 'Platform SIP Trunk',
            'valid'   => true,
            'message' => 'Trunk SID: ' . $trunk_sid,
        );
        $details['trunk_sid'] = $trunk_sid;
    }

    // ── 12a. Trunk disaster recovery points back to the legacy voice webhook ──
    if (!empty($trunk_sid) && $services['twilio_api_auth']['valid']) {
        $expected_dr_url = class_exists('VMO_Twilio_Manager')
            ? VMO_Twilio_Manager::get_instance()->phone_gateway_url('voice')
            : '';
        $trunk_info = wp_remote_get(
            'https://trunking.twilio.com/v1/Trunks/' . $trunk_sid,
            array(
                'timeout' => 10,
                'headers' => array('Authorization' => 'Basic ' . base64_encode($sid . ':' . $token)),
            )
        );
        if (is_wp_error($trunk_info)) {
            $services['sip_trunk_disaster_recovery'] = array(
                'label'   => 'Trunk Disaster Recovery',
                'valid'   => false,
                'message' => 'Could not query trunk details: ' . $trunk_info->get_error_message(),
            );
        } else {
            $trunk_body = json_decode(wp_remote_retrieve_body($trunk_info), true);
            $live_dr_url = (string) ($trunk_body['disaster_recovery_url'] ?? '');
            $live_dr_method = strtoupper((string) ($trunk_body['disaster_recovery_method'] ?? ''));
            $dr_ok = ($expected_dr_url !== '' && $live_dr_url === $expected_dr_url && $live_dr_method === 'POST');
            $services['sip_trunk_disaster_recovery'] = array(
                'label'   => 'Trunk Disaster Recovery',
                'valid'   => $dr_ok,
                'message' => $dr_ok
                    ? 'Fallback is set to the legacy voice webhook if OpenAI SIP does not answer.'
                    : ('Current fallback is ' . ($live_dr_url ?: '(empty)') . ' [' . ($live_dr_method ?: 'n/a') . ']'),
            );
            $details['trunk_disaster_recovery_url'] = $live_dr_url;
        }
    } else {
        $services['sip_trunk_disaster_recovery'] = array(
            'label'   => 'Trunk Disaster Recovery',
            'valid'   => false,
            'message' => empty($trunk_sid) ? 'Skipped (no trunk configured)' : 'Skipped (Twilio auth unavailable)',
        );
    }

    // ── 12b. Platform trunk points at the exact OpenAI SIP URI we expect ──
    if (!empty($trunk_sid) && $services['twilio_api_auth']['valid']) {
        $live_origination_sip = '';
        $origination_resp = wp_remote_get(
            'https://trunking.twilio.com/v1/Trunks/' . $trunk_sid . '/OriginationUrls',
            array(
                'timeout' => 10,
                'headers' => array('Authorization' => 'Basic ' . base64_encode($sid . ':' . $token)),
            )
        );
        if (is_wp_error($origination_resp)) {
            $services['sip_trunk_origination_uri'] = array(
                'label'   => 'Platform Trunk OpenAI SIP URI',
                'valid'   => false,
                'message' => 'Could not query trunk origination URLs: ' . $origination_resp->get_error_message(),
            );
        } else {
            $origination_body = json_decode(wp_remote_retrieve_body($origination_resp), true);
            if (!empty($origination_body['origination_urls']) && is_array($origination_body['origination_urls'])) {
                foreach ($origination_body['origination_urls'] as $origination_url) {
                    $sip_url = (string) ($origination_url['sip_url'] ?? '');
                    if (stripos($sip_url, '@sip.api.openai.com') !== false) {
                        $live_origination_sip = $sip_url;
                        break;
                    }
                }
            }

            $origination_ok = ($expected_openai_sip !== '' && $live_origination_sip === $expected_openai_sip);
            $services['sip_trunk_origination_uri'] = array(
                'label'   => 'Platform Trunk OpenAI SIP URI',
                'valid'   => $origination_ok,
                'message' => $origination_ok
                    ? 'Twilio trunk is pointed at the exact OpenAI SIP URI for this project.'
                    : ('Twilio trunk is using ' . ($live_origination_sip ?: '(missing)') . ' instead of ' . ($expected_openai_sip ?: '(missing expected SIP URI)')),
            );
            $details['trunk_origination_sip_url_live'] = $live_origination_sip;
            $details['trunk_origination_sip_url_expected'] = $expected_openai_sip;
        }
    } else {
        $services['sip_trunk_origination_uri'] = array(
            'label'   => 'Platform Trunk OpenAI SIP URI',
            'valid'   => false,
            'message' => empty($trunk_sid) ? 'Skipped (no trunk configured)' : 'Skipped (Twilio auth unavailable)',
        );
    }

    // ── 13. THIS user's number is assigned to the trunk ──
    if (!empty($twilio_phone_sid) && !empty($trunk_sid) && $services['twilio_api_auth']['valid']) {
        $assigned = false;
        $assigned_msg = '';
        $list = wp_remote_get(
            'https://trunking.twilio.com/v1/Trunks/' . $trunk_sid . '/PhoneNumbers',
            array(
                'timeout' => 10,
                'headers' => array('Authorization' => 'Basic ' . base64_encode($sid . ':' . $token)),
            )
        );
        if (!is_wp_error($list)) {
            $list_body = json_decode(wp_remote_retrieve_body($list), true);
            if (!empty($list_body['phone_numbers']) && is_array($list_body['phone_numbers'])) {
                foreach ($list_body['phone_numbers'] as $pn) {
                    if (($pn['sid'] ?? '') === $twilio_phone_sid) {
                        $assigned = true;
                        break;
                    }
                }
            }
            $assigned_msg = $assigned
                ? 'Your number is on the trunk → routes to OpenAI Realtime'
                : 'Your number is NOT on the trunk → click Auto-Configure to backfill';
        } else {
            $assigned_msg = 'Could not query trunk membership: ' . $list->get_error_message();
        }
        $services['number_on_trunk'] = array(
            'label'   => 'Number Assigned to Trunk',
            'valid'   => $assigned,
            'message' => $assigned_msg,
        );

        $number_routing_resp = wp_remote_get(
            'https://api.twilio.com/2010-04-01/Accounts/' . $sid . '/IncomingPhoneNumbers/' . $twilio_phone_sid . '.json',
            array(
                'timeout' => 10,
                'headers' => array('Authorization' => 'Basic ' . base64_encode($sid . ':' . $token)),
            )
        );
        if (is_wp_error($number_routing_resp)) {
            $services['number_live_routing'] = array(
                'label'   => 'Twilio Number Live Routing',
                'valid'   => false,
                'message' => 'Could not query Twilio number routing: ' . $number_routing_resp->get_error_message(),
            );
        } else {
            $number_routing_body = json_decode(wp_remote_retrieve_body($number_routing_resp), true);
            $live_number_trunk_sid = (string) ($number_routing_body['trunk_sid'] ?? '');
            $live_number_voice_url = (string) ($number_routing_body['voice_url'] ?? '');
            $routing_ok = ($live_number_trunk_sid !== '' && $live_number_trunk_sid === $trunk_sid);
            $services['number_live_routing'] = array(
                'label'   => 'Twilio Number Live Routing',
                'valid'   => $routing_ok,
                'message' => $routing_ok
                    ? 'This number is routing by TrunkSid on Twilio, so the legacy voice webhook is bypassed unless disaster recovery fires.'
                    : ('Twilio still shows trunk_sid=' . ($live_number_trunk_sid ?: '(empty)') . ' voice_url=' . ($live_number_voice_url ?: '(empty)')),
            );
            $details['twilio_number_trunk_sid_live'] = $live_number_trunk_sid;
            $details['twilio_number_voice_url_live'] = $live_number_voice_url;
        }
    } else {
        $services['number_on_trunk'] = array(
            'label'   => 'Number Assigned to Trunk',
            'valid'   => false,
            'message' => empty($trunk_sid) ? 'Skipped (no trunk configured)' : 'Skipped (no number SID)',
        );
        $services['number_live_routing'] = array(
            'label'   => 'Twilio Number Live Routing',
            'valid'   => false,
            'message' => empty($trunk_sid) ? 'Skipped (no trunk configured)' : 'Skipped (no number SID)',
        );
    }

    }

    // ── Summary ──
    $total = count($services);
    $passed = 0;
    foreach ($services as $svc) if (!empty($svc['valid'])) $passed++;
    $bridge_configured = (
        !empty($services['twilio_credentials']['valid']) &&
        !empty($services['twilio_api_auth']['valid']) &&
        !empty($services['phone_assigned']['valid']) &&
        !empty($services['premium_voice_tier']['valid']) &&
        !empty($services['twilio_voice_url']['valid']) &&
        !empty($services['premium_bridge_url']['valid']) &&
        !empty($services['premium_bridge_media_secret']['valid']) &&
        !empty($services['premium_bridge_listen_secret']['valid'])
    );
    if ($legacy_sip_enabled) {
        $listen_configured = (
            !empty($services['twilio_credentials']['valid']) &&
            !empty($services['twilio_api_auth']['valid']) &&
            !empty($services['phone_assigned']['valid']) &&
            !empty($services['twilio_voice_url']['valid']) &&
            !empty($services['openai_api_key']['valid']) &&
            !empty($services['openai_project_id']['valid']) &&
            !empty($services['openai_project_access']['valid']) &&
            !empty($services['openai_webhook_secret']['valid']) &&
            !empty($services['webhook_reachable']['valid']) &&
            !empty($services['sip_trunk_configured']['valid']) &&
            !empty($services['sip_trunk_origination_uri']['valid']) &&
            !empty($services['number_on_trunk']['valid']) &&
            !empty($services['number_live_routing']['valid'])
        );
        $listen_verified = ($listen_configured && !empty($services['openai_webhook_last_hit']['valid']) && !empty($services['last_sip_dial']['valid']));
    } else {
        $listen_configured = $bridge_configured;
        $listen_verified = ($bridge_configured && $latest_premium_bridge_verified);
    }
    $listen_state = $listen_verified ? 'verified' : ($listen_configured ? 'configured_not_verified' : 'not_ready');
    $listen_state_message = '';
    if (!$legacy_sip_enabled && $voice_quality_tier !== 'premium') {
        $listen_state_message = 'Premium bridge settings may be present, but Standard mode is selected in ringer settings. Save Phone Settings with Premium selected to route inbound calls to the VMO live audio bridge.';
    } elseif (!$legacy_sip_enabled && !$bridge_dns_ok) {
        $listen_state_message = 'Premium bridge settings are present, but this server could not resolve the bridge host during the diagnostic check. Premium routing will still attempt the configured bridge URL; if calls still drop, inspect the bridge server logs and Twilio call error for the exact WebSocket failure.';
    } elseif (!$legacy_sip_enabled && $listen_state === 'verified') {
        $listen_state_message = 'Premium inbound calls are configured for the VMO live audio bridge. Confirm the saved secrets match the bridge environment file before restarting the bridge service.';
    } elseif ($listen_state === 'verified') {
        $listen_state_message = 'Inbound calls should engage the enabled legacy OpenAI SIP route and the dashboard Listen button should play live audio.';
    } elseif ($listen_state === 'configured_not_verified') {
        $listen_state_message = $legacy_sip_enabled
            ? 'Twilio is pointed at the expected OpenAI SIP destination, but OpenAI has still not completed a verified realtime webhook/accept cycle for this environment.'
            : 'Bridge fields are present, but the latest inbound call has not been verified as a Premium bridge session yet. Place a fresh test call after saving Premium mode; this panel now checks whether the voice webhook actually emitted bridge TwiML.';
    } else {
        $listen_state_message = 'Fix the failing rows above in order from top to bottom. The failing row usually identifies the exact layer where the mismatch still exists.';
    }

    wp_send_json_success(array(
        'services'      => $services,
        'passed'        => $passed,
        'total'         => $total,
        'listen_ready'  => $listen_verified,
        'listen_state'  => $listen_state,
        'listen_state_message' => $listen_state_message,
        'details'       => $details,
    ));
});

// AJAX: Phone system health check — returns comprehensive diagnostics.
// Called by the frontend on phone tab open to surface configuration issues.
add_action('wp_ajax_vmo_phone_system_check', function() {
    if (!current_user_can('read')) {
        wp_send_json_error(array('message' => 'Forbidden'), 403);
    }
    if (!function_exists('vmo_verify_admin_security') || !vmo_verify_admin_security()) {
        wp_send_json_error(array('message' => 'Security check failed'), 403);
    }

    $user_id = get_current_user_id();
    $checks = array();
    $legacy_sip_enabled = false;

    // 1. Twilio credentials
    $account_sid = (string) (defined('VMO_TWILIO_ACCOUNT_SID') ? VMO_TWILIO_ACCOUNT_SID : get_option('vmo_twilio_account_sid', ''));
    $auth_token  = (string) (defined('VMO_TWILIO_AUTH_TOKEN') ? VMO_TWILIO_AUTH_TOKEN : get_option('vmo_twilio_auth_token', ''));
    $has_sid   = !empty($account_sid);
    $has_token = !empty($auth_token);
    $checks['twilio_credentials'] = ($has_sid && $has_token) ? 'ok' : 'missing';

    // 2. Twilio Manager class
    $checks['twilio_manager'] = class_exists('VMO_Twilio_Manager') ? 'ok' : 'missing';

    // 3. Phone number in database
    global $wpdb;
    $table = $wpdb->prefix . 'vmo_phone_numbers';
    $table_exists = vmo_table_exists($table);
    $checks['phone_table'] = $table_exists ? 'ok' : 'missing';

    $phone_number = '';
    $twilio_sid = '';
    if ($table_exists) {
        $phone_col = 'phone_number';
        if (class_exists('VMO_Twilio_Manager')) {
            try {
                $mgr = VMO_Twilio_Manager::get_instance();
                $phone_number = (string) ($mgr->get_user_phone($user_id) ?: '');
            } catch (Throwable $e) {}
        }
        $row = $wpdb->get_row($wpdb->prepare(
            "SELECT * FROM {$table} WHERE user_id = %d AND status IN ('active','pending_port') ORDER BY created_at DESC LIMIT 1",
            $user_id
        ));
        if ($row) {
            $twilio_sid = isset($row->twilio_sid) ? (string) $row->twilio_sid : '';
            if (empty($phone_number)) {
                // Try common column names
                foreach (array('phone_number', 'phone', 'number', 'twilio_number') as $col) {
                    if (isset($row->{$col}) && !empty($row->{$col})) {
                        $phone_number = (string) $row->{$col};
                        break;
                    }
                }
            }
        }
    }
    if (empty($phone_number)) {
        $phone_number = (string) (get_user_meta($user_id, 'vmo_phone_number', true) ?: '');
    }
    if (empty($twilio_sid)) {
        $twilio_sid = (string) get_user_meta($user_id, 'vmo_phone_sid', true);
    }
    if (empty($twilio_sid) && !empty($phone_number) && $has_sid && $has_token) {
        $lookup = wp_remote_get(
            "https://api.twilio.com/2010-04-01/Accounts/{$account_sid}/IncomingPhoneNumbers.json?PhoneNumber=" . rawurlencode($phone_number),
            array(
                'timeout' => 10,
                'headers' => array('Authorization' => 'Basic ' . base64_encode($account_sid . ':' . $auth_token)),
            )
        );
        if (!is_wp_error($lookup)) {
            $lookup_body = json_decode(wp_remote_retrieve_body($lookup), true);
            if (!empty($lookup_body['incoming_phone_numbers'][0]['sid'])) {
                $twilio_sid = (string) $lookup_body['incoming_phone_numbers'][0]['sid'];
                update_user_meta($user_id, 'vmo_phone_sid', $twilio_sid);
                if ($row && isset($row->id)) {
                    $wpdb->update(
                        $table,
                        array('twilio_sid' => $twilio_sid),
                        array('id' => $row->id)
                    );
                }
            }
        }
    }
    $checks['phone_number'] = !empty($phone_number) ? $phone_number : 'none';
    $checks['twilio_sid'] = !empty($twilio_sid) ? substr($twilio_sid, 0, 8) . '...' : 'none';

    // 4. Webhook key
    $webhook_key = (string) get_option('vmo_phone_webhook_key', '');
    $checks['webhook_key'] = !empty($webhook_key) ? 'set (' . strlen($webhook_key) . ' chars)' : 'missing';

    // 5. TwiML App
    $twiml_app = (string) get_option('vmo_twilio_twiml_app_sid', '');
    $checks['twiml_app'] = !empty($twiml_app) ? 'ok' : 'missing';

    // 6. API Key
    $api_key = (string) get_option('vmo_twilio_api_key_sid', '');
    $checks['api_key'] = !empty($api_key) ? 'ok' : 'missing';

    // 7. OpenAI key (for TTS/AI) — check both option names
    $oai_key = get_option('vmo_openai_api_key', '');
    if (empty($oai_key)) {
        $oai_key = get_option('vmo_manager_openai_api_key', '');
    }
    $checks['openai_key'] = !empty($oai_key) ? 'ok' : 'missing';

    $bridge_url = '';
    foreach (array(
        defined('VMO_PHONE_BRIDGE_URL') ? (string) VMO_PHONE_BRIDGE_URL : '',
        (string) get_option('vmo_phone_bridge_url', ''),
        getenv('VMO_PHONE_BRIDGE_URL') ?: '',
        'wss://bridge.getvmo.com',
    ) as $candidate) {
        $candidate = trim((string) $candidate);
        if ($candidate !== '') {
            $bridge_url = rtrim($candidate, '/');
            break;
        }
    }
    $bridge_url_valid = (bool) preg_match('#^(wss?|https?)://#i', $bridge_url);
    $checks['premium_bridge_url'] = $bridge_url_valid ? $bridge_url : 'missing or invalid';
    $bridge_dns_status = vmo_phone_bridge_dns_status($bridge_url_valid ? $bridge_url : '');
    $bridge_dns_ok = $bridge_url_valid && !empty($bridge_dns_status['ok']);
    $checks['premium_bridge_dns'] = $bridge_dns_ok
        ? ((string) ($bridge_dns_status['host'] ?? '') . ' — ' . (string) ($bridge_dns_status['message'] ?? 'resolves'))
        : ((string) ($bridge_dns_status['host'] ?? $bridge_url) . ' — DNS lookup failed');
    $client_bridge_signer = vmo_client_bridge_signer_status();
    $client_mode = !empty($client_bridge_signer['client_mode']);
    $client_bridge_signer_ready = !empty($client_bridge_signer['ready']);
    $checks['premium_bridge_signer'] = (string) ($client_bridge_signer['message'] ?? 'unknown');

    $media_secret = '';
    foreach (array(
        defined('VMO_MEDIA_SECRET') ? (string) VMO_MEDIA_SECRET : '',
        (string) get_option('vmo_media_secret', ''),
        getenv('VMO_MEDIA_SECRET') ?: '',
        defined('VMO_BRIDGE_MEDIA_SECRET') ? (string) VMO_BRIDGE_MEDIA_SECRET : '',
        (string) get_option('vmo_bridge_media_secret', ''),
        getenv('VMO_BRIDGE_MEDIA_SECRET') ?: '',
        defined('VMO_LISTEN_SECRET') ? (string) VMO_LISTEN_SECRET : '',
        (string) get_option('vmo_listen_secret', ''),
        getenv('VMO_LISTEN_SECRET') ?: '',
    ) as $candidate) {
        $candidate = trim((string) $candidate);
        if ($candidate !== '') {
            $media_secret = $candidate;
            break;
        }
    }
    $listen_secret = '';
    foreach (array(
        defined('VMO_LISTEN_SECRET') ? (string) VMO_LISTEN_SECRET : '',
        (string) get_option('vmo_listen_secret', ''),
        getenv('VMO_LISTEN_SECRET') ?: '',
        defined('VMO_BRIDGE_LISTEN_SECRET') ? (string) VMO_BRIDGE_LISTEN_SECRET : '',
        (string) get_option('vmo_bridge_listen_secret', ''),
        getenv('VMO_BRIDGE_LISTEN_SECRET') ?: '',
    ) as $candidate) {
        $candidate = trim((string) $candidate);
        if ($candidate !== '') {
            $listen_secret = $candidate;
            break;
        }
    }
    $checks['premium_bridge_media_secret'] = $client_mode
        ? ($client_bridge_signer_ready ? 'client-mode — master signer ready; no local secret required' : 'client-mode — master signer not ready')
        : (($media_secret !== '') ? ('set (' . strlen($media_secret) . ' chars)') : 'missing');
    $checks['premium_bridge_listen_secret'] = $client_mode
        ? ($client_bridge_signer_ready ? 'client-mode — master signer ready; no local secret required' : 'client-mode — master signer not ready')
        : (($listen_secret !== '') ? ('set (' . strlen($listen_secret) . ' chars)') : 'missing');
    $checks['phone_runtime_mode'] = $legacy_sip_enabled ? 'legacy SIP fallback enabled' : 'websocket bridge mode';
    $voice_quality_tier = strtolower(trim((string) get_user_meta($user_id, 'vmo_voice_quality_tier', true)));
    $voice_quality_tier = ($voice_quality_tier === 'premium') ? 'premium' : 'standard';
    $bridge_auth_ready = $client_mode ? $client_bridge_signer_ready : ($media_secret !== '');
    $listen_auth_ready = $client_mode ? $client_bridge_signer_ready : ($listen_secret !== '');
    $premium_bridge_enabled_for_user = ($voice_quality_tier === 'premium' && $bridge_url_valid && $bridge_auth_ready);
    $checks['voice_quality_tier'] = $voice_quality_tier;
    $checks['premium_bridge_enabled_for_user'] = $premium_bridge_enabled_for_user
        ? 'yes'
        : (($voice_quality_tier !== 'premium') ? 'no — Standard mode is selected in ringer settings' : ($client_mode ? 'no — client master bridge signer is not ready' : 'no — bridge URL or media secret is missing'));

    $rt_project = '';
    $rt_secret = '';
    $rt_project_valid = false;
    $project_access = array('valid' => false);
    if ($legacy_sip_enabled) {
        $rt_project = (string) get_option('vmo_openai_project_id', '');
        if ($rt_project === '' && defined('VMO_OPENAI_PROJECT_ID')) { $rt_project = (string) VMO_OPENAI_PROJECT_ID; }
        $rt_secret  = (string) get_option('vmo_openai_webhook_secret', '');
        if ($rt_secret === '' && defined('VMO_OPENAI_WEBHOOK_SECRET')) { $rt_secret = (string) VMO_OPENAI_WEBHOOK_SECRET; }
        $rt_project_valid = vmo_is_valid_openai_realtime_project_id($rt_project);
        $project_access = vmo_check_openai_project_api_access($oai_key, $rt_project);
        $checks['openai_realtime_project_id'] = empty($rt_project)
            ? 'missing'
            : ($rt_project_valid ? ('set (' . substr($rt_project, 0, 12) . '...)') : 'invalid format — must begin with proj_');
        $checks['openai_project_api_access'] = empty($oai_key)
            ? 'skipped — missing API key'
            : (!$rt_project_valid
                ? 'skipped — invalid project id'
                : (!empty($project_access['valid'])
                    ? 'ok'
                    : ('failed' . (!empty($project_access['status_code']) ? ' — HTTP ' . intval($project_access['status_code']) : '') . (!empty($project_access['openai_request_id']) ? ' req ' . $project_access['openai_request_id'] : ''))));
        $checks['openai_realtime_webhook_secret'] = !empty($rt_secret) ? 'set' : 'missing';
    } else {
        $checks['legacy_sip_runtime'] = 'removed — Premium uses WebSocket bridge; Standard uses Voice URL webhook';
    }

    $trunk_sid = (string) get_option('vmo_twilio_sip_trunk_sid', '');
    $checks['sip_trunk_configured'] = !empty($trunk_sid) ? substr($trunk_sid, 0, 8) . '... (legacy cleanup only)' : 'not configured (ok)';

    $number_on_trunk = null;
    $live_number_trunk_sid = '';
    $live_number_voice_url = '';
    $live_number_routing = 'unknown';
    if (!empty($trunk_sid) && !empty($twilio_sid) && $has_sid && $has_token) {
        $membership = wp_remote_get(
            'https://trunking.twilio.com/v1/Trunks/' . rawurlencode($trunk_sid) . '/PhoneNumbers',
            array(
                'timeout' => 10,
                'headers' => array('Authorization' => 'Basic ' . base64_encode($account_sid . ':' . $auth_token)),
            )
        );
        if (is_wp_error($membership)) {
            $checks['number_on_trunk'] = 'unknown — ' . $membership->get_error_message();
        } else {
            $body = json_decode(wp_remote_retrieve_body($membership), true);
            $assigned = false;
            if (!empty($body['phone_numbers']) && is_array($body['phone_numbers'])) {
                foreach ($body['phone_numbers'] as $phone) {
                    if (($phone['sid'] ?? '') === $twilio_sid) {
                        $assigned = true;
                        break;
                    }
                }
            }
            $number_on_trunk = $assigned;
            $checks['number_on_trunk'] = $assigned ? 'yes' : 'no';
        }

        $number_info = wp_remote_get(
            'https://api.twilio.com/2010-04-01/Accounts/' . rawurlencode($account_sid) . '/IncomingPhoneNumbers/' . rawurlencode($twilio_sid) . '.json',
            array(
                'timeout' => 10,
                'headers' => array('Authorization' => 'Basic ' . base64_encode($account_sid . ':' . $auth_token)),
            )
        );
        if (is_wp_error($number_info)) {
            $checks['number_live_routing'] = 'unknown — ' . $number_info->get_error_message();
        } else {
            $number_body = json_decode(wp_remote_retrieve_body($number_info), true);
            $live_number_trunk_sid = (string) ($number_body['trunk_sid'] ?? '');
            $live_number_voice_url = (string) ($number_body['voice_url'] ?? '');

            if ($live_number_trunk_sid !== '' && $live_number_trunk_sid === $trunk_sid) {
                $live_number_routing = 'trunk';
                $checks['number_live_routing'] = 'trunk';
            } elseif ($live_number_trunk_sid !== '' && $live_number_trunk_sid !== $trunk_sid) {
                $live_number_routing = 'wrong-trunk';
                $checks['number_live_routing'] = 'wrong trunk — ' . substr($live_number_trunk_sid, 0, 8) . '...';
            } elseif ($live_number_voice_url !== '') {
                $live_number_routing = 'voice-webhook';
                $checks['number_live_routing'] = 'voice webhook';
            } else {
                $live_number_routing = 'unconfigured';
                $checks['number_live_routing'] = 'unconfigured';
            }
        }
    } elseif (empty($trunk_sid)) {
        $checks['number_on_trunk'] = 'not_applicable — no platform SIP trunk configured';
        $checks['number_live_routing'] = 'not_applicable — no platform SIP trunk configured';
    } elseif (empty($twilio_sid)) {
        $checks['number_on_trunk'] = 'unknown — missing Twilio phone SID';
        $checks['number_live_routing'] = 'unknown — missing Twilio phone SID';
    } else {
        $checks['number_on_trunk'] = 'unknown';
        $checks['number_live_routing'] = 'unknown';
    }

    $has_recent_webhook_hit = false;
    if ($legacy_sip_enabled) {
        $last_verified_webhook_hit = get_transient('vmo_openai_webhook_lastverified');
        $last_webhook_hit = get_transient('vmo_openai_webhook_lasthit');
        if (is_array($last_verified_webhook_hit) && !empty($last_verified_webhook_hit['time'])) {
            $hit_time = intval($last_verified_webhook_hit['time']);
            $has_recent_webhook_hit = ((time() - $hit_time) < DAY_IN_SECONDS);
            $checks['openai_webhook_last_hit'] = 'verified ' . human_time_diff($hit_time) . ' ago';
        } elseif (is_array($last_webhook_hit) && !empty($last_webhook_hit['time'])) {
            $checks['openai_webhook_last_hit'] = 'probe seen ' . human_time_diff(intval($last_webhook_hit['time'])) . ' ago, but no verified OpenAI webhook';
        } else {
            $checks['openai_webhook_last_hit'] = 'never';
        }
    } else {
        $checks['openai_webhook_last_hit'] = 'not_applicable — legacy SIP removed';
    }

    $recent_realtime_message = '';
    if ($legacy_sip_enabled) {
        $recent_realtime_error = get_transient('vmo_realtime_last_error_' . $user_id);
        if (is_array($recent_realtime_error) && !empty($recent_realtime_error['message'])) {
            $recent_realtime_message = (string) $recent_realtime_error['message'];
            $checks['realtime_last_error'] = substr($recent_realtime_message, 0, 180);
        } else {
            $checks['realtime_last_error'] = 'none';
        }
    } else {
        $checks['realtime_last_error'] = 'not_applicable — legacy SIP removed';
    }

    $latest_premium_bridge_verified = false;
    $latest_call = null;
    $calls_table = $wpdb->prefix . 'vmo_phone_calls';
    if (function_exists('vmo_table_exists') && vmo_table_exists($calls_table)) {
        $latest_call = $wpdb->get_row($wpdb->prepare(
            "SELECT call_sid, topic, status, created_at FROM {$calls_table} WHERE user_id = %d AND direction = 'inbound' ORDER BY id DESC LIMIT 1",
            $user_id
        ));
    }
    if ($latest_call && !empty($latest_call->call_sid)) {
        $latest_trace = get_transient('vmo_voice_trace_' . sanitize_key((string) $latest_call->call_sid));
        $trace_text = is_array($latest_trace) ? implode(' | ', array_slice($latest_trace, -8)) : '';
        $topic_hit = ((string) ($latest_call->topic ?? '') === 'premium_realtime_bridge');
        $trace_hit = (strpos($trace_text, 'premium_bridge_twiml_emitted_exit') !== false);
        $latest_premium_bridge_verified = ($topic_hit || $trace_hit);
        $checks['latest_inbound_call'] = (string) $latest_call->call_sid . ' status=' . (string) ($latest_call->status ?? '') . ' topic=' . (((string) ($latest_call->topic ?? '')) ?: '(empty)');
        $checks['latest_call_hit_premium_bridge'] = $latest_premium_bridge_verified ? 'yes' : 'no';
        $checks['latest_premium_bridge_trace'] = $trace_text !== '' ? $trace_text : 'no transient trace found';
        $latest_twilio_call = vmo_twilio_call_resource_summary($account_sid, $auth_token, (string) $latest_call->call_sid);
        $checks['latest_inbound_twilio_status'] = (string) ($latest_twilio_call['message'] ?? 'Twilio lookup unavailable');
        $latest_premium_bridge_stream = get_transient('vmo_premium_bridge_last_stream_' . $user_id);
        $checks['latest_premium_bridge_stream'] = is_array($latest_premium_bridge_stream) && !empty($latest_premium_bridge_stream['message'])
            ? ((string) $latest_premium_bridge_stream['message'] . ' (' . human_time_diff((int) ($latest_premium_bridge_stream['time'] ?? time())) . ' ago)')
            : 'none recorded yet';
        $latest_premium_bridge_error = get_transient('vmo_premium_bridge_last_error_' . $user_id);
        $checks['latest_premium_bridge_error'] = is_array($latest_premium_bridge_error) && !empty($latest_premium_bridge_error['message'])
            ? ((string) $latest_premium_bridge_error['message'] . ' (' . human_time_diff((int) ($latest_premium_bridge_error['time'] ?? time())) . ' ago)')
            : 'none recorded yet';
    } else {
        $checks['latest_inbound_call'] = 'none';
        $checks['latest_call_hit_premium_bridge'] = 'no';
        $checks['latest_premium_bridge_trace'] = 'no inbound call record found yet';
        $checks['latest_inbound_twilio_status'] = 'no inbound call record found yet';
        $checks['latest_premium_bridge_stream'] = 'none recorded yet';
        $checks['latest_premium_bridge_error'] = 'none recorded yet';
    }

    if (!$legacy_sip_enabled) {
        if ($voice_quality_tier !== 'premium') {
            $listen_status = 'standard-mode — ringer settings are set to Standard, so incoming calls will use the standard transcript/text/TTS path';
        } elseif (!$bridge_url_valid) {
            $listen_status = 'unavailable — set the Premium Bridge WebSocket URL in VMO Settings';
        } elseif (!$bridge_dns_ok) {
            $listen_status = 'dns_warning — this server could not resolve the Premium bridge host, but Premium routing will still attempt the configured bridge URL';
        } elseif ($client_mode && !$client_bridge_signer_ready) {
            $listen_status = 'unavailable — client master bridge signer is not ready; confirm license key, installation id, and master proxy URL';
        } elseif (!$client_mode && $media_secret === '') {
            $listen_status = 'unavailable — set Bridge Media Secret to match VMO_MEDIA_SECRET on the bridge server';
        } elseif (!$client_mode && $listen_secret === '') {
            $listen_status = 'unavailable — set Bridge Listen Secret to match VMO_LISTEN_SECRET on the bridge server';
        } elseif ($live_number_routing === 'trunk') {
            $listen_status = 'misconfigured — this number is still routed to the old SIP trunk; remove legacy SIP routing so calls reach the Voice URL webhook';
        } elseif (!$latest_premium_bridge_verified) {
            $listen_status = 'not_verified — Premium bridge configuration is present, but the latest inbound call did not emit Premium bridge TwiML yet';
        } else {
            $listen_status = 'ok — Premium uses the WebSocket bridge and legacy SIP setup is disabled';
        }
    } elseif (empty($oai_key) || empty($rt_project) || empty($rt_secret)) {
        $listen_status = 'unavailable — set the OpenAI Realtime Project ID and Webhook Secret in VMO Settings';
    } elseif (!$rt_project_valid) {
        $listen_status = 'unavailable — the OpenAI Realtime Project ID is invalid; it must be the exact proj_... value from OpenAI Project > General';
    } elseif (empty($project_access['valid'])) {
        $listen_status = 'unavailable — the configured OpenAI API key cannot access the configured OpenAI project';
    } elseif (empty($trunk_sid)) {
        $listen_status = 'unavailable — the platform SIP trunk is not configured';
    } elseif ($number_on_trunk === false) {
        $listen_status = 'unavailable — this number is not assigned to the platform SIP trunk and is still using the legacy voice webhook path';
    } elseif ($number_on_trunk === true && $live_number_routing !== 'trunk') {
        $listen_status = 'misconfigured — the number is listed on the platform SIP trunk, but Twilio is still routing inbound calls through the legacy voice webhook path instead of TrunkSid';
    } elseif ($number_on_trunk === null && empty($twilio_sid)) {
        $listen_status = 'degraded — the Twilio phone SID is missing, so trunk assignment could not be verified';
    } elseif ($number_on_trunk === true && !$has_recent_webhook_hit) {
        $listen_status = 'not_verified — this number is on the realtime trunk, but OpenAI has never hit the realtime webhook for this environment yet';
    } elseif ($recent_realtime_message !== '' && stripos($recent_realtime_message, 'SIP <Dial> to OpenAI Realtime FAILED') !== false && !$has_recent_webhook_hit) {
        $listen_status = 'provider-blocked — the most recent realtime call reached Twilio, but OpenAI never answered the SIP invite';
    } elseif ($recent_realtime_message !== '') {
        $listen_status = 'degraded — a recent realtime error was recorded';
    } else {
        $listen_status = 'ok';
    }
    $checks['live_audio_listen_ready'] = $listen_status;

    // 8. Gateway URL test
    if (class_exists('VMO_Twilio_Manager')) {
        try {
            $mgr = VMO_Twilio_Manager::get_instance();
            $voice_url = $mgr->phone_gateway_url('voice');
            $checks['voice_webhook_url'] = $voice_url;
        } catch (Throwable $e) {
            $checks['voice_webhook_url'] = 'error: ' . $e->getMessage();
        }
    }

    // 9. Admin guidance — tell the frontend EXACTLY what to do
    $actions_needed = array();
    if (!$has_sid || !$has_token) {
        $actions_needed[] = 'Phone provisioning is managed by the VMO master service for client installs.';
    }
    if (empty($oai_key)) {
        $actions_needed[] = 'AI and billing credentials are managed by the VMO master service for client installs.';
    }
    if (!$legacy_sip_enabled) {
        if ($voice_quality_tier !== 'premium') {
            $actions_needed[] = 'PREMIUM LIVE AUDIO: Open Phone System settings, choose Premium under Voice Quality, and save. Standard mode remains available and intentionally uses the transcript/text/TTS path.';
        }
        if (!$bridge_url_valid) {
            $actions_needed[] = 'PREMIUM LIVE AUDIO: Go to VMO Settings and set Bridge WebSocket URL to wss://bridge.getvmo.com (or the active bridge host).';
        }
        if ($bridge_url_valid && !$bridge_dns_ok) {
            $actions_needed[] = 'PREMIUM LIVE AUDIO: This server could not resolve ' . (string) ($bridge_dns_status['host'] ?? 'the bridge host') . ' during diagnostics. Premium will still attempt the configured bridge; if calls still drop, check the bridge server logs and the Twilio call error for the WebSocket close reason.';
        }
        if ($client_mode && !$client_bridge_signer_ready) {
            $actions_needed[] = 'PREMIUM LIVE AUDIO: This client must have VMO_CLIENT_MODE true, a valid VMO_MASTER_PROXY_URL, vmo_license_key, and vmo_installation_id so the Kinsta master can sign media/listen bridge requests.';
        }
        if (!empty($checks['latest_premium_bridge_error']) && stripos((string) $checks['latest_premium_bridge_error'], 'Invalid URL') !== false) {
            $actions_needed[] = 'PREMIUM LIVE AUDIO: Twilio rejected the Premium Stream URL as invalid. The Stream URL must be the clean bridge endpoint; signed call data belongs in Twilio <Parameter> values.';
        }
        if (!$client_mode && $media_secret === '') {
            $actions_needed[] = 'PREMIUM LIVE AUDIO: Set Bridge Media Secret to the same value as VMO_MEDIA_SECRET in /var/www/vmo-bridge/.env.';
        }
        if (!$client_mode && $listen_secret === '') {
            $actions_needed[] = 'PREMIUM LIVE AUDIO: Set Bridge Listen Secret to the same value as VMO_LISTEN_SECRET in /var/www/vmo-bridge/.env.';
        }
        if ($live_number_routing === 'trunk') {
            $actions_needed[] = 'PREMIUM LIVE AUDIO: Click Remove Legacy SIP Routing in VMO Settings so Twilio sends inbound calls to the normal VMO Voice URL webhook.';
        }
    } elseif (empty($rt_project) || empty($rt_secret)) {
        $actions_needed[] = 'LIVE AUDIO LISTEN: Go to WP Admin → VMO Settings and enter your OpenAI Realtime Project ID (proj_...) and Webhook Secret (whsec_...). Without these, Listen will only show a text transcript.';
    } elseif (!$rt_project_valid) {
        $actions_needed[] = 'LIVE AUDIO LISTEN: Replace the current OpenAI Realtime Project ID with the exact proj_... value from OpenAI Project → General. Other OpenAI ids and names will make SIP fail before any webhook is sent.';
    } elseif (empty($project_access['valid'])) {
        $actions_needed[] = 'LIVE AUDIO LISTEN: The configured OpenAI API key could not access the configured project. Make sure the API key, project id, and webhook secret all belong to the same OpenAI project. If OpenAI support asks, use request id ' . (!empty($project_access['openai_request_id']) ? $project_access['openai_request_id'] : '(not returned)') . '.';
    }
    if (!$legacy_sip_enabled) {
        // Old SIP trunk setup is intentionally hidden in WebSocket mode.
    } elseif (empty($trunk_sid)) {
        $actions_needed[] = 'LIVE AUDIO LISTEN: Click "Auto-Configure Realtime SIP Trunk" in VMO Settings so Twilio routes inbound calls through the platform SIP trunk instead of the legacy voice webhook path.';
    } elseif ($number_on_trunk === false) {
        $actions_needed[] = 'LIVE AUDIO LISTEN: This phone number is not on the platform SIP trunk. Re-run the SIP trunk setup/backfill so inbound calls can reach the realtime bridge.';
    } elseif ($number_on_trunk === true && $live_number_routing !== 'trunk') {
        $actions_needed[] = 'LIVE AUDIO LISTEN: Twilio still shows this number routing via the legacy voice webhook path instead of TrunkSid. Re-run the realtime SIP trunk setup so the number itself is updated to the platform trunk.';
    } elseif ($number_on_trunk === true && !$has_recent_webhook_hit) {
        $actions_needed[] = 'LIVE AUDIO LISTEN: The number is on the realtime trunk, but OpenAI has never hit the realtime webhook for this environment yet. If test calls still never reach the webhook, verify OpenAI SIP enrollment and webhook registration.';
    }
    if (!$legacy_sip_enabled && $voice_quality_tier === 'premium' && $bridge_url_valid && $bridge_auth_ready && $listen_auth_ready && !$latest_premium_bridge_verified) {
        $actions_needed[] = 'PREMIUM LIVE AUDIO: Place one fresh inbound test call after saving Premium mode. If this remains not_verified, open the Latest Premium Bridge Trace row; it will show whether the voice webhook skipped the bridge or emitted bridge TwiML.';
    }
    if ($legacy_sip_enabled && $recent_realtime_message !== '' && stripos($recent_realtime_message, 'SipResponseCode=400') !== false && !$has_recent_webhook_hit) {
        $actions_needed[] = !empty($project_access['valid'])
            ? 'LIVE AUDIO LISTEN: OpenAI SIP returned 400 before any webhook fired, but the API key can access this project. The remaining likely causes are that this same project is missing the realtime.call.incoming webhook or is not enabled for Realtime SIP.'
            : 'LIVE AUDIO LISTEN: OpenAI SIP returned 400 before any webhook fired. First fix the API key/project mismatch above; if that passes and 400 remains, verify the realtime.call.incoming webhook and SIP enablement on this project.';
    } elseif ($legacy_sip_enabled && $recent_realtime_message !== '' && stripos($recent_realtime_message, 'SIP <Dial> to OpenAI Realtime FAILED') !== false && !$has_recent_webhook_hit) {
        $actions_needed[] = 'LIVE AUDIO LISTEN: The most recent realtime call reached Twilio, but OpenAI never answered the SIP invite. Verify OpenAI SIP enrollment/webhook configuration before expecting live audio listen to work.';
    }
    if (!empty($actions_needed)) {
        $checks['actions_needed'] = $actions_needed;
    }

    // 10. Settings page URLs for convenience
    $checks['settings_urls'] = array(
        'dashboard' => admin_url('admin.php?page=vmo-ai-dashboard'),
        'billing' => admin_url('admin.php?page=vmo-my-plan'),
    );

    wp_send_json_success(array('checks' => $checks));
});

// AJAX: outbound call from dashboard dialer
add_action('wp_ajax_vmo_outbound_call', function() {
    // Start a scoped output buffer to capture any stray PHP notices/warnings
    // from the Twilio API call or DB operations. We intentionally do NOT kill
    // existing buffers (the sd_ajax gateway proxy relies on its own OB wrapper).
    ob_start();

    if (!current_user_can('read')) {
        ob_end_clean();
        wp_send_json_error(array('message' => 'Forbidden'), 403);
    }
    // Accept either admin or builder nonces (compat with dashboard helpers)
    if (!function_exists('vmo_verify_admin_security') || !vmo_verify_admin_security()) {
        ob_end_clean();
        wp_send_json_error(array('message' => 'Security check failed'), 403);
    }
    $to = sanitize_text_field($_POST['to'] ?? '');
    if (!$to) {
        ob_end_clean();
        wp_send_json_error(array('message' => 'Missing destination'));
    }

    // Auto-load class if missing
    if (!class_exists('VMO_Twilio_Enhanced_Client')) {
        $path = plugin_dir_path(__FILE__) . 'includes/class-vmo-twilio-enhanced-client.php';
        if (file_exists($path)) require_once $path;
    }

    if (!class_exists('VMO_Twilio_Enhanced_Client')) {
        ob_end_clean();
        wp_send_json_error(array('message' => 'Twilio unavailable'));
    }
    try {
        $client = new VMO_Twilio_Enhanced_Client();
        $result = $client->make_outbound_call($to);
        // Discard any stray output captured during the API/DB calls
        ob_end_clean();
        if (is_wp_error($result)) wp_send_json_error(array('message' => $result->get_error_message()));
        wp_send_json_success($result);
    } catch (Throwable $e) {
        ob_end_clean();
        if (defined('WP_DEBUG') && WP_DEBUG) error_log('[VMO Outbound Call] Exception: ' . $e->getMessage());
        wp_send_json_error(array('message' => 'Call error: ' . $e->getMessage()));
    }
});

// AJAX: quick contacts for dialer picker (name + phone)
add_action('wp_ajax_vmo_get_quick_contacts', function() {
    if (!current_user_can('read')) {
        wp_send_json_error(array('message' => 'Forbidden'), 403);
    }
    if (!function_exists('vmo_verify_admin_security') || !vmo_verify_admin_security()) {
        wp_send_json_error(array('message' => 'Security check failed'), 403);
    }

    global $wpdb;
    $user_id = get_current_user_id();
    $contacts_table = $wpdb->prefix . 'vmo_contacts';
    $limit = isset($_POST['limit']) ? max(1, min(200, intval($_POST['limit']))) : 100;
    $search = isset($_POST['search']) ? sanitize_text_field(wp_unslash($_POST['search'])) : '';

    // Ensure table exists
    $exists = vmo_table_exists($contacts_table);
    if (!$exists) {
        wp_send_json_success(array('contacts' => array()));
    }

    if (!empty($search)) {
        $pattern = '%' . $wpdb->esc_like($search) . '%';
        $rows = $wpdb->get_results($wpdb->prepare(
            "SELECT * FROM {$contacts_table}
             WHERE user_id = %d AND phone IS NOT NULL AND phone <> ''
             AND (COALESCE(full_name, CONCAT(COALESCE(first_name,''),' ',COALESCE(last_name,'')), '') LIKE %s OR email LIKE %s OR phone LIKE %s OR company LIKE %s)
             ORDER BY COALESCE(full_name, CONCAT(COALESCE(first_name,''),' ',COALESCE(last_name,'')), '') ASC LIMIT %d",
            $user_id, $pattern, $pattern, $pattern, $pattern, $limit
        ), ARRAY_A);
    } else {
        $rows = $wpdb->get_results($wpdb->prepare(
            "SELECT * FROM {$contacts_table}
             WHERE user_id = %d AND phone IS NOT NULL AND phone <> ''
             ORDER BY updated_at DESC, created_at DESC LIMIT %d",
            $user_id, $limit
        ), ARRAY_A);
    }

    $contacts = array();
    foreach (($rows ?: array()) as $r) {
        $phone = preg_replace('/[^+0-9]/', '', $r['phone'] ?? '');
        if (empty($phone)) continue;
        $r_name = $r['full_name'] ?? '';
        if (empty($r_name)) { $r_name = trim(($r['first_name'] ?? '') . ' ' . ($r['last_name'] ?? '')); }
        $contacts[] = array(
            'id' => intval($r['id']),
            'name' => (string) $r_name,
            'phone' => $phone,
            'company' => (string) ($r['company'] ?: ''),
        );
    }

    wp_send_json_success(array('contacts' => $contacts));
});

// AJAX: Get live calls (active/ringing) for dashboard monitoring
add_action('wp_ajax_vmo_get_live_calls', function() {
    try {
        // Clean any buffered output (wpdb errors etc.) BEFORE any logic
        while (ob_get_level() > 0) {
            ob_end_clean();
        }

        if (!current_user_can('read')) {
            error_log('[VMO] vmo_get_live_calls: Permission denied - user cannot read');
            wp_send_json_error(array('message' => 'Forbidden'), 200);
        }
        if (!vmo_verify_admin_security()) {
            $sec_provided = isset($_POST['security']) || isset($_POST['nonce']);
            error_log('[VMO] vmo_get_live_calls: Security check failed - nonce provided: ' . ($sec_provided ? 'yes' : 'no'));
            wp_send_json_error(array('message' => 'Security check failed'), 200);
        }

        // Use Twilio Manager to get active calls
        if (!class_exists('VMO_Twilio_Manager')) {
            error_log('[VMO] vmo_get_live_calls: Twilio manager class not found');
            wp_send_json_error(array('message' => 'Twilio manager unavailable'), 200);
        }

        $manager = VMO_Twilio_Manager::get_instance();
        $user_id = get_current_user_id();
        
        $result = $manager->get_live_calls($user_id);
        
        if (is_wp_error($result)) {
            $msg = $result->get_error_message();
            error_log('[VMO] vmo_get_live_calls returned error: ' . $msg);
            wp_send_json_error(array('message' => $msg), 200);
        }
        
        // Should be array with 'calls' key
        if (!is_array($result)) {
            error_log('[VMO] vmo_get_live_calls returned non-array: ' . gettype($result));
            wp_send_json_error(array('message' => 'Invalid live calls response'), 200);
        }
        
        error_log('[VMO] vmo_get_live_calls success: ' . count($result['calls'] ?? array()) . ' calls');
        wp_send_json_success($result);
    } catch (Throwable $t) {
        // Never let background polling produce a 500 flood.
        while (ob_get_level() > 0) {
            ob_end_clean();
        }
        error_log('[VMO] vmo_get_live_calls exception: ' . $t->getMessage() . ' in ' . $t->getFile() . ':' . $t->getLine());
        wp_send_json_error(array('message' => 'Live calls unavailable: ' . $t->getMessage()), 200);
    }
});
add_action('wp_ajax_vmo_sd_phone_live', function() {
    do_action('wp_ajax_vmo_get_live_calls');
});

// AJAX: Call action (join, listen, whisper, barge)
add_action('wp_ajax_vmo_call_action', function() {
    try {
        // Clean any buffered output BEFORE any logic
        while (ob_get_level() > 0) {
            ob_end_clean();
        }

        // The listen/whisper path can make many Twilio API calls with retry
        // loops (ensure_live_supervision_conference). Extend PHP's execution
        // time so the script doesn't hit max_execution_time and crash with a
        // non-JSON fatal error that the try/catch(Throwable) can't intercept.
        @set_time_limit(120);

        if (!current_user_can('read')) {
            wp_send_json_error(array('message' => 'Forbidden'), 403);
        }
        if (!vmo_verify_admin_security()) {
            wp_send_json_error(array('message' => 'Security check failed'), 403);
        }

        $action_type = sanitize_text_field($_POST['action_type'] ?? '');
        if ($action_type === 'take_over') {
            $action_type = 'join';
        }
        $call_sid = sanitize_text_field($_POST['call_sid'] ?? '');
        $conference_sid = sanitize_text_field($_POST['conference_sid'] ?? '');
        $child_sid = sanitize_text_field($_POST['child_sid'] ?? '');
        $user_id = get_current_user_id();

        // ── Child → Parent SID resolution (before routing) ──
        // When the browser rings via REST API, the SDK only knows the child
        // leg SID. Resolve the parent SID from the reverse transient that
        // track_child_ring_call() stores during the voice webhook.
        if (empty($call_sid) && !empty($child_sid)) {
            $parent_sid = get_transient('vmo_child_ring_parent_' . sanitize_key($child_sid));
            if (is_string($parent_sid) && $parent_sid !== '') {
                $call_sid = sanitize_text_field($parent_sid);
                error_log("[VMO vmo_call_action] Resolved parent SID from child {$child_sid}: {$call_sid}");
            }
        }

        if (!in_array($action_type, array('join', 'listen', 'whisper', 'barge', 'resume_ai', 'take_over', 'hold', 'unhold', 'end', 'hangup'), true)) {
            wp_send_json_error(array('message' => 'Invalid action type: ' . $action_type));
        }

        if (empty($call_sid) && empty($child_sid)) {
            wp_send_json_error(array('message' => 'Missing call SID'));
        }

        $has_realtime_router = class_exists('VMO_Realtime_Call_Manager') && method_exists('VMO_Realtime_Call_Manager', 'get_instance');
        $has_twilio_manager = class_exists('VMO_Twilio_Manager');
        if (!$has_realtime_router && !$has_twilio_manager) {
            wp_send_json_error(array('message' => 'Call control backend unavailable'), 500);
        }

        $via = sanitize_text_field($_POST['via'] ?? 'auto');
        
        error_log("[VMO] vmo_call_action called: action_type={$action_type}, call_sid={$call_sid}, child_sid={$child_sid}, via={$via}, user_id={$user_id}");

        // ── Clear ring signal immediately when a call action fires ──
        // The user has acknowledged the call (join, listen, etc.). Stop the
        // ring_now signal so the frontend polling loop won't keep ringing.
        if ($call_sid !== '' && $user_id > 0 && $has_twilio_manager) {
            $ring_key = 'vmo_ring_uid_' . $user_id;
            $ring_data = get_transient($ring_key);
            if (is_array($ring_data) && ((string) ($ring_data['call_sid'] ?? '')) === $call_sid) {
                delete_transient($ring_key);
                error_log("[VMO] Cleared ring signal for user {$user_id} call {$call_sid} after {$action_type}");
            }
        }

        if ($has_realtime_router) {
            // Capture and discard any stray HTML output (e.g. wpdb error <div>)
            // emitted by underlying queries so we always return clean JSON.
            ob_start();
            $result = VMO_Realtime_Call_Manager::get_instance()->route_call_action(
                $action_type,
                $call_sid,
                $conference_sid,
                $via,
                $child_sid,
                $user_id
            );
            $stray = ob_get_clean();
            if ($stray !== '' && trim($stray) !== '') {
                error_log('[VMO vmo_call_action] Discarded stray output (' . strlen($stray) . ' bytes): ' . substr($stray, 0, 500));
            }
            if (is_array($result) && isset($result['status_code'])) {
                unset($result['status_code']);
            }
        } else {
            ob_start();
            $manager = VMO_Twilio_Manager::get_instance();
            $result = $manager->call_action($action_type, $call_sid, $conference_sid, $user_id, $via, $child_sid);
            $stray = ob_get_clean();
            if ($stray !== '' && trim($stray) !== '') {
                error_log('[VMO vmo_call_action] Discarded stray output (' . strlen($stray) . ' bytes): ' . substr($stray, 0, 500));
            }
        }
        
        if (is_wp_error($result)) {
            $error_msg = $result->get_error_message();
            error_log("[VMO] vmo_call_action error: {$error_msg}");
            wp_send_json_error(array('message' => $error_msg), 200);
        }
        
        // Ensure result is an array with expected keys
        $success_result = is_array($result) ? $result : array();

        // If the router returned success:false (array, not WP_Error),
        // send it as an error so the JS sees {success:false, data:{message:…}}.
        if (is_array($success_result) && isset($success_result['success']) && $success_result['success'] === false) {
            $error_msg = $success_result['message'] ?? 'Call control failed';
            error_log("[VMO] vmo_call_action router error: {$error_msg}");
            wp_send_json_error(array('message' => $error_msg), 200);
        }

        error_log("[VMO] vmo_call_action success: " . wp_json_encode($success_result));
        wp_send_json_success($success_result);
    } catch (Throwable $t) {
        while (ob_get_level() > 0) {
            ob_end_clean();
        }
        $msg = $t->getMessage();
        error_log('[VMO ajax vmo_call_action] ' . $msg . ' in ' . $t->getFile() . ':' . $t->getLine());
        error_log('[VMO ajax vmo_call_action trace] ' . $t->getTraceAsString());
        wp_send_json_error(array('message' => 'Call control failed: ' . $msg), 200);
    }
});
add_action('wp_ajax_vmo_sd_phone_control', function() {
    do_action('wp_ajax_vmo_call_action');
});

// AJAX: Generate Twilio browser-calling token
add_action('wp_ajax_vmo_twilio_browser_token', function() {
    if (!current_user_can('read')) {
        wp_send_json_error(array('message' => 'Forbidden'), 403);
    }
    if (!vmo_verify_admin_security()) {
        wp_send_json_error(array('message' => 'Security check failed'), 403);
    }
    if (class_exists('VMO_Twilio_Manager')) {
        $manager = VMO_Twilio_Manager::get_instance();
        $token = $manager->generate_browser_token(get_current_user_id());
        if (!empty($token)) {
            wp_send_json_success(array('token' => $token, 'identity' => 'vmo_user_' . get_current_user_id()));
        }
        wp_send_json_error(array('message' => 'Could not generate browser token'));
    }
    wp_send_json_error(array('message' => 'Twilio manager unavailable'));
});
add_action('wp_ajax_vmo_sd_phone_token', function() {
    do_action('wp_ajax_vmo_twilio_browser_token');
});

// AJAX: End call
add_action('wp_ajax_vmo_end_call', function() {
    if (!current_user_can('read')) {
        wp_send_json_error(array('message' => 'Forbidden'), 403);
    }
    if (!vmo_verify_admin_security()) {
        wp_send_json_error(array('message' => 'Security check failed'), 403);
    }
    
    $call_sid = sanitize_text_field($_POST['call_sid'] ?? '');
    $child_sid = sanitize_text_field($_POST['child_sid'] ?? '');
    $parent_sid = sanitize_text_field($_POST['parent_sid'] ?? '');
    $force_active = !empty($_POST['force_active']);
    if (empty($call_sid) && empty($child_sid) && empty($parent_sid) && !$force_active) {
        wp_send_json_error(array('message' => 'Missing call SID'));
    }
    
    if (class_exists('VMO_Twilio_Manager')) {
        $manager = VMO_Twilio_Manager::get_instance();
        // Pass extra info so end_call can INSERT a record if the webhook never created one
        $extra = array(
            'user_id'       => get_current_user_id(),
            'direction'     => sanitize_text_field($_POST['direction'] ?? 'outbound'),
            'caller_number' => sanitize_text_field($_POST['caller_number'] ?? ''),
            'child_sid'     => $child_sid,
            'parent_sid'    => $parent_sid,
            'reason'        => sanitize_key($_POST['reason'] ?? 'manual'),
            'status'        => sanitize_key($_POST['status'] ?? 'completed'),
            'force_active'  => $force_active ? 1 : 0,
        );
        $result = $manager->end_call($call_sid, $extra);
        if (is_wp_error($result)) {
            wp_send_json_error(array('message' => $result->get_error_message()));
        }
        wp_send_json_success($result);
    }
    
    wp_send_json_error(array('message' => 'Twilio manager unavailable'));
});
add_action('wp_ajax_vmo_sd_phone_end', function() {
    do_action('wp_ajax_vmo_end_call');
});

// AJAX: Get live AI transcript for a call
add_action('wp_ajax_vmo_get_call_transcript', function() {
    if (!current_user_can('read')) {
        wp_send_json_error(array('message' => 'Forbidden'), 403);
    }
    if (!vmo_verify_admin_security()) {
        wp_send_json_error(array('message' => 'Security check failed'), 403);
    }
    
    $call_sid = sanitize_text_field($_POST['call_sid'] ?? '');
    if (empty($call_sid)) {
        wp_send_json_error(array('message' => 'Missing call SID'));
    }
    
    if (class_exists('VMO_Twilio_Manager')) {
        $manager = VMO_Twilio_Manager::get_instance();
        $result = $manager->get_call_transcript($call_sid, get_current_user_id());
        wp_send_json_success($result);
    }
    
    wp_send_json_error(array('message' => 'Twilio manager unavailable'));
});
add_action('wp_ajax_vmo_sd_phone_transcript', function() {
    do_action('wp_ajax_vmo_get_call_transcript');
});
add_action('wp_ajax_vmo_sd_phone_outbound', function() {
    do_action('wp_ajax_vmo_outbound_call');
});
add_action('wp_ajax_vmo_sd_phone_status', function() {
    do_action('wp_ajax_vmo_get_phone_status');
});
add_action('wp_ajax_vmo_sd_phone_prefs', function() {
    do_action('wp_ajax_vmo_get_phone_prefs');
});
add_action('wp_ajax_vmo_sd_phone_save_prefs', function() {
    do_action('wp_ajax_vmo_save_phone_prefs');
});
add_action('wp_ajax_vmo_sd_phone_analytics', function() {
    do_action('wp_ajax_vmo_get_phone_analytics');
});
add_action('wp_ajax_vmo_sd_phone_contacts', function() {
    do_action('wp_ajax_vmo_get_quick_contacts');
});
add_action('wp_ajax_vmo_sd_phone_history', function() {
    do_action('wp_ajax_vmo_get_call_history');
});
add_action('wp_ajax_vmo_sd_phone_recordings', function() {
    do_action('wp_ajax_vmo_get_call_recordings');
});
add_action('wp_ajax_vmo_sd_phone_details', function() {
    do_action('wp_ajax_vmo_get_call_details');
});
add_action('wp_ajax_vmo_sd_dashboard_stats', function() {
    do_action('wp_ajax_vmo_dashboard_stats');
});

// ═══════════════════════════════════════════════════════════════════════════════
// VMO REALTIME SESSION — Mints an ephemeral OpenAI Realtime token so the browser
// can open a WebRTC session with built-in barge-in. The master OPENAI_API_KEY
// NEVER reaches the client. Returns short-lived client_secret + model + ICE.
// Wired by assets/vmo-realtime.js (VMORealtimeSession). Listed aliases support
// both master and SD/client gateway paths. Premium Live Voice has two modes at
// product level: Default Chat Voice stays on STT/text/TTS, while this Realtime
// path is used only when the user selects Premium Live Voice. [VMO_REALTIME_TOKEN_ENDPOINT]
// ═══════════════════════════════════════════════════════════════════════════════
add_action('wp_ajax_vmo_realtime_session', 'vmo_handle_realtime_session');
add_action('wp_ajax_vmo_sd_realtime_session', 'vmo_handle_realtime_session');
add_action('wp_ajax_vmo_realtime_sdp', 'vmo_handle_realtime_sdp');
add_action('wp_ajax_vmo_sd_realtime_sdp', 'vmo_handle_realtime_sdp');

if (!function_exists('vmo_realtime_resolve_api_key')) {
function vmo_realtime_resolve_api_key() {
    $api_key = '';
    if (function_exists('vmo_resolve_openai_api_key')) {
        $resolved = vmo_resolve_openai_api_key();
        if (!empty($resolved['valid'])) { $api_key = (string) $resolved['key']; }
    }
    if ($api_key === '') {
        $api_key = trim((string) get_option('vmo_openai_api_key', ''));
    }
    if ($api_key === '' && defined('VMO_OPENAI_API_KEY')) {
        $api_key = trim((string) VMO_OPENAI_API_KEY);
    }
    return $api_key;
}
}


if (!function_exists('vmo_realtime_safety_identifier')) {
function vmo_realtime_safety_identifier($user_id) {
    // OpenAI-Safety-Identifier has a hard 64 character limit. Keep this
    // anonymous, stable per WP install/user, and safely under the limit.
    $raw = (string) $user_id . '|' . wp_salt('auth');
    return 'vmo-' . substr(hash('sha256', $raw), 0, 60);
}
}

if (!function_exists('vmo_realtime_default_model')) {
function vmo_realtime_default_model() {
    // Cost-safe Premium Live Voice default. OpenAI's current docs list GPT-Realtime mini
    // as the cost-efficient realtime speech model; older gpt-4o realtime preview IDs
    // are not valid on the current Realtime API and cause model_not_found fallbacks.
    return 'gpt-realtime-mini';
}
}

if (!function_exists('vmo_realtime_is_supported_model_name')) {
function vmo_realtime_is_supported_model_name($model) {
    $model = is_string($model) ? trim($model) : '';
    if ($model === '') { return false; }
    return (bool) preg_match('/^gpt-realtime(?:-[A-Za-z0-9.]+)?$/i', $model);
}
}

if (!function_exists('vmo_realtime_normalize_model')) {
function vmo_realtime_normalize_model($model) {
    $model = is_string($model) ? trim($model) : '';

    // Migrate old/nonexistent internal aliases to currently documented model
    // IDs that support realtime audio over WebRTC.
    $legacy = strtolower($model);
    if ($legacy === '' || $legacy === 'gpt-realtime' || $legacy === 'gpt-realtime-mini' || $legacy === 'gpt-4o-mini-realtime-preview' || $legacy === 'gpt-4o-realtime-preview') {
        return vmo_realtime_default_model();
    }
    if ($legacy === 'gpt-realtime-2') {
        return 'gpt-realtime-2';
    }
    if ($legacy === 'gpt-realtime-1.5') {
        return 'gpt-realtime-1.5';
    }

    if (!vmo_realtime_is_supported_model_name($model)) {
        return vmo_realtime_default_model();
    }
    return $model;
}
}

if (!function_exists('vmo_realtime_model_candidates')) {
function vmo_realtime_model_candidates($preferred = '') {
    $preferred = vmo_realtime_normalize_model($preferred);
    $candidates = [$preferred, vmo_realtime_default_model(), 'gpt-realtime-1.5', 'gpt-realtime-2'];
    $out = [];
    foreach ($candidates as $candidate) {
        $candidate = vmo_realtime_normalize_model($candidate);
        if ($candidate !== '' && !in_array($candidate, $out, true)) {
            $out[] = $candidate;
        }
    }
    return $out;
}
}

if (!function_exists('vmo_realtime_is_model_unavailable_response')) {
function vmo_realtime_is_model_unavailable_response($code, $body) {
    $body = (string) $body;
    $json = json_decode($body, true);
    $err_code = is_array($json) ? (string) ($json['error']['code'] ?? '') : '';
    $msg = is_array($json) ? (string) ($json['error']['message'] ?? '') : $body;
    if ($err_code === 'model_not_found') { return true; }
    return (bool) preg_match('/model .*does not exist|do not have access|not have access to/i', $msg);
}
}

if (!function_exists('vmo_realtime_sanitize_session_config')) {
function vmo_realtime_sanitize_session_config($session_config) {
    // VMO_REALTIME_SESSION_CONFIG_SANITIZER_v2
    // Convert legacy Premium Voice config fields to the current Realtime session
    // schema before either the SDP call or browser session.update uses them.
    if (!is_array($session_config)) {
        $session_config = [];
    }
    if (isset($session_config['max_response_output_tokens']) && !isset($session_config['max_output_tokens'])) {
        $session_config['max_output_tokens'] = $session_config['max_response_output_tokens'];
    }
    unset($session_config['max_response_output_tokens']);
    if (empty($session_config['max_output_tokens'])) {
        $session_config['max_output_tokens'] = 4096;
    }
    if (empty($session_config['type'])) {
        $session_config['type'] = 'realtime';
    }
    return $session_config;
}
}

if (!function_exists('vmo_realtime_build_session_config')) {
function vmo_realtime_build_session_config($context, $voice, $model, $user_id) {
    $jarvis_onboarding_ready = $context !== 'jarvis'
        || (function_exists('vmo_jarvis_has_connected_consultation_website') && vmo_jarvis_has_connected_consultation_website($user_id));

    $instructions = '';
    switch ($context) {
        case 'consultation':
            $instructions = "You are Jarvis, the VMO business consultant. Conduct a warm, focused discovery conversation. Ask one question at a time, listen carefully, and confirm understanding. The user is a business owner; assume they are not technical. If interrupted, stop immediately and listen.";
            break;
        case 'receptionist':
            $instructions = "You are the AI receptionist for the user's business. Greet callers warmly, capture their reason for calling, and offer to take a message or schedule a callback. Keep responses short — this is a phone conversation.";
            break;
        case 'jarvis':
        default:
            $instructions = $jarvis_onboarding_ready
                ? "You are Jarvis, the user's AI business operator inside the VMO dashboard. Be concise and proactive by default, but when the user asks for strategy, KPIs, business updates, diagnostics, rankings, traffic, conversions, revenue, campaigns, or detailed recommendations, give enough detail to be genuinely useful instead of cutting yourself short. When the user asks about business state, KPIs, recent actions, or revenue, call the appropriate tool to fetch fresh data instead of guessing. If the user interrupts while you are talking, stop immediately, listen to the interruption, preserve the context of what you were explaining, then answer the combined/latest intent."
                : "You are Jarvis in PRE-CONSULTATION SUPPORT MODE inside the VMO dashboard. You may answer general VMO/plugin questions, explain features, troubleshoot navigation, explain why consultation matters, and tell the user where to click. Be concise by default, but provide detailed help when the user clearly needs it. Do not generate, design, refine, edit, publish, or claim to set up a website, page, section, email system, phone system, SEO system, payments, shipping, CRM, KPIs, domains, or hosting. If the user asks you to build, refine, set up, connect, publish, send, or operate anything that depends on the business foundation, tell them to click the red Business Wizard button at the top to begin the Business Consultation first. If interrupted, stop immediately and listen.";
            break;
    }

    $should_apply_platform_faq = false;
    if (function_exists('vmo_get_platform_faq_grounding_text')) {
        if ($context === 'receptionist' && function_exists('vmo_platform_faq_should_ground_current_site')) {
            $should_apply_platform_faq = vmo_platform_faq_should_ground_current_site();
        } elseif ($context === 'jarvis' && !$jarvis_onboarding_ready) {
            $should_apply_platform_faq = true;
        }
    }
    if ($should_apply_platform_faq) {
        $faq_grounding = vmo_get_platform_faq_grounding_text('realtime_' . $context, 9500);
        if ($faq_grounding !== '') {
            $instructions .= "\n\n" . $faq_grounding;
        }
    }

    // ── CAPABILITY + LIVE-STATE PARITY WITH DEFAULT JARVIS ──────────────────
    // Premium Live Voice (Realtime/WebRTC) must NEVER be a dumber, less aware,
    // or more limited agent than default-voice Jarvis. It is the SAME Jarvis —
    // just faster and more natural sounding. Inject (a) the exact same live
    // system context default Jarvis receives, and (b) an explicit capability
    // manifest plus the jarvis_command tool contract, so premium voice stops
    // denying real capabilities or inventing false limitations.
    if ($context === 'jarvis' && $jarvis_onboarding_ready) {
        if (function_exists('vmo_build_jarvis_system_context')) {
            $live_state = vmo_build_jarvis_system_context($user_id);
            if (is_string($live_state) && $live_state !== '') {
                $instructions .= "\n\nLIVE BUSINESS STATE (authoritative — never contradict this):\n" . $live_state;
            }
        }
        $instructions .= "\n\nYOUR CAPABILITIES (you genuinely have these — never say you cannot do them):"
            . "\n- Email operator: read, summarize, count, and search the user's inbox; tell them who emailed, the subject, and a summary (or full text on request); draft a reply; send the reply or save it as a draft; mark messages read/done; sync the mailbox."
            . "\n- Navigation: open any dashboard area (Overview, Strategic, Reports, Phone, SMS, Email/Inbox, Payments, Shipping/Orders, SEO, CRM/Contacts, Promotions) and open Compose, the Power Dialer, or SMS compose."
            . "\n- Business intelligence: report plan, token balance, KPIs, revenue, leads/customers, tasks, published sites, SEO status, phone/SMS/email provisioning, and recent automated actions."
            . "\nTO ACT, CALL THE jarvis_command TOOL with a plain-English `command` string describing exactly what the user asked (e.g. \"show my unread emails\", \"summarize emails from John\", \"reply to that email saying I'll call tomorrow\", \"send the reply\", \"go to CRM\"). Use the tool's returned data as the source of truth and speak it back naturally. When the user asks you to compose or reply, read your drafted wording back to them and let them correct it by voice before you send. Never fabricate email contents, counts, senders, KPIs, or business state — always call a tool to fetch real data first.";
    }
    $instructions = apply_filters('vmo_realtime_instructions', $instructions, $context);

    $tools = [
        [
            'type' => 'function',
            'name' => 'get_business_state',
            'description' => 'Fetch a snapshot of the user\'s current business state: setup progress, active goals, pending actions.',
            'parameters' => ['type' => 'object', 'properties' => new stdClass(), 'required' => []],
        ],
        [
            'type' => 'function',
            'name' => 'get_kpi_snapshot',
            'description' => 'Fetch the latest KPI snapshot: revenue, leads, traffic, and trend deltas.',
            'parameters' => ['type' => 'object', 'properties' => new stdClass(), 'required' => []],
        ],
        [
            'type' => 'function',
            'name' => 'get_recent_actions',
            'description' => 'Fetch a list of the most recent automated/agent actions taken on behalf of the business.',
            'parameters' => [
                'type' => 'object',
                'properties' => [ 'limit' => ['type' => 'integer', 'description' => 'Max actions to return (default 10)'] ],
                'required' => [],
            ],
        ],
    ];

    // Parity tool: gives Premium Live Voice the SAME action surface as
    // default-mode Jarvis (email read/summarize/reply/send/draft/mark/sync, tab
    // navigation, compose/dialer/SMS) by routing through the shared
    // VMO_Integration_Hub::process_jarvis_command() processor.
    if ($context === 'jarvis' && $jarvis_onboarding_ready) {
        $tools[] = [
            'type' => 'function',
            'name' => 'jarvis_command',
            'description' => 'Execute a Jarvis operator command on the user\'s VMO workspace. Use for ALL email actions (read, summarize, count, search by sender, reply, send, save draft, mark read/done, sync inbox) and for navigating the dashboard or opening compose/dialer/SMS. Pass the user\'s request verbatim as a plain-English command. Returns real data — always call this before stating email counts, senders, contents, or business state.',
            'parameters' => [
                'type' => 'object',
                'properties' => [
                    'command' => ['type' => 'string', 'description' => 'The plain-English command, e.g. "show my unread emails", "summarize emails from Sarah", "reply to that email and say I will call tomorrow", "send the reply", "go to CRM".'],
                ],
                'required' => ['command'],
            ],
        ];
    }
    $tools = apply_filters('vmo_realtime_tools', $tools, $context);

    return vmo_realtime_sanitize_session_config([
        'type'         => 'realtime',
        'model'        => $model,
        'instructions' => $instructions,
        // Relaxed response allowance for Jarvis Premium Voice. The prompt still
        // tells Jarvis to be concise when appropriate, but the model is no longer
        // artificially forced into tiny answers when the user needs detail.
        'max_output_tokens' => 4096,
        'tools'        => $tools,
        'tool_choice'  => 'auto',
        'audio'        => [
            'output' => [
                'voice' => $voice,
            ],
            'input' => [
                // Live user-speech transcription so the user's words appear as
                // captions as they speak (parity with default mode). We use the
                // broadly-available 'whisper-1' model — NOT 'gpt-realtime-whisper'
                // — because not every project has access to the dedicated
                // realtime transcription model, and requesting an unavailable one
                // makes the whole WebRTC session fail. If even whisper-1 is
                // rejected, the JS client (see input_audio_transcription error
                // handler) drops this field and retries, so the voice session
                // still succeeds without captions rather than falling back to TTS.
                'transcription' => [
                    'model' => 'whisper-1',
                ],
                'turn_detection' => [
                    'type' => 'server_vad',
                    'threshold' => 0.22,
                    'prefix_padding_ms' => 220,
                    'silence_duration_ms' => 520,
                    // Client explicitly creates responses after speech_stopped so
                    // speaker echo does not make Jarvis answer itself.
                    'create_response' => true,
                    'interrupt_response' => true,
                ],
            ],
        ],
    ]);
}
}

if (!function_exists('vmo_realtime_is_client_mode')) {
function vmo_realtime_is_client_mode() {
    return (defined('VMO_CLIENT_MODE') && VMO_CLIENT_MODE === true)
        || get_option('vmo_quicklaunch_hosted_client') === '1'
        || get_option('vmo_client_distribution_channel') === 'quicklaunch_hosted'
        || (defined('VMO_CLIENT_DISTRIBUTION_CHANNEL') && VMO_CLIENT_DISTRIBUTION_CHANNEL === 'quicklaunch_hosted');
}
}

if (!function_exists('vmo_realtime_is_unmetered_master')) {
function vmo_realtime_is_unmetered_master($user_id = 0) {
    $user_id = (int) ($user_id ?: get_current_user_id());
    if (vmo_realtime_is_client_mode()) {
        return false;
    }
    if (defined('VMO_MASTER_VERSION') && VMO_MASTER_VERSION === true && !(defined('VMO_CLIENT_MODE') && VMO_CLIENT_MODE === true)) {
        return true;
    }
    // Master-site administrators are the plugin operators, not billable
    // customers. They hold all the real API keys and must be able to run
    // Jarvis Premium Live Voice without their personal token balance halting
    // the session mid-conversation. Without this, a master admin whose
    // vmo_token_balance is low gets metered and the realtime chat abruptly
    // ends after a minute or two — even though the master should never bill
    // its own operator. (Mirrors the master-admin TTS token bypass.)
    if ($user_id > 0 && user_can($user_id, 'manage_options')) {
        return true;
    }
    return $user_id > 0
        && function_exists('vmo_is_internal_unmetered_user')
        && vmo_is_internal_unmetered_user($user_id);
}
}

if (!function_exists('vmo_realtime_authorized_window_seconds')) {
function vmo_realtime_authorized_window_seconds($unmetered = false) {
    return $unmetered ? (4 * HOUR_IN_SECONDS) : 60;
}
}

if (!function_exists('vmo_realtime_master_usage_sync_url')) {
function vmo_realtime_master_usage_sync_url() {
    $proxy_base = defined('VMO_MASTER_PROXY_URL') && VMO_MASTER_PROXY_URL
        ? (string) VMO_MASTER_PROXY_URL
        : (string) get_option('vmo_master_proxy_url', 'https://getvmo.com/wp-json/vmo-builder/v1/proxy');
    $proxy_base = trim($proxy_base);
    if ($proxy_base === '') {
        return '';
    }
    $root = preg_replace('#/proxy/?$#', '', rtrim($proxy_base, '/'));
    return trailingslashit($root) . 'usage/sync';
}
}

if (!function_exists('vmo_realtime_sync_master_usage')) {
function vmo_realtime_sync_master_usage($tokens, $session_id, $context, $from_minute, $to_minute) {
    if (!vmo_realtime_is_client_mode()) {
        return ['success' => false, 'message' => 'Not in client mode'];
    }
    $tokens = (int) $tokens;
    if ($tokens < 1) {
        return ['success' => true, 'token_balance' => null];
    }
    $license_key = (string) get_option('vmo_license_key', '');
    $installation_id = (string) get_option('vmo_installation_id', '');
    $url = vmo_realtime_master_usage_sync_url();
    if ($license_key === '' || $url === '') {
        return ['success' => false, 'message' => 'Missing master license or usage sync URL'];
    }

    $record_id = 'rt_' . md5($session_id . '|' . (int) $from_minute . '|' . (int) $to_minute . '|' . $tokens);
    $record = [
        'service'     => 'openai',
        'action'      => 'realtime_audio_min',
        'tokens_used' => $tokens,
        'cost_cents'  => 0,
        'metadata'    => [
            'record_id'   => $record_id,
            'session_id'  => $session_id,
            'context'     => $context,
            'from_minute' => (int) $from_minute,
            'to_minute'   => (int) $to_minute,
            'source'      => 'premium_live_voice_heartbeat',
        ],
        'created_at'  => current_time('mysql'),
    ];

    $response = wp_remote_post($url, [
        'timeout' => 20,
        'headers' => [
            'Content-Type'       => 'application/json',
            'X-VMO-License'      => $license_key,
            'X-VMO-Installation' => $installation_id,
            'X-VMO-Plan'         => (string) get_user_meta(get_current_user_id(), 'vmo_subscription_status', true),
        ],
        'body' => wp_json_encode(['records' => [$record]]),
    ]);
    if (is_wp_error($response)) {
        return ['success' => false, 'message' => $response->get_error_message()];
    }
    $code = (int) wp_remote_retrieve_response_code($response);
    $body = (string) wp_remote_retrieve_body($response);
    $json = json_decode($body, true);
    if ($code < 200 || $code >= 300 || !is_array($json) || empty($json['success'])) {
        return [
            'success' => false,
            'message' => is_array($json) && !empty($json['message']) ? (string) $json['message'] : substr($body, 0, 300),
            'http_code' => $code,
        ];
    }
    if (isset($json['token_balance'])) {
        update_user_meta(get_current_user_id(), 'vmo_token_balance', (int) $json['token_balance']);
    }
    return ['success' => true, 'token_balance' => $json['token_balance'] ?? null];
}
}


// VMO_REALTIME_SAVE_TURN_ENDPOINT_v1
// Premium Live Voice uses OpenAI Realtime/WebRTC directly over the data channel,
// so its turns do not pass through vmo_conversation_chat. Persist those user and
// assistant transcripts here so Premium and Default Voice share the same Jarvis
// history, long-term memory, and future context.
add_action('wp_ajax_vmo_realtime_save_turn', 'vmo_handle_realtime_save_turn');
if (!function_exists('vmo_handle_realtime_save_turn')) {
function vmo_handle_realtime_save_turn() {
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Forbidden'], 403);
    }
    $nonce = isset($_POST['security']) ? sanitize_text_field(wp_unslash($_POST['security'])) : '';
    $nonce2 = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : '';
    $ok = false;
    if (function_exists('vmo_verify_admin_security')) {
        $ok = (bool) vmo_verify_admin_security();
    }
    if (!$ok) {
        $ok = ($nonce && wp_verify_nonce($nonce, 'vmo_builder_nonce'))
            || ($nonce2 && (wp_verify_nonce($nonce2, 'vmo_builder_nonce') || wp_verify_nonce($nonce2, 'vmo_admin_nonce')));
    }
    if (!$ok) {
        wp_send_json_error(['message' => 'Security check failed'], 403);
    }

    $role = isset($_POST['role']) ? sanitize_key((string) wp_unslash($_POST['role'])) : 'assistant';
    if ($role === 'ai') { $role = 'assistant'; }
    if (!in_array($role, ['user', 'assistant'], true)) { $role = 'assistant'; }

    $chat_type = isset($_POST['chat_type']) ? sanitize_key((string) wp_unslash($_POST['chat_type'])) : 'jarvis';
    if (!in_array($chat_type, ['jarvis', 'consultant'], true)) { $chat_type = 'jarvis'; }

    $content = isset($_POST['content']) ? wp_kses_post(wp_unslash($_POST['content'])) : '';
    $content = trim((string) $content);
    if ($content === '') {
        wp_send_json_success(['saved' => false, 'reason' => 'empty']);
    }

    global $wpdb;
    $table = $wpdb->prefix . 'vmo_chat_history';
    $wpdb->insert($table, [
        'user_id' => get_current_user_id(),
        'message_role' => $role,
        'message_content' => $content,
        'conversation_context' => 'premium_live_voice',
        'chat_type' => $chat_type,
        'created_at' => current_time('mysql'),
    ], ['%d', '%s', '%s', '%s', '%s', '%s']);

    if ($wpdb->last_error) {
        wp_send_json_error(['message' => 'Failed to save realtime turn', 'detail' => $wpdb->last_error], 500);
    }
    wp_send_json_success(['saved' => true]);
}
}

if (!function_exists('vmo_handle_realtime_session')) {
function vmo_handle_realtime_session() {
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Forbidden'], 403);
    }
    if (!function_exists('vmo_verify_admin_security') || !vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Security check failed'], 403);
    }

    $rt_user_id = get_current_user_id();
    $rt_minute_cost = function_exists('vmo_get_token_cost') ? (int) vmo_get_token_cost('realtime_audio_min') : 0;
    if ($rt_minute_cost < 1) { $rt_minute_cost = 1547; }
    $rt_unmetered = vmo_realtime_is_unmetered_master($rt_user_id);
    $rt_authorized_seconds = vmo_realtime_authorized_window_seconds($rt_unmetered);
    if (!$rt_unmetered && function_exists('vmo_can_user_use_cost_bearing_services')
        && !vmo_can_user_use_cost_bearing_services($rt_user_id, $rt_minute_cost)) {
        wp_send_json_error([
            'message' => 'Not enough VMO Credit to start a live voice session. Add a booster or top up to continue.',
            'code'    => 'insufficient_credit',
        ], 402);
    }

    $context = isset($_POST['context']) ? sanitize_key((string) $_POST['context']) : 'jarvis';
    if (!in_array($context, ['jarvis', 'consultation', 'receptionist'], true)) {
        $context = 'jarvis';
    }

    $voice = isset($_POST['voice']) ? sanitize_text_field((string) $_POST['voice']) : '';
    $voice = preg_replace('/[^a-zA-Z0-9_\-]/', '', $voice);
    if ($voice === '') { $voice = 'ash'; }

    $model = function_exists('vmo_get_model') ? vmo_get_model('realtime') : vmo_realtime_default_model();
    $model = vmo_realtime_normalize_model($model);
    $session_update = vmo_realtime_build_session_config($context, $voice, $model, $rt_user_id);

    $session_id = function_exists('wp_generate_uuid4') ? 'vmo_rt_' . wp_generate_uuid4() : 'vmo_rt_' . md5(uniqid('', true));
    $safety_id = vmo_realtime_safety_identifier($rt_user_id);

    set_transient('vmo_rt_cfg_' . md5($session_id), [
        'user_id' => (int) $rt_user_id,
        'context' => $context,
        'voice' => $voice,
        'model' => $model,
        'session_update' => $session_update,
        'minute_cost' => $rt_minute_cost,
        'unmetered' => $rt_unmetered,
        'authorized_seconds' => $rt_authorized_seconds,
        'safety_id' => $safety_id,
        'created' => time(),
    ], 15 * MINUTE_IN_SECONDS);

    wp_send_json_success([
        'session_id' => $session_id,
        'model' => $model,
        'voice' => $voice,
        'context' => $context,
        'sdp_action' => 'vmo_realtime_sdp',
        'master_routed' => true,
        'client_secret' => null,
        'authorized_until_seconds' => $rt_authorized_seconds,
        'metering_mode' => $rt_unmetered ? 'unmetered' : 'metered',
        'unmetered' => $rt_unmetered,
        'session_update' => $session_update,
    ]);
}
}

if (!function_exists('vmo_handle_realtime_sdp')) {
function vmo_handle_realtime_sdp() {
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Forbidden'], 403);
    }
    if (!function_exists('vmo_verify_admin_security') || !vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Security check failed'], 403);
    }

    $session_id = isset($_POST['session_id']) ? sanitize_text_field((string) $_POST['session_id']) : '';
    $offer_sdp = isset($_POST['offer_sdp']) ? (string) wp_unslash($_POST['offer_sdp']) : '';
    if ($session_id === '' || $offer_sdp === '' || strpos($offer_sdp, 'v=0') !== 0) {
        wp_send_json_error(['message' => 'Invalid Realtime SDP offer'], 400);
    }

    $cfg_key = 'vmo_rt_cfg_' . md5($session_id);
    $cfg = get_transient($cfg_key);
    if (!is_array($cfg) || (int) ($cfg['user_id'] ?? 0) !== (int) get_current_user_id()) {
        wp_send_json_error(['message' => 'Realtime session expired or does not belong to this user'], 410);
    }

    // VMO_REALTIME_RESTORE_SESSION_UPDATE_v2
    // Rehydrate the exact config minted by vmo_realtime_session. A previous build
    // referenced $session_update here without restoring it from the transient, which
    // could strip voice/VAD/tool settings during SDP proxy exchange.
    $session_update = isset($cfg['session_update']) && is_array($cfg['session_update'])
        ? vmo_realtime_sanitize_session_config($cfg['session_update'])
        : vmo_realtime_build_session_config((string) ($cfg['context'] ?? 'jarvis'), (string) ($cfg['voice'] ?? 'ash'), (string) ($cfg['model'] ?? vmo_realtime_default_model()), get_current_user_id());

    $api_key = vmo_realtime_resolve_api_key();
    if ($api_key === '' && vmo_realtime_is_client_mode()) {
        // Client/QuickLaunch sites do not store real provider keys. The outbound
        // request is intercepted by VMO_Client_Bootstrap and routed to the Kinsta
        // master, which injects the real OpenAI key. This placeholder is stripped
        // before the request leaves the client.
        $api_key = 'vmo-client-proxy';
    }
    if ($api_key === '') {
        wp_send_json_error(['message' => 'OpenAI API key not configured'], 500);
    }

    $model = vmo_realtime_normalize_model((string) ($cfg['model'] ?? vmo_realtime_default_model()));
    $safety_id = (string) ($cfg['safety_id'] ?? vmo_realtime_safety_identifier(get_current_user_id()));
    $safety_id = preg_replace('/[^A-Za-z0-9_\-]/', '', $safety_id);
    if ($safety_id === '' || strlen($safety_id) > 64) {
        $safety_id = vmo_realtime_safety_identifier(get_current_user_id());
    }
    $rt_minute_cost = (int) ($cfg['minute_cost'] ?? 1547);
    if ($rt_minute_cost < 1) { $rt_minute_cost = 1547; }
    $rt_unmetered = !empty($cfg['unmetered']) || vmo_realtime_is_unmetered_master(get_current_user_id());
    $rt_authorized_seconds = vmo_realtime_authorized_window_seconds($rt_unmetered);
    if (!$rt_unmetered && function_exists('vmo_can_user_use_cost_bearing_services')
        && !vmo_can_user_use_cost_bearing_services(get_current_user_id(), $rt_minute_cost)) {
        wp_send_json_error(['message' => 'Not enough VMO Credit to start Premium Live Voice.', 'code' => 'insufficient_credit'], 402);
    }
    $response = null;
    $answer_sdp = '';
    $code = 0;
    $model_errors = [];
    $chosen_model = $model;

    // In QuickLaunch/client mode this wp_remote_post is intercepted by
    // VMO_Client_Bootstrap and routed to the Kinsta master proxy. In master mode
    // it goes directly from Kinsta to OpenAI. Either way, the browser/Vultr
    // client never calls the provider REST endpoint or sees the real API key.
    foreach (vmo_realtime_model_candidates($model) as $candidate_model) {
        $target_url = 'https://api.openai.com/v1/realtime/calls';

        // Current Realtime GA unified WebRTC signaling expects multipart/form-data:
        //   sdp=<browser offer SDP>
        //   session=<JSON realtime session config including the model>
        // The previous raw application/sdp + ?model=... path was a preview-era shape
        // and caused model_not_found loops with current model IDs.
        $candidate_session = $session_update;
        $candidate_session['model'] = $candidate_model;
        $boundary = '----vmo-realtime-' . md5($session_id . '|' . $candidate_model . '|' . microtime(true));
        $multipart_body = '';
        $multipart_body .= '--' . $boundary . "\r\n";
        $multipart_body .= 'Content-Disposition: form-data; name="sdp"' . "\r\n\r\n";
        $multipart_body .= $offer_sdp . "\r\n";
        $multipart_body .= '--' . $boundary . "\r\n";
        $multipart_body .= 'Content-Disposition: form-data; name="session"' . "\r\n";
        $multipart_body .= 'Content-Type: application/json' . "\r\n\r\n";
        $multipart_body .= wp_json_encode($candidate_session) . "\r\n";
        $multipart_body .= '--' . $boundary . '--' . "\r\n";

        $response = wp_remote_post($target_url, [
            'timeout' => 30,
            'headers' => [
                'Authorization' => 'Bearer ' . $api_key,
                'Content-Type'  => 'multipart/form-data; boundary=' . $boundary,
                'Accept'        => 'application/sdp',
                'OpenAI-Safety-Identifier' => $safety_id,
            ],
            'body' => $multipart_body,
            'vmo_operation' => 'realtime_audio_min',
            'vmo_request_id' => $session_id . '_sdp_' . md5($candidate_model),
        ]);

        if (is_wp_error($response)) {
            wp_send_json_error(['message' => 'Realtime SDP exchange failed: ' . $response->get_error_message()], 502);
        }

        $code = wp_remote_retrieve_response_code($response);
        $answer_sdp = (string) wp_remote_retrieve_body($response);
        if ($code >= 200 && $code < 300 && $answer_sdp !== '' && strpos(ltrim($answer_sdp), 'v=0') === 0) {
            $chosen_model = $candidate_model;
            $model = $candidate_model;
            break;
        }

        $msg = $answer_sdp !== '' ? substr($answer_sdp, 0, 600) : ('HTTP ' . $code);
        $model_errors[] = $candidate_model . ': ' . $msg;

        // If the account lacks the first/premium model, try the cheaper broadly
        // available realtime previews before falling back to delayed Default TTS.
        if (!vmo_realtime_is_model_unavailable_response($code, $answer_sdp)) {
            wp_send_json_error([
                'message' => 'Realtime SDP exchange error: ' . $msg,
                'http_code' => $code,
                'model' => $candidate_model,
            ], 502);
        }
    }

    if ($answer_sdp === '' || strpos(ltrim($answer_sdp), 'v=0') !== 0) {
        wp_send_json_error([
            'message' => 'Realtime SDP exchange failed for all configured models: ' . implode(' | ', $model_errors),
            'http_code' => $code,
            'models_tried' => vmo_realtime_model_candidates($model),
        ], 502);
    }

    // Minute 1 is charged before the user receives a usable answer SDP.
    // In client mode, the SDP request is routed through the Kinsta master proxy
    // with X-VMO-Operation=realtime_audio_min, so the master performs the
    // canonical deduction. In master mode, deduct locally here.
    if (!$rt_unmetered && !vmo_realtime_is_client_mode() && function_exists('vmo_deduct_tokens')) {
        vmo_deduct_tokens(get_current_user_id(), $rt_minute_cost, 'realtime_audio_min', 'Realtime voice session — minute 1 (' . (string) ($cfg['context'] ?? 'jarvis') . ')');
    }
    set_transient('vmo_rt_meter_' . md5($session_id), [
        'user_id' => (int) get_current_user_id(),
        'charged_minutes' => $rt_unmetered ? 0 : 1,
        'started' => time(),
        'context' => (string) ($cfg['context'] ?? 'jarvis'),
        'minute_cost' => $rt_minute_cost,
        'unmetered' => $rt_unmetered,
        'authorized_seconds' => $rt_authorized_seconds,
    ], $rt_unmetered ? (4 * HOUR_IN_SECONDS) : (2 * HOUR_IN_SECONDS));

    wp_send_json_success([
        'answer_sdp' => $answer_sdp,
        'session_id' => $session_id,
        'model' => $model,
        'master_routed' => true,
        'charged_minutes' => $rt_unmetered ? 0 : 1,
        'authorized_until_seconds' => $rt_authorized_seconds,
        'metering_mode' => $rt_unmetered ? 'unmetered' : 'metered',
        'unmetered' => $rt_unmetered,
    ]);
}
}

// ═══════════════════════════════════════════════════════════════════════════════
// VMO REALTIME USAGE METER — The browser reports elapsed connected time for a
// live Realtime session here so each additional minute is charged against the
// user's VMO Credit (minute 1 was prepaid at mint). Idempotent per session: it
// only deducts the difference between minutes already charged and minutes used.
// Returns halt=true when the balance can no longer fund another minute so the
// client can end the call. [VMO_REALTIME_USAGE_METER]
// ═══════════════════════════════════════════════════════════════════════════════
add_action('wp_ajax_vmo_realtime_usage', 'vmo_handle_realtime_usage');
add_action('wp_ajax_vmo_sd_realtime_usage', 'vmo_handle_realtime_usage');
if (!function_exists('vmo_handle_realtime_usage')) {
function vmo_handle_realtime_usage() {
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Forbidden'], 403);
    }
    if (!function_exists('vmo_verify_admin_security') || !vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Security check failed'], 403);
    }

    $session_id = isset($_POST['session_id']) ? sanitize_text_field((string) $_POST['session_id']) : '';
    $elapsed    = isset($_POST['elapsed_seconds']) ? (int) $_POST['elapsed_seconds'] : 0;
    $prebill    = isset($_POST['prebill_seconds']) ? (int) $_POST['prebill_seconds'] : $elapsed;
    if ($session_id === '' || $elapsed < 0 || $prebill < 0) {
        wp_send_json_error(['message' => 'Invalid usage report'], 400);
    }
    // Clamp to a sane ceiling (4h) so a tampered report cannot over- or
    // under-charge wildly.
    $max_elapsed = 4 * HOUR_IN_SECONDS;
    if ($elapsed > $max_elapsed) { $elapsed = $max_elapsed; }
    if ($prebill > $max_elapsed) { $prebill = $max_elapsed; }
    if ($prebill < $elapsed) { $prebill = $elapsed; }

    $user_id = get_current_user_id();
    $key     = 'vmo_rt_meter_' . md5($session_id);
    $meter   = get_transient($key);
    $is_unmetered_master = vmo_realtime_is_unmetered_master($user_id);
    $authorized_unmetered_seconds = vmo_realtime_authorized_window_seconds(true);

    // Unknown or foreign session is a fail-closed state. Do not allow the
    // browser to continue an unmetered Realtime connection.
    if (!is_array($meter) || (int) ($meter['user_id'] ?? 0) !== (int) $user_id) {
        if ($is_unmetered_master) {
            wp_send_json_success([
                'charged_minutes' => 0,
                'authorized_until_seconds' => $authorized_unmetered_seconds,
                'halt' => false,
                'metering_mode' => 'unmetered',
                'unmetered' => true,
            ]);
        }
        wp_send_json_success(['charged_minutes' => 0, 'authorized_until_seconds' => 0, 'halt' => true, 'code' => 'unknown_realtime_session']);
    }

    if (!empty($meter['unmetered']) || $is_unmetered_master) {
        $bill_seconds = max($elapsed, $prebill);
        $minutes_used = (int) ceil(max(1, $bill_seconds) / 60);
        $meter['charged_minutes'] = max((int) ($meter['charged_minutes'] ?? 0), $minutes_used);
        $meter['unmetered'] = true;
        $meter['authorized_seconds'] = $authorized_unmetered_seconds;
        set_transient($key, $meter, 4 * HOUR_IN_SECONDS);
        wp_send_json_success([
            'charged_minutes' => 0,
            'authorized_until_seconds' => $authorized_unmetered_seconds,
            'halt' => false,
            'metering_mode' => 'unmetered',
            'unmetered' => true,
        ]);
    }

    $per_min = function_exists('vmo_get_token_cost') ? (int) vmo_get_token_cost('realtime_audio_min') : 0;
    if ($per_min < 1) { $per_min = (int) ($meter['minute_cost'] ?? 1547); }
    if ($per_min < 1) { $per_min = 1547; }

    // Bill slightly ahead of actual elapsed time so a user cannot drift into a
    // provider-billed minute that VMO has not already charged for. Premium Live
    // Voice is therefore billed in started/authorized minutes.
    $bill_seconds = max($elapsed, $prebill);
    $minutes_used = (int) ceil($bill_seconds / 60);
    if ($minutes_used < 1) { $minutes_used = 1; }
    $already = (int) ($meter['charged_minutes'] ?? 1);
    $delta   = $minutes_used - $already;
    $halt    = false;

    if ($delta > 0) {
        $charge_tokens = $delta * $per_min;
        $charged = false;
        if (vmo_realtime_is_client_mode()) {
            $sync = vmo_realtime_sync_master_usage($charge_tokens, $session_id, (string) ($meter['context'] ?? 'jarvis'), $already + 1, $minutes_used);
            $charged = !empty($sync['success']);
        } elseif (function_exists('vmo_deduct_tokens')) {
            $charged = vmo_deduct_tokens($user_id, $charge_tokens, 'realtime_audio_min', 'Realtime voice session — minutes ' . ($already + 1) . '-' . $minutes_used);
        }

        if ($charged) {
            $meter['charged_minutes'] = $minutes_used;
            set_transient($key, $meter, 2 * HOUR_IN_SECONDS);
        } else {
            // Balance/master sync could not fund the upcoming minutes — end the call.
            $halt = true;
        }
    }

    // Proactively stop the session if the user can no longer afford the next
    // minute, so we never run an unfunded minute.
    if (!$halt && function_exists('vmo_can_user_use_cost_bearing_services')
        && !vmo_can_user_use_cost_bearing_services($user_id, $per_min)) {
        $halt = true;
    }

    $authorized_minutes = (int) ($meter['charged_minutes'] ?? $minutes_used);
    wp_send_json_success([
        'charged_minutes' => $authorized_minutes,
        'authorized_until_seconds' => $authorized_minutes * 60,
        'halt'            => $halt,
    ]);
}
}

// ═══════════════════════════════════════════════════════════════════════════════
// VMO REALTIME TOOL BRIDGE — Server side of the function-call bridge. The JS
// client receives a tool_call from the model, POSTs here with the tool name
// and arguments, and we return the JSON the model needs. No external API
// calls; purely reads existing local state. [VMO_REALTIME_TOOL_BRIDGE]
// ═══════════════════════════════════════════════════════════════════════════════
add_action('wp_ajax_vmo_realtime_tool', 'vmo_handle_realtime_tool');
add_action('wp_ajax_vmo_sd_realtime_tool', 'vmo_handle_realtime_tool');
if (!function_exists('vmo_handle_realtime_tool')) {
function vmo_handle_realtime_tool() {
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Forbidden'], 403);
    }
    if (!function_exists('vmo_verify_admin_security') || !vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Security check failed'], 403);
    }
    $tool = isset($_POST['tool']) ? sanitize_key((string) $_POST['tool']) : '';
    $args_raw = isset($_POST['args']) ? wp_unslash((string) $_POST['args']) : '{}';
    $args = json_decode($args_raw, true);
    if (!is_array($args)) { $args = []; }

    $result = apply_filters('vmo_realtime_tool_result', null, $tool, $args);
    if ($result !== null) {
        wp_send_json_success(['tool' => $tool, 'result' => $result]);
    }

    $user_id = get_current_user_id();
    switch ($tool) {
        case 'get_business_state':
            $state = [
                'user_id' => $user_id,
                'setup_progress' => (int) get_user_meta($user_id, 'vmo_setup_progress', true),
                'active_plan' => (string) get_user_meta($user_id, 'vmo_active_plan', true),
                'business_name' => (string) get_user_meta($user_id, 'vmo_business_name', true),
                'has_website' => (bool) get_user_meta($user_id, 'vmo_has_website', true),
            ];
            wp_send_json_success(['tool' => $tool, 'result' => $state]);
            break;
        case 'get_kpi_snapshot':
            $kpi = get_user_meta($user_id, 'vmo_kpi_snapshot', true);
            if (!is_array($kpi)) { $kpi = ['note' => 'No KPI snapshot recorded yet.']; }
            wp_send_json_success(['tool' => $tool, 'result' => $kpi]);
            break;
        case 'get_recent_actions':
            $limit = max(1, min(50, (int) ($args['limit'] ?? 10)));
            $actions = get_user_meta($user_id, 'vmo_recent_actions', true);
            if (!is_array($actions)) { $actions = []; }
            wp_send_json_success(['tool' => $tool, 'result' => array_slice($actions, 0, $limit)]);
            break;
        case 'jarvis_command':
            // PARITY BRIDGE: route Premium Live Voice (Realtime) commands through
            // the exact same processor as default-mode Jarvis, so premium voice can
            // read/summarize/reply/send emails, navigate tabs, and run actions
            // identically. The model passes a natural-language `command` string.
            $command = isset($args['command']) ? (string) $args['command'] : '';
            if ($command === '' && isset($args['query'])) { $command = (string) $args['query']; }
            if ($command === '') {
                wp_send_json_success(['tool' => $tool, 'result' => ['no_match' => true, 'message' => 'No command was provided.']]);
                break;
            }
            if (class_exists('VMO_Integration_Hub')) {
                $hub_result = VMO_Integration_Hub::get_instance()->process_jarvis_command($command, $user_id);
                wp_send_json_success(['tool' => $tool, 'result' => $hub_result]);
            } else {
                wp_send_json_success(['tool' => $tool, 'result' => ['no_match' => true, 'message' => 'The command router is not available right now.']]);
            }
            break;
        default:
            wp_send_json_error(['message' => 'Unknown tool: ' . $tool], 400);
    }
}
}

// AJAX: Save phone preferences (ring settings, greeting, external phone)
add_action('wp_ajax_vmo_save_phone_prefs', function() {
    if (!current_user_can('read')) {
        wp_send_json_error(array('message' => 'Forbidden'), 403);
    }
    if (!function_exists('vmo_verify_admin_security') || !vmo_verify_admin_security()) {
        wp_send_json_error(array('message' => 'Security check failed'), 403);
    }
    
    $user_id = get_current_user_id();
    $greeting = sanitize_text_field($_POST['greeting'] ?? '');
    $ring_url = esc_url_raw($_POST['ring_url'] ?? '');
    $ring_attachment_id = intval($_POST['ring_attachment_id'] ?? 0);

    // Reject any ringtone URL that isn't an audio file. Browsers will silently
    // strip-and-loop the audio track of a video file (e.g. iPhone .mov uploads),
    // producing a runaway autoplay loop. Force the value (and attachment id) to
    // empty so the synth fallback is used instead.
    if ($ring_url !== '') {
        $bad_re   = '/\.(mov|mp4|m4v|webm|mkv|avi|3gp|wmv|flv|jpg|jpeg|png|gif|webp|bmp|svg|pdf|heic)(\?|#|$)/i';
        $audio_re = '/\.(mp3|wav|ogg|m4a|aac|flac|opus|weba)(\?|#|$)/i';
        if (preg_match($bad_re, $ring_url) || !preg_match($audio_re, $ring_url)) {
            error_log('[VMO Phone Prefs] Rejected non-audio ringtone URL for user ' . $user_id . ': ' . $ring_url);
            $ring_url = '';
            $ring_attachment_id = 0;
        }
    }

    $external_phone = sanitize_text_field($_POST['external_phone'] ?? '');

    // AI receptionist voice (OpenAI TTS voice ID)
    $ai_voice_raw = sanitize_key($_POST['ai_receptionist_voice'] ?? '');
    $allowed_voices = array('ash', 'marin', 'cedar', 'alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer', 'sage', 'ballad', 'coral', 'verse');
    $ai_voice = in_array($ai_voice_raw, $allowed_voices, true) ? $ai_voice_raw : '';

    $existing_recording = get_user_meta($user_id, 'call_recording_enabled', true);
    $existing_alerts = get_user_meta($user_id, 'vmo_email_incoming_alerts', true);
    $existing_summaries = get_user_meta($user_id, 'vmo_email_call_summaries', true);

    $call_recording_enabled = isset($_POST['call_recording_enabled'])
        ? intval($_POST['call_recording_enabled'])
        : (($existing_recording === '' || $existing_recording === false) ? 1 : intval($existing_recording));
        
    $email_incoming_alerts = isset($_POST['email_incoming_alerts'])
        ? intval($_POST['email_incoming_alerts'])
        : (($existing_alerts === '' || $existing_alerts === false) ? 1 : intval($existing_alerts));
        
    $email_call_summaries = isset($_POST['email_call_summaries'])
        ? intval($_POST['email_call_summaries'])
        : (($existing_summaries === '' || $existing_summaries === false) ? 1 : intval($existing_summaries));

    // Voice quality tier: 'standard' (realtime-mini) or 'premium' (realtime-1.5)
    $voice_quality_raw = sanitize_key($_POST['voice_quality_tier'] ?? '');
    $voice_quality_tier = in_array($voice_quality_raw, array('standard', 'premium'), true) ? $voice_quality_raw : 'standard';

    $ring_enabled = isset($_POST['ring_enabled']) ? intval($_POST['ring_enabled']) : 1;
    $ring_volume = isset($_POST['ring_volume']) ? floatval($_POST['ring_volume']) : 0.6;
    $ring_preset = sanitize_key($_POST['ring_preset'] ?? 'classic');
    $extensions_raw = wp_unslash($_POST['extensions'] ?? '');
    $extensions = array();
    if (!empty($extensions_raw)) {
        $decoded = json_decode($extensions_raw, true);
        if (is_array($decoded)) {
            foreach ($decoded as $k => $v) {
                $kk = preg_replace('/[^1-7]/', '', (string) $k);
                if ($kk === '') continue;
                $vv = sanitize_text_field((string) $v);
                if ($vv !== '') {
                    $extensions[$kk] = $vv;
                }
            }
        }
    }

    // Parse extension labels
    $ext_labels_raw = wp_unslash($_POST['extension_labels'] ?? '');
    $extension_labels = array();
    if (!empty($ext_labels_raw)) {
        $decoded_labels = json_decode($ext_labels_raw, true);
        if (is_array($decoded_labels)) {
            foreach ($decoded_labels as $k => $v) {
                $kk = preg_replace('/[^1-7]/', '', (string) $k);
                if ($kk === '') continue;
                $vv = sanitize_text_field((string) $v);
                if ($vv !== '') {
                    $extension_labels[$kk] = $vv;
                }
            }
        }
    }
    
    // Save to user meta
    update_user_meta($user_id, 'vmo_ai_greeting', $greeting);
    if ($ai_voice !== '') {
        update_user_meta($user_id, 'vmo_selected_reception_voice', $ai_voice);
    }
    update_user_meta($user_id, 'vmo_ring_url', $ring_url);
    update_user_meta($user_id, 'vmo_ring_attachment_id', $ring_attachment_id);
    update_user_meta($user_id, 'vmo_external_phone', $external_phone);

    update_user_meta($user_id, 'call_recording_enabled', $call_recording_enabled ? 1 : 0);
    update_user_meta($user_id, 'vmo_email_incoming_alerts', $email_incoming_alerts ? 1 : 0);
    update_user_meta($user_id, 'vmo_email_call_summaries', $email_call_summaries ? 1 : 0);
    update_user_meta($user_id, 'vmo_phone_prefs_initialized', current_time('mysql'));
    update_user_meta($user_id, 'vmo_voice_quality_tier', $voice_quality_tier);

    update_user_meta($user_id, 'vmo_ring_enabled', $ring_enabled ? 1 : 0);
    update_user_meta($user_id, 'vmo_ring_volume', max(0, min(1, $ring_volume)));
    update_user_meta($user_id, 'vmo_ring_preset', in_array($ring_preset, array('classic', 'pulse', 'chime', 'soft'), true) ? $ring_preset : 'classic');
    update_user_meta($user_id, 'vmo_phone_extensions', $extensions);
    update_user_meta($user_id, 'vmo_phone_extension_labels', $extension_labels);
    
    wp_send_json_success(array('message' => 'Preferences saved successfully'));
});

// AJAX: Get phone preferences
add_action('wp_ajax_vmo_get_phone_prefs', function() {
    if (!current_user_can('read')) {
        wp_send_json_error(array('message' => 'Forbidden'), 403);
    }
    if (!function_exists('vmo_verify_admin_security') || !vmo_verify_admin_security()) {
        wp_send_json_error(array('message' => 'Security check failed'), 403);
    }
    
    $user_id = get_current_user_id();
    
    $enabled_meta = get_user_meta($user_id, 'vmo_ring_enabled', true);
    $volume_meta = get_user_meta($user_id, 'vmo_ring_volume', true);
    $preset_meta = get_user_meta($user_id, 'vmo_ring_preset', true);
    $ext_meta = get_user_meta($user_id, 'vmo_phone_extensions', true);
    $ext_labels_meta = get_user_meta($user_id, 'vmo_phone_extension_labels', true);
    $prefs_initialized = get_user_meta($user_id, 'vmo_phone_prefs_initialized', true);
    $incoming_alerts_meta = get_user_meta($user_id, 'vmo_email_incoming_alerts', true);
    $summary_alerts_meta = get_user_meta($user_id, 'vmo_email_call_summaries', true);

    // Default to true (1) if no affirmative "0" exists
    if ($incoming_alerts_meta === '') {
        $incoming_alerts_meta = 1;
    }
    if ($summary_alerts_meta === '') {
        $summary_alerts_meta = 1;
    }

    wp_send_json_success(array(
        'greeting' => get_user_meta($user_id, 'vmo_ai_greeting', true),
        'ai_receptionist_voice' => get_user_meta($user_id, 'vmo_selected_reception_voice', true) ?: 'ash',
        'ring_url' => get_user_meta($user_id, 'vmo_ring_url', true),
        'ring_attachment_id' => get_user_meta($user_id, 'vmo_ring_attachment_id', true),
        'external_phone' => get_user_meta($user_id, 'vmo_external_phone', true),
        'call_recording_enabled' => (int) get_user_meta($user_id, 'call_recording_enabled', true),
        'email_incoming_alerts' => (($incoming_alerts_meta !== '') ? (int) $incoming_alerts_meta : 1),
        'email_call_summaries' => (($summary_alerts_meta !== '') ? (int) $summary_alerts_meta : 1),
        'voice_quality_tier' => get_user_meta($user_id, 'vmo_voice_quality_tier', true) ?: 'standard',
        'ring_enabled' => ($enabled_meta === '' ? 1 : (int) $enabled_meta),
        'ring_volume' => ($volume_meta === '' ? 0.6 : (float) $volume_meta),
        'ring_preset' => ($preset_meta ? $preset_meta : 'classic'),
        'extensions' => (is_array($ext_meta) ? $ext_meta : array()),
        'extension_labels' => (is_array($ext_labels_meta) ? $ext_labels_meta : array()),
    ));
});

if (!function_exists('vmo_sms_normalize_phone')) {
    function vmo_sms_normalize_phone($phone) {
        $digits = preg_replace('/\D+/', '', (string) $phone);
        if ($digits === '') {
            return '';
        }
        if (strlen($digits) === 11 && strpos($digits, '1') === 0) {
            return substr($digits, 1);
        }
        return $digits;
    }
}

if (!function_exists('vmo_sms_get_business_number')) {
    function vmo_sms_get_business_number($user_id) {
        if (class_exists('VMO_Twilio_Manager')) {
            try {
                $phone = VMO_Twilio_Manager::get_instance()->get_user_phone($user_id);
                if (is_string($phone)) {
                    return $phone;
                }
                if (is_array($phone)) {
                    return (string) ($phone['number'] ?? $phone['phone_number'] ?? '');
                }
            } catch (Throwable $e) {
            }
        }

        $meta_phone = get_user_meta($user_id, 'vmo_phone_number', true);
        if (is_array($meta_phone)) {
            return (string) ($meta_phone['number'] ?? $meta_phone['phone_number'] ?? '');
        }

        return (string) $meta_phone;
    }
}

if (!function_exists('vmo_sms_collect_conversations')) {
    function vmo_sms_collect_conversations(array $rows) {
        $conversations = array();

        foreach ($rows as $row) {
            $direction = (string) ($row['direction'] ?? '');
            if ($direction !== 'inbound' && $direction !== 'outbound') {
                continue;
            }

            $contact_number = ($direction === 'inbound')
                ? (string) ($row['from_number'] ?? '')
                : (string) ($row['to_number'] ?? '');
            $contact_key = vmo_sms_normalize_phone($contact_number);
            if ($contact_key === '') {
                continue;
            }

            if (!isset($conversations[$contact_key])) {
                $preview = trim((string) preg_replace('/\s+/', ' ', (string) ($row['body'] ?? '')));
                if (strlen($preview) > 120) {
                    $preview = substr($preview, 0, 117) . '...';
                }

                $conversations[$contact_key] = array(
                    'contact_number' => $contact_number,
                    'contact_key' => $contact_key,
                    'last_body' => (string) ($row['body'] ?? ''),
                    'last_preview' => $preview,
                    'last_direction' => $direction,
                    'last_status' => (string) ($row['status'] ?? ''),
                    'last_at' => (string) ($row['created_at'] ?? ''),
                    'message_count' => 0,
                    'inbound_count' => 0,
                    'outbound_count' => 0,
                );
            }

            $conversations[$contact_key]['message_count']++;
            if ($direction === 'inbound') {
                $conversations[$contact_key]['inbound_count']++;
            } else {
                $conversations[$contact_key]['outbound_count']++;
            }
        }

        return array_values($conversations);
    }
}

if (!function_exists('vmo_get_sms_activation_snapshot')) {
    function vmo_get_sms_activation_snapshot($user_id, $refresh = true) {
        $user_id = (int) $user_id;
        $brand_sid = (string) get_user_meta($user_id, 'vmo_a2p_brand_sid', true);
        $campaign_sid = (string) get_user_meta($user_id, 'vmo_a2p_campaign_sid', true);
        $messaging_service_sid = (string) get_user_meta($user_id, 'vmo_a2p_messaging_service_sid', true);
        $sms_status = (string) get_user_meta($user_id, 'vmo_sms_activated', true);
        $has_business_number = (function_exists('vmo_sms_get_business_number') && vmo_sms_get_business_number($user_id) !== '');

        $snapshot = array(
            'brand_status' => (string) (get_user_meta($user_id, 'vmo_a2p_brand_status', true) ?: 'not_started'),
            'campaign_status' => (string) (get_user_meta($user_id, 'vmo_a2p_campaign_status', true) ?: 'not_started'),
            'sms_status' => $sms_status ?: 'inactive',
            'sms_active' => ($sms_status === '1' || $sms_status === 'active'),
            'sms_activated_at' => (string) get_user_meta($user_id, 'vmo_sms_activated_date', true),
            'tracked_in_plugin' => (!empty($brand_sid) || !empty($campaign_sid) || !empty($messaging_service_sid)),
            'campaign_cancelled' => ((string) get_user_meta($user_id, 'vmo_a2p_campaign_cancelled', true) === '1'),
            'campaign_cancelled_at' => (string) get_user_meta($user_id, 'vmo_a2p_campaign_cancelled_at', true),
            'campaign_next_billing' => (($next_billing = (int) get_user_meta($user_id, 'vmo_a2p_campaign_next_billing', true)) > 0 ? gmdate('Y-m-d H:i:s', $next_billing) : ''),
            'campaign_use_case' => (string) get_user_meta($user_id, 'vmo_a2p_campaign_use_case', true),
            'registration_intent' => (string) get_user_meta($user_id, 'vmo_a2p_registration_intent', true),
            'checked_at' => current_time('mysql'),
        );

        if ($refresh && class_exists('VMO_Twilio_A2P_Manager') && ($snapshot['tracked_in_plugin'] || $has_business_number)) {
            try {
                $manager = VMO_Twilio_A2P_Manager::get_instance();
                if ($manager && method_exists($manager, 'check_status')) {
                    $live = $manager->check_status($user_id);
                    if (is_array($live)) {
                        $snapshot = array_merge($snapshot, $live);
                    }
                }
            } catch (Throwable $e) {
                if (function_exists('vmo_log')) {
                    vmo_log('SMS activation status refresh failed for user ' . $user_id . ': ' . $e->getMessage());
                }
            }
        }

        $snapshot['sms_active'] = !empty($snapshot['sms_active']);
        $snapshot['sms_status'] = ($snapshot['sms_active'] ? 'active' : ((string) ($snapshot['sms_status'] ?? 'inactive')));
        $snapshot['brand_status'] = (string) ($snapshot['brand_status'] ?? 'not_started');
        $snapshot['campaign_status'] = (string) ($snapshot['campaign_status'] ?? 'not_started');
        $snapshot['sms_activated_at'] = (string) ($snapshot['sms_activated_at'] ?? '');
        $snapshot['tracked_in_plugin'] = !empty($snapshot['tracked_in_plugin']);
        $snapshot['campaign_cancelled'] = !empty($snapshot['campaign_cancelled']);
        $snapshot['campaign_cancelled_at'] = (string) ($snapshot['campaign_cancelled_at'] ?? '');
        $snapshot['campaign_next_billing'] = (string) ($snapshot['campaign_next_billing'] ?? '');
        $snapshot['campaign_use_case'] = (string) ($snapshot['campaign_use_case'] ?? '');
        $snapshot['registration_intent'] = (string) ($snapshot['registration_intent'] ?: 'regular');
        $snapshot['bulk_marketing_ready'] = !empty($snapshot['bulk_marketing_ready'])
            || (!$snapshot['campaign_cancelled'] && !empty($snapshot['sms_active']) && (($snapshot['registration_intent'] === 'marketing') || strtoupper($snapshot['campaign_use_case']) === 'MARKETING'));
        $snapshot['checked_at'] = (string) ($snapshot['checked_at'] ?? current_time('mysql'));

        return $snapshot;
    }
}

// AJAX: SMS workspace summary and conversation list
add_action('wp_ajax_vmo_get_sms_workspace', function() {
    if (!current_user_can('read')) {
        wp_send_json_error(array('message' => 'Forbidden'), 403);
    }
    if (!function_exists('vmo_verify_admin_security') || !vmo_verify_admin_security()) {
        wp_send_json_error(array('message' => 'Security check failed'), 403);
    }

    global $wpdb;
    $user_id = get_current_user_id();
    $sms_table = $wpdb->prefix . 'vmo_sms_logs';
    $table_exists = function_exists('vmo_table_exists') ? vmo_table_exists($sms_table) : ($wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $sms_table)) === $sms_table);
    if (!$table_exists && function_exists('vmo_create_twilio_tables')) {
        vmo_create_twilio_tables();
        $table_exists = function_exists('vmo_table_exists') ? vmo_table_exists($sms_table) : ($wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $sms_table)) === $sms_table);
    }

    $rows = array();
    if ($table_exists) {
        $rows = $wpdb->get_results($wpdb->prepare(
            "SELECT id, twilio_message_sid, from_number, to_number, direction, body, status, created_at
             FROM {$sms_table}
             WHERE user_id = %d
             ORDER BY created_at DESC, id DESC
             LIMIT 250",
            $user_id
        ), ARRAY_A);
    }

    $conversations = vmo_sms_collect_conversations($rows ?: array());
    $activation = function_exists('vmo_get_sms_activation_snapshot')
        ? vmo_get_sms_activation_snapshot($user_id, true)
        : array(
            'sms_status' => (string) get_user_meta($user_id, 'vmo_sms_activated', true),
            'sms_active' => ((string) get_user_meta($user_id, 'vmo_sms_activated', true) === 'active'),
            'brand_status' => (string) get_user_meta($user_id, 'vmo_a2p_brand_status', true),
            'campaign_status' => (string) get_user_meta($user_id, 'vmo_a2p_campaign_status', true),
            'sms_activated_at' => (string) get_user_meta($user_id, 'vmo_sms_activated_date', true),
            'campaign_cancelled' => ((string) get_user_meta($user_id, 'vmo_a2p_campaign_cancelled', true) === '1'),
            'campaign_cancelled_at' => (string) get_user_meta($user_id, 'vmo_a2p_campaign_cancelled_at', true),
            'campaign_next_billing' => (($next_billing = (int) get_user_meta($user_id, 'vmo_a2p_campaign_next_billing', true)) > 0 ? gmdate('Y-m-d H:i:s', $next_billing) : ''),
            'campaign_use_case' => (string) get_user_meta($user_id, 'vmo_a2p_campaign_use_case', true),
            'registration_intent' => (string) get_user_meta($user_id, 'vmo_a2p_registration_intent', true),
            'tracked_in_plugin' => false,
            'checked_at' => current_time('mysql'),
        );
    $sms_status = (string) ($activation['sms_status'] ?? 'inactive');
    $token_balance = function_exists('vmo_get_token_balance') ? (int) vmo_get_token_balance($user_id) : (int) get_user_meta($user_id, 'vmo_token_balance', true);
    $brand_cost = class_exists('VMO_Twilio_Token_Economics') ? VMO_Twilio_Token_Economics::A2P_BRAND() : 13500;
    $campaign_cost = class_exists('VMO_Twilio_Token_Economics') ? VMO_Twilio_Token_Economics::A2P_CAMPAIGN_MONTHLY() : 30000;
    $per_reply_cost = class_exists('VMO_Twilio_Token_Economics')
        ? (VMO_Twilio_Token_Economics::SMS_OUTBOUND() + VMO_Twilio_Token_Economics::SMS_A2P_SURCHARGE())
        : 50;
    $recommended_campaign_balance = $campaign_cost + (class_exists('VMO_Twilio_Token_Economics') ? VMO_Twilio_Token_Economics::PHONE_MONTHLY_RENTAL() : 3750);

    $inbound_total = 0;
    $outbound_total = 0;
    foreach (($rows ?: array()) as $row) {
        $dir = (string) ($row['direction'] ?? '');
        if ($dir === 'inbound') {
            $inbound_total++;
        } elseif ($dir === 'outbound') {
            $outbound_total++;
        }
    }

    wp_send_json_success(array(
        'business_phone' => vmo_sms_get_business_number($user_id),
        'external_phone' => (string) get_user_meta($user_id, 'vmo_external_phone', true),
        'sms_status' => $sms_status,
        'sms_active' => !empty($activation['sms_active']),
        'brand_status' => (string) ($activation['brand_status'] ?? 'not_started'),
        'campaign_status' => (string) ($activation['campaign_status'] ?? 'not_started'),
        'campaign_cancelled' => !empty($activation['campaign_cancelled']),
        'campaign_cancelled_at' => (string) ($activation['campaign_cancelled_at'] ?? ''),
        'campaign_next_billing' => (string) ($activation['campaign_next_billing'] ?? ''),
        'campaign_use_case' => (string) ($activation['campaign_use_case'] ?? ''),
        'registration_intent' => (string) (($activation['registration_intent'] ?? '') ?: 'regular'),
        'bulk_marketing_ready' => !empty($activation['bulk_marketing_ready']),
        'sms_activated_at' => (string) ($activation['sms_activated_at'] ?? ''),
        'sms_checked_at' => (string) ($activation['checked_at'] ?? ''),
        'tracked_in_plugin' => !empty($activation['tracked_in_plugin']),
        'token_balance' => $token_balance,
        'latest_contact' => (string) get_user_meta($user_id, 'vmo_sms_last_contact', true),
        'counts' => array(
            'conversations' => count($conversations),
            'messages' => $inbound_total + $outbound_total,
            'inbound' => $inbound_total,
            'outbound' => $outbound_total,
        ),
        'costs' => array(
            'brand_tokens' => $brand_cost,
            'campaign_tokens' => $campaign_cost,
            'per_reply_tokens' => $per_reply_cost,
            'recommended_campaign_balance' => $recommended_campaign_balance,
        ),
        'conversations' => $conversations,
    ));
});

// AJAX: Load one SMS conversation thread
add_action('wp_ajax_vmo_get_sms_thread', function() {
    if (!current_user_can('read')) {
        wp_send_json_error(array('message' => 'Forbidden'), 403);
    }
    if (!function_exists('vmo_verify_admin_security') || !vmo_verify_admin_security()) {
        wp_send_json_error(array('message' => 'Security check failed'), 403);
    }

    global $wpdb;
    $user_id = get_current_user_id();
    $contact = sanitize_text_field(wp_unslash($_POST['contact'] ?? ''));
    $contact_key = vmo_sms_normalize_phone($contact);
    $sms_table = $wpdb->prefix . 'vmo_sms_logs';
    $table_exists = function_exists('vmo_table_exists') ? vmo_table_exists($sms_table) : ($wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $sms_table)) === $sms_table);

    if ($contact_key === '' || !$table_exists) {
        wp_send_json_success(array('contact' => $contact, 'messages' => array()));
    }

    $rows = $wpdb->get_results($wpdb->prepare(
        "SELECT id, twilio_message_sid, from_number, to_number, direction, body, status, created_at
         FROM {$sms_table}
         WHERE user_id = %d AND direction IN ('inbound', 'outbound')
         ORDER BY created_at DESC, id DESC
         LIMIT 600",
        $user_id
    ), ARRAY_A);

    $messages = array();
    foreach (($rows ?: array()) as $row) {
        $direction = (string) ($row['direction'] ?? '');
        $message_contact = ($direction === 'inbound')
            ? (string) ($row['from_number'] ?? '')
            : (string) ($row['to_number'] ?? '');
        if (vmo_sms_normalize_phone($message_contact) !== $contact_key) {
            continue;
        }

        $messages[] = array(
            'id' => (int) ($row['id'] ?? 0),
            'message_sid' => (string) ($row['twilio_message_sid'] ?? ''),
            'from_number' => (string) ($row['from_number'] ?? ''),
            'to_number' => (string) ($row['to_number'] ?? ''),
            'direction' => $direction,
            'body' => (string) ($row['body'] ?? ''),
            'status' => (string) ($row['status'] ?? ''),
            'created_at' => (string) ($row['created_at'] ?? ''),
        );
    }

    if (count($messages) > 300) {
        $messages = array_slice($messages, 0, 300);
    }
    $messages = array_reverse($messages);

    wp_send_json_success(array(
        'contact' => $contact,
        'contact_key' => $contact_key,
        'messages' => $messages,
    ));
});

// AJAX: Send an SMS from the dashboard workspace
add_action('wp_ajax_vmo_send_sms_message', function() {
    if (!current_user_can('read')) {
        wp_send_json_error(array('message' => 'Forbidden'), 403);
    }
    if (!function_exists('vmo_verify_admin_security') || !vmo_verify_admin_security()) {
        wp_send_json_error(array('message' => 'Security check failed'), 403);
    }

    $user_id = get_current_user_id();
    $to = sanitize_text_field(wp_unslash($_POST['to'] ?? ''));
    $message = trim(sanitize_textarea_field(wp_unslash($_POST['message'] ?? '')));
    $activation = function_exists('vmo_get_sms_activation_snapshot')
        ? vmo_get_sms_activation_snapshot($user_id, true)
        : array(
            'sms_active' => (((string) get_user_meta($user_id, 'vmo_sms_activated', true) === '1') || ((string) get_user_meta($user_id, 'vmo_sms_activated', true) === 'active')),
            'brand_status' => (string) get_user_meta($user_id, 'vmo_a2p_brand_status', true),
            'campaign_status' => (string) get_user_meta($user_id, 'vmo_a2p_campaign_status', true),
            'tracked_in_plugin' => false,
        );

    if ($to === '' || $message === '') {
        wp_send_json_error(array('message' => 'Recipient and message are required'), 400);
    }

    if (empty($activation['sms_active'])) {
        $brand_status = strtoupper((string) ($activation['brand_status'] ?? 'not_started'));
        $campaign_status = strtoupper((string) ($activation['campaign_status'] ?? 'not_started'));
        $tracked_in_plugin = !empty($activation['tracked_in_plugin']);
        $message = $tracked_in_plugin
            ? 'SMS is not active on this business number yet. Current status — Brand: ' . str_replace('_', ' ', $brand_status) . ', Campaign: ' . str_replace('_', ' ', $campaign_status) . '. Open SMS Activation to refresh status inside the plugin.'
            : 'SMS is not active on this business number yet. VMO could not confirm a completed carrier registration on this number during the latest live check.';
        wp_send_json_error(array('message' => $message), 409);
    }

    if (!class_exists('VMO_Twilio_Enhanced_Client')) {
        $path = plugin_dir_path(__FILE__) . 'includes/class-vmo-twilio-enhanced-client.php';
        if (file_exists($path)) {
            require_once $path;
        }
    }
    if (!class_exists('VMO_Twilio_Manager')) {
        $path = plugin_dir_path(__FILE__) . 'includes/class-vmo-twilio-manager.php';
        if (file_exists($path)) {
            require_once $path;
        }
    }

    if (!class_exists('VMO_Twilio_Enhanced_Client')) {
        wp_send_json_error(array('message' => 'Twilio SMS client unavailable'), 500);
    }

    $client = new VMO_Twilio_Enhanced_Client();
    $result = $client->send_sms($to, $message, array('track_status' => true));
    if (is_wp_error($result)) {
        wp_send_json_error(array('message' => $result->get_error_message()), 400);
    }

    if (class_exists('VMO_Twilio_Manager')) {
        try {
            $manager = VMO_Twilio_Manager::get_instance();
            if (method_exists($manager, 'log_sms_message')) {
                $manager->log_sms_message(
                    $user_id,
                    sanitize_text_field((string) ($result['sid'] ?? '')),
                    vmo_sms_get_business_number($user_id),
                    $to,
                    'outbound',
                    $message,
                    sanitize_text_field((string) ($result['status'] ?? 'queued'))
                );
            }
        } catch (Throwable $e) {
        }
    }

    wp_send_json_success(array(
        'message_sid' => sanitize_text_field((string) ($result['sid'] ?? '')),
        'status' => sanitize_text_field((string) ($result['status'] ?? 'queued')),
        'token_balance' => function_exists('vmo_get_token_balance') ? (int) vmo_get_token_balance($user_id) : null,
    ));
});

// AJAX: Generate an AI-assisted SMS draft for the current conversation
add_action('wp_ajax_vmo_generate_sms_draft', function() {
    if (!current_user_can('read')) {
        wp_send_json_error(array('message' => 'Forbidden'), 403);
    }
    if (!function_exists('vmo_verify_admin_security') || !vmo_verify_admin_security()) {
        wp_send_json_error(array('message' => 'Security check failed'), 403);
    }

    $user_id = get_current_user_id();
    $to = sanitize_text_field(wp_unslash($_POST['to'] ?? ''));
    $intent = sanitize_text_field(wp_unslash($_POST['intent'] ?? 'general reply'));
    $notes = sanitize_textarea_field(wp_unslash($_POST['notes'] ?? ''));
    $contact_key = vmo_sms_normalize_phone($to);

    if (!function_exists('vmo_openai_chat')) {
        wp_send_json_error(array('message' => 'AI request helper unavailable.'), 500);
    }

    if (function_exists('vmo_deduct_tokens_for_operation') && !vmo_deduct_tokens_for_operation($user_id, 'chat')) {
        wp_send_json_error(array(
            'message' => 'Insufficient tokens to generate an AI SMS draft.',
            'tokens_required' => function_exists('vmo_get_token_cost') ? vmo_get_token_cost('chat') : 50,
            'tokens_available' => function_exists('vmo_get_token_balance') ? vmo_get_token_balance($user_id) : 0,
        ), 402);
    }

    global $wpdb;
    $thread_lines = array();
    $sms_table = $wpdb->prefix . 'vmo_sms_logs';
    $table_exists = function_exists('vmo_table_exists') ? vmo_table_exists($sms_table) : ($wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $sms_table)) === $sms_table);
    if ($table_exists && $contact_key !== '') {
        $rows = $wpdb->get_results($wpdb->prepare(
            "SELECT from_number, to_number, direction, body, created_at
             FROM {$sms_table}
             WHERE user_id = %d AND direction IN ('inbound', 'outbound')
             ORDER BY created_at DESC, id DESC
             LIMIT 120",
            $user_id
        ), ARRAY_A);

        $filtered = array();
        foreach (($rows ?: array()) as $row) {
            $direction = (string) ($row['direction'] ?? '');
            $message_contact = ($direction === 'inbound')
                ? (string) ($row['from_number'] ?? '')
                : (string) ($row['to_number'] ?? '');
            if (vmo_sms_normalize_phone($message_contact) === $contact_key) {
                $filtered[] = $row;
            }
        }

        $filtered = array_reverse(array_slice($filtered, 0, 6));
        $profile = function_exists('vmo_get_consultation_profile') ? vmo_get_consultation_profile($user_id) : array();
        $business_name = sanitize_text_field($profile['business_name'] ?? (get_user_meta($user_id, 'business_name', true) ?: 'The business'));
        foreach ($filtered as $row) {
            $speaker = ((string) ($row['direction'] ?? '') === 'inbound') ? 'Customer' : $business_name;
            $thread_lines[] = $speaker . ': ' . trim((string) ($row['body'] ?? ''));
        }
    }

    $profile = function_exists('vmo_get_consultation_profile') ? vmo_get_consultation_profile($user_id) : array();
    $business_name = sanitize_text_field($profile['business_name'] ?? (get_user_meta($user_id, 'business_name', true) ?: 'The business'));
    $industry = sanitize_text_field($profile['industry'] ?? (get_user_meta($user_id, 'business_type', true) ?: 'General'));
    $intent_lower = strtolower($intent);
    $opt_out_guidance = (strpos($intent_lower, 'promo') !== false || strpos($intent_lower, 'market') !== false)
        ? 'Include a short opt-out line such as "Reply STOP to opt out."'
        : 'Do not include opt-out language unless the user specifically requested a promotional SMS.';

    $messages = array(
        array(
            'role' => 'system',
            'content' => 'You write concise, professional business SMS messages. Return only the final SMS body with no quotes, headings, markdown, or JSON. Keep it human, practical, and normally under 320 characters. ' . $opt_out_guidance,
        ),
        array(
            'role' => 'user',
            'content' => "Business name: {$business_name}\nIndustry: {$industry}\nRecipient: {$to}\nIntent: {$intent}\nAdditional guidance: {$notes}\nRecent thread:\n" . (empty($thread_lines) ? 'No recent thread available.' : implode("\n", $thread_lines)) . "\n\nDraft the next SMS reply now.",
        ),
    );

    $model = function_exists('vmo_get_model') ? vmo_get_model('nano') : (defined('VMO_OPENAI_MODEL') ? VMO_OPENAI_MODEL : 'gpt-5.5-nano');
    if (function_exists('vmo_get_optimal_model')) {
        try {
            $model = vmo_get_optimal_model('content_creation', array('skip_log' => true));
        } catch (Throwable $e) {
        }
    }

    $response = vmo_openai_chat($messages, $model, array(
        'temperature' => 0.55,
        'max_tokens' => 180,
        'timeout' => 60,
    ));

    if (is_wp_error($response)) {
        wp_send_json_error(array('message' => 'AI draft request failed: ' . $response->get_error_message()), 500);
    }

    $payload = json_decode(wp_remote_retrieve_body($response), true);
    $draft = trim((string) ($payload['choices'][0]['message']['content'] ?? ''));
    $draft = preg_replace('/^```(?:text)?\s*|\s*```$/i', '', $draft);
    $draft = trim((string) $draft, "\"' \n\r\t");

    if ($draft === '') {
        wp_send_json_error(array('message' => 'AI did not return a usable SMS draft.'), 500);
    }

    wp_send_json_success(array(
        'draft' => $draft,
        'tokens_used' => function_exists('vmo_get_token_cost') ? vmo_get_token_cost('chat') : 50,
        'token_balance' => function_exists('vmo_get_token_balance') ? (int) vmo_get_token_balance($user_id) : null,
    ));
});

// AJAX: Get call history with recordings
add_action('wp_ajax_vmo_get_call_history', function() {
    if (!current_user_can('read')) {
        wp_send_json_error(array('message' => 'Forbidden'), 403);
    }
    if (!function_exists('vmo_verify_admin_security') || !vmo_verify_admin_security()) {
        wp_send_json_error(array('message' => 'Security check failed'), 403);
    }
    
    global $wpdb;
    $user_id = get_current_user_id();
    $calls_table = $wpdb->prefix . 'vmo_phone_calls';
    $pd_table = $wpdb->prefix . 'vmo_power_dialer_calls';
    $limit = isset($_POST['limit']) ? max(1, min(100, intval($_POST['limit']))) : 50;
    $offset = isset($_POST['offset']) ? max(0, intval($_POST['offset'])) : 0;
    $contact_id = isset($_POST['contact_id']) ? intval($_POST['contact_id']) : 0;
    $is_admin = current_user_can('manage_options');
    $debug = array('user_id' => $user_id, 'is_admin' => $is_admin, 'steps' => array());

    // ── Step 1: Ensure vmo_phone_calls table exists with correct schema ──
    $table_exists = vmo_table_exists($calls_table);
    if (!$table_exists) {
        $charset_collate = $wpdb->get_charset_collate();
        require_once ABSPATH . 'wp-admin/includes/upgrade.php';
        dbDelta("CREATE TABLE IF NOT EXISTS {$calls_table} (
            id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
            user_id BIGINT UNSIGNED NOT NULL,
            call_sid VARCHAR(255) NOT NULL,
            caller_number VARCHAR(50) DEFAULT NULL,
            direction ENUM('inbound','outbound') DEFAULT 'inbound',
            status VARCHAR(50) DEFAULT 'completed',
            duration INT UNSIGNED DEFAULT 0,
            recording_url VARCHAR(2083) DEFAULT NULL,
            transcript LONGTEXT DEFAULT NULL,
            summary LONGTEXT DEFAULT NULL,
            topic VARCHAR(255) DEFAULT NULL,
            ai_handled TINYINT(1) DEFAULT 0,
            contact_id BIGINT UNSIGNED DEFAULT NULL,
            tokens_used INT UNSIGNED DEFAULT 0,
            created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
            INDEX idx_user_id (user_id),
            INDEX idx_call_sid (call_sid),
            INDEX idx_status (status),
            INDEX idx_contact (contact_id)
        ) {$charset_collate};");
        $table_exists = vmo_table_exists($calls_table);
        $debug['steps'][] = $table_exists ? 'table_created' : 'table_create_failed';
    }
    $debug['table_exists'] = $table_exists;

    if (!$table_exists) {
        wp_send_json_success(array(
            'calls' => array(), 'total' => 0, 'limit' => $limit, 'offset' => $offset,
            'debug' => array_merge($debug, array('error' => 'Table could not be created'))
        ));
    }

    // ── Step 2: Verify critical columns exist (schema self-healing) ──
    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
    $cols = $wpdb->get_col("SHOW COLUMNS FROM {$calls_table}", 0);
    $debug['columns'] = is_array($cols) ? $cols : array();
    $required_cols = array('user_id', 'call_sid', 'caller_number', 'direction', 'status', 'duration', 'ai_handled', 'contact_id', 'tokens_used', 'created_at');
    $missing = array_diff($required_cols, is_array($cols) ? $cols : array());
    if (!empty($missing)) {
        $suppress = $wpdb->suppress_errors(true);
        $col_defs = array(
            'user_id' => 'BIGINT UNSIGNED NOT NULL DEFAULT 0',
            'call_sid' => 'VARCHAR(255) NOT NULL DEFAULT ""',
            'caller_number' => 'VARCHAR(50) DEFAULT NULL',
            'direction' => "VARCHAR(20) DEFAULT 'inbound'",
            'status' => "VARCHAR(50) DEFAULT 'completed'",
            'duration' => 'INT UNSIGNED DEFAULT 0',
            'recording_url' => 'VARCHAR(2083) DEFAULT NULL',
            'transcript' => 'LONGTEXT DEFAULT NULL',
            'summary' => 'LONGTEXT DEFAULT NULL',
            'ai_handled' => 'TINYINT(1) DEFAULT 0',
            'contact_id' => 'BIGINT UNSIGNED DEFAULT NULL',
            'created_at' => 'DATETIME DEFAULT CURRENT_TIMESTAMP',
        );
        foreach ($missing as $col) {
            if (isset($col_defs[$col])) {
                // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
                $wpdb->query("ALTER TABLE {$calls_table} ADD COLUMN {$col} {$col_defs[$col]}");
            }
        }
        $wpdb->suppress_errors($suppress);
        $debug['steps'][] = 'schema_repaired:' . implode(',', $missing);
    }

    // ── Step 3: Query calls — progressive fallback ──
    $total = 0;
    $calls = array();
    $source = 'none';

    // 3a: Try user's own calls (last 30 days)
    $where_tpl = "user_id = %d AND created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)";
    $where_params = array( $user_id );
    if ($contact_id > 0 && in_array('contact_id', is_array($cols) ? $cols : array(), true)) {
        $where_tpl .= " AND contact_id = %d";
        $where_params[] = $contact_id;
    }
    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- {$where_tpl} contains only placeholders; table name from internal constant
    $total = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$calls_table} WHERE {$where_tpl}", ...$where_params ) );
    if ($total > 0) {
        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- {$where_tpl} contains only placeholders; table name from internal constant
        $calls = $wpdb->get_results( $wpdb->prepare(
            "SELECT * FROM {$calls_table} WHERE {$where_tpl} ORDER BY created_at DESC LIMIT %d OFFSET %d",
            ...array_merge( $where_params, array( $limit, $offset ) )
        ), ARRAY_A );
        $source = 'user_calls';
    }

    // 3b: Admin fallback — all users, last 30 days
    if ($total === 0 && $is_admin) {
        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
        $total = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$calls_table} WHERE created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)");
        if ($total > 0) {
            $calls = $wpdb->get_results($wpdb->prepare(
                "SELECT * FROM {$calls_table} WHERE created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY) ORDER BY created_at DESC LIMIT %d OFFSET %d",
                $limit, $offset
            ), ARRAY_A);
            $source = 'admin_all_30d';
            // Fix ownership for orphaned calls
            $wpdb->query($wpdb->prepare(
                "UPDATE {$calls_table} SET user_id = %d WHERE user_id NOT IN (SELECT ID FROM {$wpdb->users})",
                $user_id
            ));
        }
    }

    // 3c: Admin fallback — all calls ever (maybe created_at is off due to timezone)
    if ($total === 0 && $is_admin) {
        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
        $total = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$calls_table}");
        if ($total > 0) {
            $calls = $wpdb->get_results($wpdb->prepare(
                "SELECT * FROM {$calls_table} ORDER BY id DESC LIMIT %d OFFSET %d",
                $limit, $offset
            ), ARRAY_A);
            $source = 'admin_all_ever';
        }
    }

    // 3d: Pull from power dialer calls table as additional source
    $pd_exists = vmo_table_exists($pd_table);
    if ($pd_exists) {
        $pd_where = $is_admin ? "1=1" : $wpdb->prepare("user_id = %d", $user_id);
        $pd_calls = $wpdb->get_results($wpdb->prepare(
            "SELECT call_sid, phone AS caller_number, 'outbound' AS direction, status, duration, created_at, user_id FROM {$pd_table} WHERE {$pd_where} ORDER BY created_at DESC LIMIT %d",
            $limit
        ), ARRAY_A);
        if (!empty($pd_calls)) {
            // Merge power dialer calls, avoiding duplicates by call_sid
            $existing_sids = array();
            foreach ($calls as $c) {
                $sid = $c['call_sid'] ?? '';
                if ($sid !== '') $existing_sids[$sid] = true;
            }
            foreach ($pd_calls as $pdc) {
                $sid = $pdc['call_sid'] ?? '';
                if ($sid !== '' && isset($existing_sids[$sid])) continue;
                $calls[] = $pdc;
                $total++;
            }
            if (empty($source) || $source === 'none') $source = 'power_dialer';
            else $source .= '+power_dialer';
        }
    }

    // 3e: Last resort for admins — fetch recent calls from Twilio API and backfill DB
    //     Uses direct wp_remote_get() because twilio_request() is private.
    if ($total === 0 && $is_admin) {
        try {
            $tw_sid = defined('VMO_TWILIO_ACCOUNT_SID') ? VMO_TWILIO_ACCOUNT_SID : get_option('vmo_twilio_account_sid', '');
            $tw_tok = defined('VMO_TWILIO_AUTH_TOKEN')  ? VMO_TWILIO_AUTH_TOKEN  : get_option('vmo_twilio_auth_token', '');
            $biz_phone = '';
            if (class_exists('VMO_Twilio_Manager')) {
                $mgr = VMO_Twilio_Manager::get_instance();
                if ($mgr && method_exists($mgr, 'get_user_phone')) {
                    $biz_phone = (string) ($mgr->get_user_phone($user_id) ?: '');
                }
            }
            $debug['twilio_backfill_phone'] = $biz_phone;
            $debug['twilio_backfill_has_creds'] = ($tw_sid !== '' && $tw_tok !== '');

            if ($tw_sid !== '' && $tw_tok !== '' && $biz_phone !== '') {
                $tw_base = 'https://api.twilio.com/2010-04-01/Accounts/' . $tw_sid;
                $tw_auth = array('Authorization' => 'Basic ' . base64_encode($tw_sid . ':' . $tw_tok)); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- HTTP Basic auth header
                $tw_args = array('headers' => $tw_auth, 'timeout' => 15);

                // Helper to fetch + parse Twilio calls
                $fetch_twilio = function($params) use ($tw_base, $tw_args) {
                    $url = add_query_arg($params, $tw_base . '/Calls.json');
                    $resp = wp_remote_get($url, $tw_args);
                    if (is_wp_error($resp)) return array();
                    $code = wp_remote_retrieve_response_code($resp);
                    if ($code >= 400) return array();
                    $body = json_decode(wp_remote_retrieve_body($resp), true);
                    return (!empty($body['calls']) && is_array($body['calls'])) ? $body['calls'] : array();
                };

                // Fetch inbound calls (To = business phone)
                $twilio_in = $fetch_twilio(array('To' => $biz_phone, 'PageSize' => 20));
                // Fetch outbound calls (From = business phone)
                $twilio_out = $fetch_twilio(array('From' => $biz_phone, 'PageSize' => 20));
                $all_twilio = array_merge($twilio_in, $twilio_out);

                $debug['twilio_api_inbound_count'] = count($twilio_in);
                $debug['twilio_api_outbound_count'] = count($twilio_out);

                $seen_sids = array();
                foreach ($all_twilio as $tc) {
                    $sid = $tc['sid'] ?? '';
                    if ($sid === '' || isset($seen_sids[$sid])) continue;
                    $seen_sids[$sid] = true;

                    $created_raw = $tc['date_created'] ?? '';
                    $created_mysql = '';
                    if ($created_raw) {
                        $ts = strtotime($created_raw);
                        // Use wp_date() so the stored timestamp matches WP site timezone,
                        // consistent with current_time('mysql') used elsewhere.
                        if ($ts) $created_mysql = wp_date('Y-m-d H:i:s', $ts);
                    }
                    $dir = ($tc['direction'] ?? '') === 'outbound-api' ? 'outbound' : 'inbound';
                    $dur = (int) ($tc['duration'] ?? 0);
                    $st  = strtolower($tc['status'] ?? 'completed');
                    $caller = ($dir === 'inbound') ? ($tc['from'] ?? '') : ($tc['to'] ?? '');

                    // Backfill into DB for future queries (ignore duplicate errors)
                    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                    $wpdb->insert($calls_table, array(
                        'user_id'       => $user_id,
                        'call_sid'      => $sid,
                        'caller_number' => $caller,
                        'direction'     => $dir,
                        'status'        => $st,
                        'duration'      => $dur,
                        'ai_handled'    => ($dir === 'inbound') ? 1 : 0,
                        'created_at'    => $created_mysql ?: current_time('mysql'),
                    ), array('%d', '%s', '%s', '%s', '%s', '%d', '%d', '%s'));

                    $calls[] = array(
                        'call_sid'      => $sid,
                        'caller_number' => $caller,
                        'direction'     => $dir,
                        'status'        => $st,
                        'duration'      => $dur,
                        'created_at'    => $created_mysql,
                        'user_id'       => $user_id,
                    );
                    $total++;
                }
                if ($total > 0) $source = 'twilio_api_backfill';
            }
        } catch (Throwable $e) {
            $debug['twilio_error'] = $e->getMessage();
        }
    }

    // Sort merged results by created_at DESC
    usort($calls, function($a, $b) {
        return strcmp($b['created_at'] ?? '', $a['created_at'] ?? '');
    });
    $calls = array_slice($calls, 0, $limit);

    $debug['source'] = $source;
    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
    $debug['total_in_table'] = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$calls_table}");
    $debug['pd_table_exists'] = $pd_exists;
    if ($pd_exists) {
        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
        $debug['pd_total'] = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$pd_table}");
    }

    // ── Step 4: Normalize fields for Strategic Dashboard UI ──
    $biz_number = '';
    if (class_exists('VMO_Twilio_Manager')) {
        try {
            $mgr = VMO_Twilio_Manager::get_instance();
            if ($mgr && method_exists($mgr, 'get_user_phone')) {
                $biz_number = (string) ($mgr->get_user_phone($user_id) ?: '');
            }
        } catch (Throwable $e) {
            $biz_number = '';
        }
    }

    $normalized = array();
    $contacts_table = $wpdb->prefix . 'vmo_contacts';
    $contacts_exist = vmo_table_exists($contacts_table);
    $contact_map = array();
    if ($contacts_exist && !empty($calls)) {
        $phones = array();
        foreach ($calls as $c) {
            $cn = preg_replace('/[^0-9]/', '', (string) ($c['caller_number'] ?? ''));
            if ($cn !== '') $phones[$cn] = true;
        }
        if ($phones) {
            // Use SELECT * to avoid 'Unknown column' errors from schema drift
            $rows = $wpdb->get_results($wpdb->prepare(
                "SELECT * FROM {$contacts_table} WHERE user_id = %d",
                $user_id
            ), ARRAY_A);
            foreach (($rows ?: array()) as $r) {
                $d = preg_replace('/[^0-9]/', '', (string) ($r['phone'] ?? ''));
                if ($d !== '') {
                    // Resolve name from whichever column exists
                    $cname = '';
                    if (!empty($r['full_name'])) {
                        $cname = (string) $r['full_name'];
                    } elseif (!empty($r['first_name']) || !empty($r['last_name'])) {
                        $cname = trim(($r['first_name'] ?? '') . ' ' . ($r['last_name'] ?? ''));
                    } elseif (!empty($r['name'])) {
                        $cname = (string) $r['name'];
                    }
                    $contact_map[$d] = array(
                        'name' => $cname,
                        'category' => (string) ($r['primary_category'] ?? 'lead'),
                    );
                }
            }
        }
    }
    // WordPress site timezone — used to annotate timestamps for browser conversion
    $wp_tz = wp_timezone();

    foreach (($calls ?: array()) as $c) {
        $direction = (string) ($c['direction'] ?? 'inbound');
        $status = (string) ($c['status'] ?? ($c['call_status'] ?? ''));
        $duration = (int) ($c['duration'] ?? ($c['call_duration'] ?? 0));
        $call_sid = (string) ($c['call_sid'] ?? '');
        $created = (string) ($c['created_at'] ?? ($c['call_start_time'] ?? ''));

        // Convert timestamp to ISO 8601 with timezone offset so the browser
        // can display it in the user's local time via toLocaleString().
        if ($created !== '') {
            try {
                $dt = new DateTime($created, $wp_tz);
                $created = $dt->format('c'); // e.g. "2026-02-21T04:41:11+00:00"
            } catch (Exception $e) {
                // keep original string on parse failure
            }
        }

        $other = (string) ($c['caller_number'] ?? ($c['caller'] ?? ''));
        if ($other === '' && isset($c['to_number'])) {
            $other = (string) $c['to_number'];
        }

        $from_number = ($direction === 'outbound') ? $biz_number : $other;
        $to_number = ($direction === 'outbound') ? $other : $biz_number;
        $rec = (string) ($c['recording_url'] ?? ($c['recording_local_path'] ?? ''));

        $caller_name = '';
        $caller_category = '';
        $other_digits = preg_replace('/[^0-9]/', '', $other);
        if ($other_digits !== '' && isset($contact_map[$other_digits])) {
            $caller_name = $contact_map[$other_digits]['name'];
            $caller_category = $contact_map[$other_digits]['category'];
        }

        $normalized[] = array(
            'call_sid' => $call_sid,
            'from_number' => $from_number,
            'to_number' => $to_number,
            'direction' => $direction,
            'call_start_time' => $created,
            'call_duration' => $duration,
            'call_status' => $status,
            'recording_url' => $rec,
            'caller_name' => $caller_name,
            'caller_category' => $caller_category,
            'summary' => (string) ($c['summary'] ?? ''),
            'transcript' => (string) ($c['transcript'] ?? ''),
        );
    }

    wp_send_json_success(array(
        'calls' => $normalized,
        'total' => intval($total),
        'limit' => $limit,
        'offset' => $offset,
        'debug' => $debug
    ));
});

// AJAX: Get recordings for a call (by Call SID)
add_action('wp_ajax_vmo_get_call_recordings', function() {
    if (!current_user_can('read')) {
        wp_send_json_error(array('message' => 'Forbidden'), 403);
    }
    if (!function_exists('vmo_verify_admin_security') || !vmo_verify_admin_security()) {
        wp_send_json_error(array('message' => 'Security check failed'), 403);
    }

    global $wpdb;
    $user_id = get_current_user_id();
    $call_sid = isset($_POST['call_sid']) ? sanitize_text_field(wp_unslash($_POST['call_sid'])) : '';
    if ($call_sid === '') {
        wp_send_json_error(array('message' => 'Missing call_sid'), 400);
    }

    $recording_url = '';
    $duration = 0;
    $created_at = '';
    $is_admin = current_user_can('manage_options');

    $phone_calls_table = $wpdb->prefix . 'vmo_phone_calls';
    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
    if ($wpdb->get_var("SHOW TABLES LIKE '{$phone_calls_table}'") === $phone_calls_table) {
        // Try user-scoped query first
        $row = $wpdb->get_row($wpdb->prepare(
            "SELECT recording_url, duration, created_at FROM {$phone_calls_table} WHERE user_id=%d AND call_sid=%s LIMIT 1",
            $user_id,
            $call_sid
        ), ARRAY_A);
        // Admin fallback: call_sid is globally unique; user_id may mismatch when
        // inbound calls are routed to a different user or stored with user_id=0.
        if (!is_array($row) && $is_admin) {
            $row = $wpdb->get_row($wpdb->prepare(
                "SELECT recording_url, duration, created_at FROM {$phone_calls_table} WHERE call_sid=%s LIMIT 1",
                $call_sid
            ), ARRAY_A);
        }
        if (is_array($row)) {
            $recording_url = (string) ($row['recording_url'] ?? '');
            $duration = (int) ($row['duration'] ?? 0);
            $created_at = (string) ($row['created_at'] ?? '');
        }
    }

    if ($recording_url === '') {
        $pd_calls_table = $wpdb->prefix . 'vmo_power_dialer_calls';
        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
        if ($wpdb->get_var("SHOW TABLES LIKE '{$pd_calls_table}'") === $pd_calls_table) {
            $row = $wpdb->get_row($wpdb->prepare(
                "SELECT recording_url, duration, created_at FROM {$pd_calls_table} WHERE user_id=%d AND call_sid=%s LIMIT 1",
                $user_id,
                $call_sid
            ), ARRAY_A);
            if (!is_array($row) && $is_admin) {
                $row = $wpdb->get_row($wpdb->prepare(
                    "SELECT recording_url, duration, created_at FROM {$pd_calls_table} WHERE call_sid=%s LIMIT 1",
                    $call_sid
                ), ARRAY_A);
            }
            if (is_array($row)) {
                $recording_url = (string) ($row['recording_url'] ?? '');
                $duration = (int) ($row['duration'] ?? 0);
                $created_at = (string) ($row['created_at'] ?? '');
            }
        }
    }

    // ── Twilio API fallback: use TwilioManager (proper credentials) ──
    $twilio_debug = array();
    if ($recording_url === '') {
        $twilio_source = 'none';
        if (class_exists('VMO_Twilio_Manager')) {
            $manager = VMO_Twilio_Manager::get_instance();
            $twilio_rec = $manager->fetch_call_recordings($call_sid);
            if (is_array($twilio_rec)) {
                $twilio_debug = $twilio_rec['debug'] ?? array();
                if (!empty($twilio_rec['url'])) {
                    $recording_url = $twilio_rec['url'];
                    $duration      = $twilio_rec['duration'] ?: $duration;
                    $created_at    = $twilio_rec['created'] ?: $created_at;
                    $twilio_source = $twilio_rec['source'];
                    if (defined('WP_DEBUG') && WP_DEBUG) error_log('[VMO Recording Playback] TwilioManager found recording for ' . $call_sid . ' via ' . $twilio_source);
                    // Persist to DB for instant future plays
                    if (isset($phone_calls_table)) {
                        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                        $wpdb->update($phone_calls_table, array('recording_url' => $recording_url), array('call_sid' => $call_sid));
                    }
                } else {
                    if (defined('WP_DEBUG') && WP_DEBUG) error_log('[VMO Recording Playback] TwilioManager found NO recording for ' . $call_sid . ' debug=' . wp_json_encode($twilio_debug));
                }
            } else {
                $twilio_debug['error'] = 'fetch returned null (simulation mode or empty sid)';
                if (defined('WP_DEBUG') && WP_DEBUG) error_log('[VMO Recording Playback] TwilioManager returned null for ' . $call_sid);
            }
        } else {
            $twilio_debug['error'] = 'VMO_Twilio_Manager class not available';
            if (defined('WP_DEBUG') && WP_DEBUG) error_log('[VMO Recording Playback] VMO_Twilio_Manager not available');
        }

    }

    // ── Transcript fallback when no recording exists ──
    $transcript_lines = array();
    if ($recording_url === '') {
        // Suppress DB errors so HTML doesn't pollute the JSON response
        $prev_suppress = $wpdb->suppress_errors(true);

        // 1. Try vmo_call_logs (actual schema: twilio_call_sid, transcription, ai_summary)
        $call_logs_table = $wpdb->prefix . 'vmo_call_logs';
        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
        if ($wpdb->get_var("SHOW TABLES LIKE '{$call_logs_table}'") === $call_logs_table) {
            // Check which columns exist — schema may vary between installs
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
            $cols = $wpdb->get_col("SHOW COLUMNS FROM {$call_logs_table}", 0);
            $cols_set = array_flip($cols);

            // Newer realtime schema has speaker/message/call_id columns
            if (isset($cols_set['speaker'], $cols_set['message'])) {
                $sid_col = isset($cols_set['call_sid']) ? 'call_sid' : (isset($cols_set['twilio_call_sid']) ? 'twilio_call_sid' : '');
                $time_col = isset($cols_set['timestamp']) ? 'timestamp' : (isset($cols_set['created_at']) ? 'created_at' : '');
                if ($sid_col !== '') {
                    $q = "SELECT speaker, message" . ($time_col ? ", {$time_col} AS ts" : "") . " FROM {$call_logs_table} WHERE {$sid_col}=%s ORDER BY id ASC LIMIT 200";
                    $logs = $wpdb->get_results($wpdb->prepare($q, $call_sid), ARRAY_A);
                    if (is_array($logs)) {
                        foreach ($logs as $l) {
                            $transcript_lines[] = array(
                                'speaker' => (string) ($l['speaker'] ?? ''),
                                'message' => (string) ($l['message'] ?? ''),
                                'time'    => (string) ($l['ts'] ?? ''),
                            );
                        }
                    }
                }
            }
            // Original schema has transcription/ai_summary as text blobs
            elseif (isset($cols_set['transcription'])) {
                $sid_col = isset($cols_set['twilio_call_sid']) ? 'twilio_call_sid' : (isset($cols_set['call_sid']) ? 'call_sid' : '');
                if ($sid_col !== '') {
                    $raw = (string) $wpdb->get_var($wpdb->prepare(
                        "SELECT COALESCE(transcription,'') FROM {$call_logs_table} WHERE {$sid_col}=%s LIMIT 1", $call_sid
                    ));
                    if ($raw !== '') {
                        $dec = json_decode($raw, true);
                        if (is_array($dec)) {
                            $entries = isset($dec['logs']) && is_array($dec['logs']) ? $dec['logs'] : $dec;
                            foreach ($entries as $e) {
                                if (!is_array($e)) continue;
                                $transcript_lines[] = array(
                                    'speaker' => (string) ($e['speaker'] ?? ($e['role'] ?? '')),
                                    'message' => (string) ($e['message'] ?? ($e['content'] ?? '')),
                                    'time'    => (string) ($e['timestamp'] ?? ($e['time'] ?? '')),
                                );
                            }
                        } else {
                            // Plain-text transcript
                            $transcript_lines[] = array('speaker' => '', 'message' => $raw, 'time' => '');
                        }
                    }
                }
            }
        }

        // 2. Fall back to vmo_phone_calls.transcript column
        if (empty($transcript_lines) && isset($phone_calls_table)) {
            $raw_t = (string) $wpdb->get_var($wpdb->prepare(
                "SELECT COALESCE(transcript,'') FROM {$phone_calls_table} WHERE call_sid=%s LIMIT 1",
                $call_sid
            ));
            if ($raw_t !== '') {
                $dec = json_decode($raw_t, true);
                if (is_array($dec)) {
                    $entries = isset($dec['logs']) && is_array($dec['logs']) ? $dec['logs'] : $dec;
                    foreach ($entries as $e) {
                        if (!is_array($e)) continue;
                        $transcript_lines[] = array(
                            'speaker' => (string) ($e['speaker'] ?? ($e['role'] ?? '')),
                            'message' => (string) ($e['message'] ?? ($e['content'] ?? '')),
                            'time'    => (string) ($e['timestamp'] ?? ($e['time'] ?? '')),
                        );
                    }
                } else {
                    // Plain-text transcript: "Caller: ...\nAI: ..." (stored by handle_ai_conversation)
                    $lines = preg_split('/\r?\n/', $raw_t);
                    foreach ($lines as $line) {
                        $line = trim($line);
                        if ($line === '') continue;
                        if (preg_match('/^(Caller|AI|User|Assistant|Bot|System)\s*:\s*(.+)$/i', $line, $m)) {
                            $speaker_raw = strtolower($m[1]);
                            $is_ai = in_array($speaker_raw, array('ai', 'assistant', 'bot', 'system'), true);
                            $transcript_lines[] = array(
                                'speaker' => $is_ai ? 'AI' : 'Caller',
                                'message' => trim($m[2]),
                                'time'    => '',
                            );
                        } else {
                            // Unprefixed line — treat as system message
                            $transcript_lines[] = array(
                                'speaker' => '',
                                'message' => $line,
                                'time'    => '',
                            );
                        }
                    }
                }
            }
        }

        $wpdb->suppress_errors($prev_suppress);

        // Sibling transcript search: if no transcript found and the call
        // is a simultaneous ring (outbound with no AI interaction), find
        // the related inbound call's transcript from the same time window.
        if (empty($transcript_lines) && $recording_url === '' && isset($phone_calls_table)) {
            $this_call = $wpdb->get_row($wpdb->prepare(
                "SELECT direction, created_at FROM {$phone_calls_table} WHERE call_sid = %s LIMIT 1",
                $call_sid
            ), ARRAY_A);
            if (is_array($this_call) && ($this_call['direction'] ?? '') === 'outbound' && !empty($this_call['created_at'])) {
                $ts = strtotime($this_call['created_at']);
                if ($ts) {
                    $w_start = date('Y-m-d H:i:s', $ts - 60);
                    $w_end   = date('Y-m-d H:i:s', $ts + 60);
                    $sibling_sid = $wpdb->get_var($wpdb->prepare(
                        "SELECT call_sid FROM {$phone_calls_table}
                         WHERE direction = 'inbound'
                           AND call_sid != %s
                           AND created_at BETWEEN %s AND %s
                         ORDER BY created_at DESC LIMIT 1",
                        $call_sid, $w_start, $w_end
                    ));
                    if ($sibling_sid) {
                        // Check vmo_call_logs for sibling transcript
                        $call_logs_table2 = $wpdb->prefix . 'vmo_call_logs';
                        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
                        if ($wpdb->get_var("SHOW TABLES LIKE '{$call_logs_table2}'") === $call_logs_table2) {
                            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
                            $cols2 = $wpdb->get_col("SHOW COLUMNS FROM {$call_logs_table2}", 0);
                            $cols2_set = array_flip($cols2 ?: array());
                            if (isset($cols2_set['speaker'], $cols2_set['message'])) {
                                $sid_col2 = isset($cols2_set['call_sid']) ? 'call_sid' : (isset($cols2_set['twilio_call_sid']) ? 'twilio_call_sid' : '');
                                if ($sid_col2 !== '') {
                                    $logs2 = $wpdb->get_results($wpdb->prepare(
                                        "SELECT speaker, message FROM {$call_logs_table2} WHERE {$sid_col2}=%s ORDER BY id ASC LIMIT 200",
                                        $sibling_sid
                                    ), ARRAY_A);
                                    if (is_array($logs2) && !empty($logs2)) {
                                        foreach ($logs2 as $l2) {
                                            $transcript_lines[] = array(
                                                'speaker' => (string) ($l2['speaker'] ?? ''),
                                                'message' => (string) ($l2['message'] ?? ''),
                                                'time'    => '',
                                            );
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    // Always return success; JS decides what to show based on recordings vs transcript.
    wp_send_json_success(array(
        'recordings' => $recording_url !== '' ? array(
            array(
                'recording_url' => $recording_url,
                'media_url' => $recording_url,
                'duration' => $duration,
                'date_created' => $created_at,
            ),
        ) : array(),
        'transcript' => $transcript_lines,
        'has_audio'  => $recording_url !== '',
        'debug'      => array(
            'db_had_url'   => $recording_url !== '' && empty($twilio_debug),
            'twilio_debug' => $twilio_debug,
            'call_sid'     => $call_sid,
        ),
    ));
});

// AJAX: Get call details (transcript/summary) by Call SID
add_action('wp_ajax_vmo_get_call_details', function() {
    if (!current_user_can('read')) {
        wp_send_json_error(array('message' => 'Forbidden'), 403);
    }
    if (!function_exists('vmo_verify_admin_security') || !vmo_verify_admin_security()) {
        wp_send_json_error(array('message' => 'Security check failed'), 403);
    }

    global $wpdb;
    $user_id = get_current_user_id();
    $call_sid = isset($_POST['call_sid']) ? sanitize_text_field(wp_unslash($_POST['call_sid'])) : '';
    if ($call_sid === '') {
        wp_send_json_error(array('message' => 'Missing call_sid'), 400);
    }

    $data = null;
    $phone_calls_table = $wpdb->prefix . 'vmo_phone_calls';
    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
    if ($wpdb->get_var("SHOW TABLES LIKE '{$phone_calls_table}'") === $phone_calls_table) {
        $data = $wpdb->get_row($wpdb->prepare(
            "SELECT * FROM {$phone_calls_table} WHERE user_id=%d AND call_sid=%s LIMIT 1",
            $user_id,
            $call_sid
        ), ARRAY_A);
    }

    if (!$data) {
        $pd_calls_table = $wpdb->prefix . 'vmo_power_dialer_calls';
        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
        if ($wpdb->get_var("SHOW TABLES LIKE '{$pd_calls_table}'") === $pd_calls_table) {
            $data = $wpdb->get_row($wpdb->prepare(
                "SELECT * FROM {$pd_calls_table} WHERE user_id=%d AND call_sid=%s LIMIT 1",
                $user_id,
                $call_sid
            ), ARRAY_A);
        }
    }

    if (!$data) {
        wp_send_json_error(array('message' => 'Call not found'), 404);
    }

    // Best-effort transcript extraction. Strategic dashboard expects `logs: [{speaker,message,timestamp}]`.
    $raw_transcript = '';
    foreach (array('transcript', 'transcription', 'conversation_data') as $k) {
        if (!empty($data[$k])) {
            $raw_transcript = (string) $data[$k];
            break;
        }
    }

    $logs = array();
    if ($raw_transcript !== '') {
        $decoded = json_decode($raw_transcript, true);
        if (is_array($decoded)) {
            // Accept either an array of entries or a {logs:[...]} structure
            $entries = $decoded;
            if (isset($decoded['logs']) && is_array($decoded['logs'])) {
                $entries = $decoded['logs'];
            }
            if (is_array($entries)) {
                foreach ($entries as $e) {
                    if (!is_array($e)) continue;
                    $logs[] = array(
                        'speaker' => (string) ($e['speaker'] ?? ($e['role'] ?? '')),
                        'message' => (string) ($e['message'] ?? ($e['content'] ?? '')),
                        'timestamp' => (string) ($e['timestamp'] ?? ($e['time'] ?? ($e['created_at'] ?? ''))),
                    );
                }
            }
        } else {
            // Plain text transcript (e.g. "Caller: hello\nAI: welcome...")
            // Parse Caller/AI prefixed lines into structured log entries
            $lines = preg_split('/\n/', $raw_transcript);
            $parsed_any = false;
            foreach ($lines as $line) {
                $line = trim($line);
                if ($line === '') continue;
                if (preg_match('/^(Caller|AI|Agent|User|System|TRANSCRIPT)\s*:\s*(.+)/i', $line, $m)) {
                    $logs[] = array(
                        'speaker' => strtoupper($m[1]),
                        'message' => trim($m[2]),
                        'timestamp' => '',
                    );
                    $parsed_any = true;
                } elseif ($parsed_any && !empty($logs)) {
                    // Continuation line — append to the last entry
                    $logs[count($logs) - 1]['message'] .= "\n" . $line;
                } else {
                    $logs[] = array(
                        'speaker' => 'TRANSCRIPT',
                        'message' => $line,
                        'timestamp' => '',
                    );
                }
            }
            // Set timestamp on first entry
            if (!empty($logs)) {
                $logs[0]['timestamp'] = (string) ($data['created_at'] ?? '');
            }
        }
    }

    wp_send_json_success(array(
        'call_sid' => (string) ($data['call_sid'] ?? $call_sid),
        'status' => (string) ($data['status'] ?? ($data['call_status'] ?? '')),
        'duration' => (int) ($data['duration'] ?? 0),
        'summary' => (string) ($data['summary'] ?? ($data['call_summary'] ?? '')),
        'logs' => $logs,
    ));
});

// AJAX: Upload ringtone to media library
add_action('wp_ajax_vmo_upload_ringtone', function() {
    if (!current_user_can('upload_files')) {
        wp_send_json_error(array('message' => 'Forbidden'), 403);
    }
    // Accept both nonces for compatibility
    $nonce_valid = false;
    if (isset($_POST['security']) && wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['security'])), 'vmo_builder_nonce')) {
        $nonce_valid = true;
    }
    if (isset($_POST['nonce']) && wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['nonce'])), 'vmo_admin_nonce')) {
        $nonce_valid = true;
    }
    if (!$nonce_valid) {
        wp_send_json_error(array('message' => 'Security check failed'), 403);
    }
    
    if (empty($_FILES['file'])) {
        wp_send_json_error(array('message' => 'No file uploaded'));
    }
    
    require_once(ABSPATH . 'wp-admin/includes/file.php');
    require_once(ABSPATH . 'wp-admin/includes/media.php');
    require_once(ABSPATH . 'wp-admin/includes/image.php');
    
    $file = $_FILES['file'];
    $upload_overrides = array('test_form' => false);
    $movefile = wp_handle_upload($file, $upload_overrides);
    
    if (isset($movefile['error'])) {
        wp_send_json_error(array('message' => $movefile['error']));
    }
    
    $attachment = array(
        'post_mime_type' => $movefile['type'],
        'post_title' => sanitize_file_name($file['name']),
        'post_content' => '',
        'post_status' => 'inherit'
    );
    
    $attach_id = wp_insert_attachment($attachment, $movefile['file']);
    $attach_data = wp_generate_attachment_metadata($attach_id, $movefile['file']);
    wp_update_attachment_metadata($attach_id, $attach_data);
    
    wp_send_json_success(array(
        'url' => $movefile['url'],
        'attachment_id' => $attach_id,
        'source_url' => wp_get_attachment_url($attach_id)
    ));
});

// === User Init — Internal Bypass + No Free Tokens ===
// No free tokens are granted. New users default to 'free' with 0 balance.
// They must purchase a plan to get tokens. Internal unmetered mode is explicit.
add_action('init', function () {
    if (is_user_logged_in()) {
        $user_id = get_current_user_id();
        $current_balance = get_user_meta($user_id, 'vmo_token_balance', true);
        
        // First-time user setup — NO tokens granted
        if ($current_balance === '') {
            update_user_meta($user_id, 'vmo_token_balance', 0);
            update_user_meta($user_id, 'vmo_subscription_status', 'free');
            update_user_meta($user_id, 'vmo_token_type', 'none');
        }
        
        if (function_exists('vmo_is_internal_unmetered_user') && vmo_is_internal_unmetered_user($user_id)) {
            update_user_meta($user_id, 'vmo_subscription_status', 'enterprise_pack');
            update_user_meta($user_id, 'vmo_plan_name', 'Master Account');
            
            if ((int)$current_balance < 5000000) {
                update_user_meta($user_id, 'vmo_token_balance', 10000000);
            }
        }
    }
});

// === AJAX Token Balance Endpoint ===
add_action('wp_ajax_vmo_get_token_balance', function () {
    check_ajax_referer('vmo_builder_nonce', 'security');
    
    // Get usage data with percentage and days remaining
    if (class_exists('VMO_Usage_Calculator')) {
        $usage_data = VMO_Usage_Calculator::get_usage_data(get_current_user_id());
        $credit_status = function_exists('vmo_get_credit_status') ? vmo_get_credit_status(get_current_user_id()) : [];
        wp_send_json_success([
            'balance' => $usage_data['current_balance'],
            'plan_tokens' => $credit_status['effective_plan_tokens'] ?? $usage_data['plan_tokens'],
            'remaining_percent' => $credit_status['remaining_percent'] ?? max(0, 100 - (float) $usage_data['usage_percent']),
            'usage_percent' => $usage_data['usage_percent'],
            'days_remaining' => $usage_data['days_remaining'],
            'low_credit' => !empty($credit_status['low_credit']),
            'no_credit' => !empty($credit_status['no_credit']),
            'credit_status' => $credit_status,
            'display' => round($usage_data['usage_percent']) . '% used • ' . $usage_data['days_remaining'] . ' days remaining'
        ]);
    } else {
        // Fallback to old behavior
        $balance = vmo_get_token_balance(get_current_user_id());
        $credit_status = function_exists('vmo_get_credit_status') ? vmo_get_credit_status(get_current_user_id()) : [];
        wp_send_json_success([
            'balance' => $balance,
            'plan_tokens' => $credit_status['effective_plan_tokens'] ?? 0,
            'remaining_percent' => $credit_status['remaining_percent'] ?? 0,
            'low_credit' => !empty($credit_status['low_credit']),
            'no_credit' => !empty($credit_status['no_credit']),
            'credit_status' => $credit_status,
        ]);
    }
});

// === AJAX Conversation Endpoints ===
// Note: These methods don't exist as static methods, so removing to prevent conflicts
// The conversation system uses vmo_conversation_chat action defined in generate-content.php

// === AJAX Business Consultation Endpoints ===
// The enhanced consultation system is the only active consultation route.

//=== TEMPLATE REGISTRATION ===//
add_filter('theme_page_templates', function($templates) {
    $templates['vmo-fullwidth-template.php'] = 'VMO Fullwidth Template';
    return $templates;
});

/* --------------------------------------------------------------
   Frontend runtime for published VMO pages

   Problem: older generator output is a full HTML document (doctype/html/head/body)
   but publishing stores it in post_content. Without wp_head/wp_footer, scripts in
   <head> never load reliably and the site looks unstyled/broken.

   Fix: (1) extract head/body/footer on save and store head/footer in post meta
        (2) output stored head/footer and enqueue baseline libs on VMO pages
        (3) template calls wp_head/wp_footer (see templates/vmo-fullwidth-template.php)
---------------------------------------------------------------- */

if (!function_exists('vmo_is_vmo_fullwidth_page')) {
    function vmo_is_vmo_fullwidth_page($post_id = null) {
        if (!$post_id) {
            $post_id = function_exists('get_queried_object_id') ? (int) get_queried_object_id() : 0;
        }
        if (!$post_id) return false;
        return get_post_meta($post_id, '_wp_page_template', true) === 'vmo-fullwidth-template.php';
    }
}

if (!function_exists('vmo_split_html_document')) {
    function vmo_split_html_document($html) {
        $html = (string) $html;
        $trim = ltrim($html);

        $looks_full = (bool) preg_match('/^\s*(<!doctype\b|<html\b)/i', $trim);
        $has_html_tag = (bool) preg_match('/<html\b/i', $html);
        if (!$looks_full && !$has_html_tag) {
            return [
                'is_full_document' => false,
                'head' => '',
                'body' => $html,
                'footer' => ''
            ];
        }

        $out = [
            'is_full_document' => true,
            'head' => '',
            'body' => $html,
            'footer' => ''
        ];

        if (!class_exists('DOMDocument')) {
            // Regex fallback
            if (preg_match('/<head\b[^>]*>([\s\S]*?)<\/head>/i', $html, $m)) {
                $out['head'] = $m[1];
            }
            if (preg_match('/<body\b[^>]*>([\s\S]*?)<\/body>/i', $html, $m)) {
                $out['body'] = $m[1];
            }
            return $out;
        }

        $prev = libxml_use_internal_errors(true);
        $dom = new DOMDocument();
        // Best-effort parse; tolerate imperfect HTML from LLM output.
        $dom->loadHTML($html, LIBXML_NOWARNING | LIBXML_NOERROR);

        $to_inner_html = static function($node) use ($dom) {
            if (!$node) return '';
            $buf = '';
            foreach ($node->childNodes as $child) {
                $buf .= $dom->saveHTML($child);
            }
            return $buf;
        };

        $headNode = $dom->getElementsByTagName('head')->item(0);
        $bodyNode = $dom->getElementsByTagName('body')->item(0);

        $out['head'] = $to_inner_html($headNode);

        // Pull scripts out of body into a footer bucket (helps prevent inline scripts
        // getting neutered inside post_content and ensures correct placement).
        if ($bodyNode) {
            $footer = '';
            $scripts = [];
            foreach ($bodyNode->getElementsByTagName('script') as $script) {
                $scripts[] = $script;
            }
            foreach ($scripts as $script) {
                $footer .= $dom->saveHTML($script);
                if ($script->parentNode) {
                    $script->parentNode->removeChild($script);
                }
            }
            $out['footer'] = $footer;
            $out['body'] = $to_inner_html($bodyNode);
        }

        libxml_clear_errors();
        libxml_use_internal_errors($prev);
        return $out;
    }
}

if (!function_exists('vmo_kses_generated_head_footer')) {
    function vmo_kses_generated_head_footer($html) {
        // Allow a controlled subset of tags needed for generated sites.
        $allowed = [
            'meta' => [
                'charset' => true,
                'name' => true,
                'content' => true,
                'property' => true,
                'http-equiv' => true,
            ],
            'link' => [
                'rel' => true,
                'href' => true,
                'as' => true,
                'type' => true,
                'media' => true,
                'sizes' => true,
                'crossorigin' => true,
                'integrity' => true,
                'referrerpolicy' => true,
            ],
            'style' => [
                'type' => true,
                'media' => true,
                'id' => true,
            ],
            'script' => [
                'src' => true,
                'type' => true,
                'async' => true,
                'defer' => true,
                'nomodule' => true,
                'crossorigin' => true,
                'integrity' => true,
                'referrerpolicy' => true,
                'id' => true,
            ],
            'title' => [],
            'base' => [ 'href' => true, 'target' => true ],
        ];
        return wp_kses((string) $html, $allowed);
    }
}

// Enqueue a baseline runtime for VMO-staged pages; exported sites must remain standalone HTML/CSS/JS.
add_action('wp_enqueue_scripts', function() {
    if (is_admin()) return;
    if (!is_singular()) return;
    $post_id = (int) get_queried_object_id();
    if (!$post_id || !vmo_is_vmo_fullwidth_page($post_id)) return;
    $post_content = (string) get_post_field('post_content', $post_id);
    $visual_preset = sanitize_key((string) get_post_meta($post_id, 'vmo_visual_preset', true));
    $motion_signature = strtolower($visual_preset . ' ' . $post_content);

    // Tailwind CSS (bundled locally for generated site preview)
    wp_enqueue_script('vmo-tailwind-cdn', VMO_AI_BUILDER_URL . 'assets/vendor/tailwindcss.min.js', [], '3.4.0', false);

    // GSAP + ScrollTrigger for cinematic motion hooks (bundled locally)
    wp_enqueue_script('vmo-gsap', VMO_AI_BUILDER_URL . 'assets/vendor/gsap.min.js', [], '3.12.2', true);
    wp_enqueue_script('vmo-gsap-scrolltrigger', VMO_AI_BUILDER_URL . 'assets/vendor/ScrollTrigger.min.js', ['vmo-gsap'], '3.12.2', true);

    if (preg_match('/lenis|smooth|scroll|parallax|cinematic|story|fullscreen|pin|scrolltrigger/', $motion_signature)) {
        wp_enqueue_script('vmo-lenis', 'https://cdn.jsdelivr.net/npm/lenis@1.1.13/dist/lenis.min.js', [], '1.1.13', true);
    }

    if (preg_match('/swiper|carousel|slider|testimonial|gallery|product_story|click_to_detail/', $motion_signature)) {
        wp_enqueue_style('vmo-swiper', 'https://unpkg.com/swiper@11/swiper-bundle.min.css', [], '11.0.0');
        wp_enqueue_script('vmo-swiper', 'https://unpkg.com/swiper@11/swiper-bundle.min.js', [], '11.0.0', true);
    }

    $motion_runtime_path = VMO_AI_BUILDER_DIR . 'assets/vmo-motion-runtime.js';
    if (file_exists($motion_runtime_path)) {
        $motion_deps = array('vmo-gsap-scrolltrigger');
        if (wp_script_is('vmo-lenis', 'enqueued')) {
            $motion_deps[] = 'vmo-lenis';
        }
        if (wp_script_is('vmo-swiper', 'enqueued')) {
            $motion_deps[] = 'vmo-swiper';
        }
        wp_enqueue_script('vmo-motion-runtime', VMO_AI_BUILDER_URL . 'assets/vmo-motion-runtime.js', $motion_deps, filemtime($motion_runtime_path), true);
        $agent_plan = json_decode((string) get_post_meta($post_id, 'vmo_v2_agent_plan', true), true);
        $motion_plan = is_array($agent_plan['agents']['motion_experience_director']['result']['motion_plan'] ?? null)
            ? $agent_plan['agents']['motion_experience_director']['result']['motion_plan']
            : array();
        wp_add_inline_script('vmo-motion-runtime', 'window.vmoWebsiteMotionRuntime = ' . wp_json_encode(array(
            'pipelineVersion' => (string) get_post_meta($post_id, 'vmo_canonical_pipeline_version', true),
            'visualPreset' => $visual_preset,
            'motionPresets' => is_array($motion_plan['motion_presets'] ?? null) ? array_values($motion_plan['motion_presets']) : array(),
            'reducedMotionRequired' => true,
        )) . ';', 'before');
    }

    // AOS animation library (bundled locally)
    wp_enqueue_style('vmo-aos', VMO_AI_BUILDER_URL . 'assets/vendor/aos.css', [], '2.3.1');
    wp_enqueue_script('vmo-aos', VMO_AI_BUILDER_URL . 'assets/vendor/aos.min.js', [], '2.3.1', true);

    $forms_runtime_path = VMO_AI_BUILDER_DIR . 'assets/vmo-forms.js';
    if (file_exists($forms_runtime_path) && preg_match('/\b(vmo-form|vmo-form-submit|data-vmo-form|data-form-type)\b/i', $post_content)) {
        $site_owner_id = (int) get_post_meta($post_id, 'vmo_site_owner_id', true);
        if ($site_owner_id <= 0) {
            $site_owner_id = (int) get_option('vmo_site_owner_id', 0);
        }
        if ($site_owner_id <= 0) {
            $site_owner_id = (int) get_post_field('post_author', $post_id);
        }

        $form_secret = '';
        if ($site_owner_id > 0) {
            $form_secret = get_user_meta($site_owner_id, 'vmo_form_contract_secret', true);
            $form_secret = is_string($form_secret) ? trim($form_secret) : '';
            if (strlen($form_secret) < 32) {
                $form_secret = wp_generate_password(64, false, false);
                update_user_meta($site_owner_id, 'vmo_form_contract_secret', $form_secret);
            }
        }

        $issued_at = time();
        $form_token = '';
        if ($site_owner_id > 0 && $form_secret !== '') {
            $signature = hash_hmac('sha256', $site_owner_id . '|*|' . $issued_at, $form_secret);
            $form_token = rtrim(strtr(base64_encode($site_owner_id . ':*:' . $issued_at . ':' . $signature), '+/', '-_'), '=');
        }

        wp_enqueue_script('vmo-forms', VMO_AI_BUILDER_URL . 'assets/vmo-forms.js', ['jquery'], filemtime($forms_runtime_path), true);
        wp_localize_script('vmo-forms', 'vmo_form_config', [
            'ajaxurl' => admin_url('admin-ajax.php'),
            'site_owner' => $site_owner_id,
            'form_token' => $form_token,
        ]);
    }

    // 360° Product Spin Viewer (for interactive product rotation)
    $spin_viewer_path = VMO_AI_BUILDER_DIR . 'assets/vmo-360-spin-viewer.js';
    if (file_exists($spin_viewer_path)) {
        wp_enqueue_script('vmo-360-spin-viewer', VMO_AI_BUILDER_URL . 'assets/vmo-360-spin-viewer.js', [], filemtime($spin_viewer_path), true);
    }

    // Animated Overlays System (clouds, smoke, particles, ripples, etc.)
    $overlays_path = VMO_AI_BUILDER_DIR . 'assets/vmo-animated-overlays.js';
    if (file_exists($overlays_path)) {
        wp_enqueue_script('vmo-animated-overlays', VMO_AI_BUILDER_URL . 'assets/vmo-animated-overlays.js', [], filemtime($overlays_path), true);
    }

    // Minimal initializer for AOS (no-op if absent)
    wp_add_inline_script('vmo-aos', 'window.AOS && AOS.init({ once: true, duration: 650, easing: "ease-out" });');
}, 20);

// ══════════════════════════════════════════════════════════════════
// Frontend: Enqueue VMO Visit Tracker on ALL public pages
// Sends visitor analytics data to class-vmo-analytics-tracker.php
// ══════════════════════════════════════════════════════════════════
add_action('wp_enqueue_scripts', function() {
    if (is_admin()) return;

    $tracker_path = VMO_AI_BUILDER_DIR . 'assets/vmo-visit-tracker.js';
    if (!file_exists($tracker_path)) return;

    // Determine the site owner who installed VMO (for multi-tenant analytics)
    $site_owner_id = get_option('vmo_site_owner_id', 1);

    wp_enqueue_script(
        'vmo-visit-tracker',
        VMO_AI_BUILDER_URL . 'assets/vmo-visit-tracker.js',
        [],
        filemtime($tracker_path),
        true
    );
    wp_localize_script('vmo-visit-tracker', 'vmo_tracker_data', [
        'ajaxurl'       => admin_url('admin-ajax.php'),
        'site_user_id'  => (int) $site_owner_id,
    ]);
}, 5);

// Inject extracted <head> assets saved during publish
add_action('wp_head', function() {
    if (is_admin()) return;
    if (!is_singular()) return;
    $post_id = (int) get_queried_object_id();
    if (!$post_id || !vmo_is_vmo_fullwidth_page($post_id)) return;

    $head = (string) get_post_meta($post_id, 'vmo_generated_head_html', true);
    if ($head) {
        echo vmo_kses_generated_head_footer($head); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
    }
}, 5);

// Inject extracted footer scripts saved during publish
add_action('wp_footer', function() {
    if (is_admin()) return;
    if (!is_singular()) return;
    $post_id = (int) get_queried_object_id();
    if (!$post_id || !vmo_is_vmo_fullwidth_page($post_id)) return;

    $footer = (string) get_post_meta($post_id, 'vmo_generated_footer_html', true);
    if ($footer) {
        echo vmo_kses_generated_head_footer($footer); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
    }
}, 50);

// === Force Template on Publish ===
add_action('save_post_page', function($post_id) {
    // Only set if not already set
    if (get_post_meta($post_id, '_wp_page_template', true) !== 'vmo-fullwidth-template.php') {
        update_post_meta($post_id, '_wp_page_template', 'vmo-fullwidth-template.php');
    }
}, 10, 1);

// === Use Custom Template for Published Pages ===
add_filter('template_include', function($template) {
    // Only apply to frontend pages, never admin
    if (is_admin()) {
        return $template;
    }
    
    if (is_page()) {
        $selected = get_page_template_slug(get_queried_object_id());
        if ($selected === 'vmo-fullwidth-template.php') {
            return plugin_dir_path(__FILE__) . 'templates/vmo-fullwidth-template.php';
        }
    }
    return $template;
});

// === VMO AUTOMATIC SERVICE PROVISIONING CRON HOOKS ===
add_action('init', function() {
    // Only register cron hooks after WordPress and classes are fully loaded
    
    // SEO Automation Cron Hooks
    add_action('vmo_seo_audit', function($user_id) {
        if (class_exists('VMO_Enhanced_Business_Wizard')) {
            $wizard = VMO_Enhanced_Business_Wizard::get_instance();
            $wizard->execute_seo_audit($user_id);
        }
    });

    add_action('vmo_generate_automated_blog_post', function($user_id) {
        if (class_exists('VMO_Enhanced_Business_Wizard')) {
            $wizard = VMO_Enhanced_Business_Wizard::get_instance();
            $wizard->execute_automated_blog_generation($user_id);
        }
    });

    add_action('vmo_seo_competitive_analysis', function($user_id) {
        if (class_exists('VMO_Enhanced_Business_Wizard')) {
            $wizard = VMO_Enhanced_Business_Wizard::get_instance();
            $wizard->execute_seo_competitive_analysis($user_id);
        }
    });

    add_action('vmo_seo_strategy_review', function($user_id) {
        if (class_exists('VMO_Enhanced_Business_Wizard')) {
            $wizard = VMO_Enhanced_Business_Wizard::get_instance();
            $wizard->execute_seo_strategy_review($user_id);
        }
    });

    // Email Marketing Automation Cron Hooks
    add_action('vmo_email_performance_analysis', function($user_id) {
        if (class_exists('VMO_Enhanced_Business_Wizard')) {
            $wizard = VMO_Enhanced_Business_Wizard::get_instance();
            $wizard->execute_email_performance_analysis($user_id);
        }
    });

    add_action('vmo_email_optimization_check', function($user_id) {
        if (class_exists('VMO_Enhanced_Business_Wizard')) {
            $wizard = VMO_Enhanced_Business_Wizard::get_instance();
            $wizard->execute_email_optimization_check($user_id);
        }
    });

    add_action('vmo_email_competitive_analysis', function($user_id) {
        if (class_exists('VMO_Enhanced_Business_Wizard')) {
            $wizard = VMO_Enhanced_Business_Wizard::get_instance();
            $wizard->execute_email_competitive_analysis($user_id);
        }
    });

    add_action('vmo_email_strategy_review', function($user_id) {
        if (class_exists('VMO_Enhanced_Business_Wizard')) {
            $wizard = VMO_Enhanced_Business_Wizard::get_instance();
            $wizard->execute_email_strategy_review($user_id);
        }
    });

    // COMPREHENSIVE WEEKLY EMAIL AUTOMATION with Deep Research
    add_action('vmo_weekly_email_research', function($user_id) {
        if (class_exists('VMO_Enhanced_Business_Wizard')) {
            $wizard = VMO_Enhanced_Business_Wizard::get_instance();
            $wizard->execute_weekly_email_research($user_id);
        }
    });

    // CONTACT MANAGEMENT AUTOMATION
    add_action('vmo_sync_all_contacts', function($user_id) {
        if (class_exists('VMO_Enhanced_Business_Wizard')) {
            $wizard = VMO_Enhanced_Business_Wizard::get_instance();
            $wizard->execute_contact_sync($user_id);
        }
    });

    add_action('vmo_contact_engagement_action', function($user_id, $contact_id, $action) {
        if (class_exists('VMO_Enhanced_Business_Wizard')) {
            $wizard = VMO_Enhanced_Business_Wizard::get_instance();
            // Execute engagement action based on contact category and strategy
            $wizard->execute_contact_engagement($user_id, $contact_id, $action);
        }
    });
});

// Voice System AJAX Handlers
add_action('wp_ajax_vmo_start_conversation', 'vmo_handle_start_conversation');
function vmo_handle_start_conversation() {
    if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['security'])), 'vmo_builder_nonce')) {
        wp_send_json_error(array('message' => 'Security check failed'));
        return;
    }
    
    if (!is_user_logged_in()) {
        wp_send_json_error(array('message' => 'User not logged in'));
        return;
    }
    
    // Generate conversation ID
    $conversation_id = 'conv_' . get_current_user_id() . '_' . time();
    
    wp_send_json_success(array(
        'conversation_id' => $conversation_id,
        'message' => 'Conversation started',
        'status' => 'ready'
    ));
}

add_action('wp_ajax_vmo_send_message', 'vmo_handle_send_message');
function vmo_handle_send_message() {
    if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['security'])), 'vmo_builder_nonce')) {
        wp_send_json_error(array('message' => 'Security check failed'));
        return;
    }
    
    if (!is_user_logged_in()) {
        wp_send_json_error(array('message' => 'User not logged in'));
        return;
    }
    
    $message = sanitize_text_field(wp_unslash($_POST['message']));
    $conversation_id = sanitize_text_field(wp_unslash($_POST['conversation_id']));
    
    if (empty($message)) {
        wp_send_json_error(array('message' => 'Message cannot be empty'));
        return;
    }
    
    // Process message with OpenAI
    $api_key = get_option('vmo_openai_api_key');
    if (empty($api_key)) {
        wp_send_json_error(array('message' => 'AI API key not configured'));
        return;
    }
    
    // Check token balance
    $user_id = get_current_user_id();
    $token_balance = (int)get_user_meta($user_id, 'vmo_token_balance', true);
    
    if ($token_balance < 50) { // 50 tokens for voice conversation
        wp_send_json_error(array('message' => 'Insufficient token balance'));
        return;
    }
    
    // Rate limit AI calls (voice: 10/min, 60/hr)
    $rate_check = vmo_check_ai_rate_limit($user_id, 'voice_msg', 10, 60);
    if (is_wp_error($rate_check)) { wp_send_json_error(array('message' => $rate_check->get_error_message())); return; }
    
    // Call OpenAI API (Guardrail 5A: adaptive model fallback)
    $_voice_model = function_exists('vmo_resolve_model')
        ? vmo_resolve_model($user_id, function_exists('vmo_get_model') ? vmo_get_model('fast') : 'gpt-5.5-mini')['model']
        : (function_exists('vmo_get_model') ? vmo_get_model('fast') : 'gpt-5.5-mini');
    $response = wp_remote_post('https://api.openai.com/v1/chat/completions', array(
        'timeout' => 30,
        'headers' => array(
            'Authorization' => 'Bearer ' . $api_key,
            'Content-Type' => 'application/json',
        ),
        'body' => json_encode(array(
            'model' => $_voice_model,
            'messages' => array(
                array(
                    'role' => 'system',
                    'content' => 'You are a helpful AI assistant for the VMO Website Builder. Provide concise, helpful responses that are suitable for voice conversation. Keep responses under 100 words.'
                ),
                array(
                    'role' => 'user',
                    'content' => $message
                )
            ),
            'max_tokens' => 250,
            'temperature' => 0.7
        )),
        'timeout' => 30
    ));
    
    if (is_wp_error($response)) {
        wp_send_json_error(array('message' => 'Failed to connect to AI service'));
        return;
    }
    
    $body = wp_remote_retrieve_body($response);
    $data = json_decode($body, true);
    
    if (!isset($data['choices'][0]['message']['content'])) {
        wp_send_json_error(array('message' => 'Invalid API response'));
        return;
    }
    
    // Deduct tokens through the central token registry.
    if (function_exists('vmo_deduct_tokens')) {
        $token_cost = function_exists('vmo_get_token_cost') ? (int) vmo_get_token_cost('chat') : 50;
        vmo_deduct_tokens($user_id, $token_cost, 'chat', 'Voice/Text Conversation');
    } else {
        update_user_meta($user_id, 'vmo_token_balance', max(0, $token_balance - 50));
    }
    
    wp_send_json_success(array(
        'response' => $data['choices'][0]['message']['content'],
        'conversation_id' => $conversation_id,
        'tokens_used' => 50
    ));
}

add_action('wp_ajax_vmo_conversation_message', 'vmo_handle_conversation_message');
function vmo_handle_conversation_message() {
    // Alias for vmo_send_message for compatibility
    vmo_handle_send_message();
}

// VOICE SYSTEM REMOVED: All voice API endpoints and functions removed for No Voice version

// === DEDICATED AI AGENT ENDPOINTS ===

if (!function_exists('vmo_extract_json_object_from_text')) {
    function vmo_extract_json_object_from_text($raw_text) {
        $raw_text = is_string($raw_text) ? trim($raw_text) : '';
        if ($raw_text === '') {
            return null;
        }

        $clean = preg_replace('/```(?:json)?(.*?)```/is', '$1', $raw_text) ?? $raw_text;
        $decoded = json_decode($clean, true);
        if (is_array($decoded)) {
            return $decoded;
        }

        $start = strpos($clean, '{');
        $end = strrpos($clean, '}');
        if ($start === false || $end === false || $end <= $start) {
            return null;
        }

        $candidate = substr($clean, $start, ($end - $start) + 1);
        $decoded = json_decode($candidate, true);
        return is_array($decoded) ? $decoded : null;
    }
}

// Content Refinement Agent  
add_action('wp_ajax_vmo_refine_content', 'vmo_handle_content_refinement');
if (!function_exists('vmo_refinement_strip_code_fences')) {
    function vmo_refinement_strip_code_fences($content) {
        $content = trim((string) $content);
        if (preg_match('/```(?:html)?\s*(.*?)```/is', $content, $matches)) {
            return trim($matches[1]);
        }
        return $content;
    }
}

if (!function_exists('vmo_refinement_requires_canonical_regeneration')) {
    function vmo_refinement_requires_canonical_regeneration($prompt, $agent_type) {
        $haystack = strtolower((string) $prompt . ' ' . (string) $agent_type);
        return (bool) preg_match('/\b(full|entire|whole|all)\s+(website|site|page|homepage|layout|design)\b|\b(regenerate|rebuild|recreate|generate)\s+(the\s+)?(website|site|page|homepage)\b/', $haystack);
    }
}

if (!function_exists('vmo_refinement_has_forbidden_runtime_dependency')) {
    function vmo_refinement_has_forbidden_runtime_dependency($html) {
        $html = (string) $html;
        if (preg_match('/admin-ajax\.php|\/wp-json\/|wp-content\/plugins|wp-content\/themes|wp-includes|window\.wp\b|wp\.apiFetch|<\?php/i', $html)) {
            return true;
        }
        if (preg_match('/(^|[\s>])\[(?!generated_image|generated_video)([a-z][a-z0-9_:-]*)(?:\s+[^\]]*)?\](?:\s*\[\/\2\])?/i', $html, $matches)) {
            $shortcode_name = strtolower((string) ($matches[2] ?? ''));
            if ($shortcode_name !== '' && strpos($shortcode_name, 'data-') !== 0 && strpos($shortcode_name, 'aria-') !== 0) {
                return true;
            }
        }
        return false;
    }
}

if (!function_exists('vmo_build_component_refinement_prompt')) {
    function vmo_build_component_refinement_prompt($prompt, $current_html, array $component_state, $section_index, $agent_type, $current_page) {
        $component_context = wp_json_encode(array(
            'component_state_version' => $component_state['version'] ?? 'v2.0-2026-04-30',
            'source_of_truth' => $component_state['sourceOfTruth'] ?? 'react_style_component_state',
            'render_bridge' => $component_state['renderBridge'] ?? 'standalone_html_css_js',
            'current_page' => $current_page,
            'section_index' => (int) $section_index,
            'agent_type' => $agent_type,
            'component_count' => is_array($component_state['components'] ?? null) ? count($component_state['components']) : 0,
        ));

        return "You are VMO_Agent_Website_Refinement_Assistant in the v2.0 canonical website system.\n"
            . "You refine a generated standalone premium website. WordPress is only the VMO business-suite shell; editing happens inside the VMO builder/template, not WordPress editors, and the public website must run as plain HTML/CSS/JS without WordPress at runtime.\n"
            . "Do not break its design system, component state, media queue, forms, payments, maps, SEO, tracking, or mobile behavior.\n\n"
            . "COMPONENT STATE CONTRACT:\n{$component_context}\n\n"
            . "CURRENT HTML FRAGMENT:\n{$current_html}\n\n"
            . "USER REFINEMENT REQUEST:\n{$prompt}\n\n"
            . "NON-NEGOTIABLE RULES:\n"
            . "1. Return only the complete refined HTML fragment. No markdown, no code fences, no explanation.\n"
            . "2. Preserve existing section identity, useful classes, IDs, data-vmo-* attributes, media placeholders, tracking attributes, forms, portable payment links/attributes, map embeds, and responsive structure unless the request explicitly changes them.\n"
            . "3. Keep the output standalone-safe and builder-safe. Do not add external script tags, inline event handlers, unsafe iframes, unrelated libraries, shortcodes, wp-admin/admin-ajax/wp-json calls, wp-content plugin/theme URLs, window.wp references, or PHP.\n"
            . "4. For media changes, use queueable VMO markers such as data-vmo-generate/data-vmo-prompt or data-vmo-sora/data-vmo-sora-prompt instead of fake final asset URLs; those markers are builder-time only and must resolve before export.\n"
            . "5. For style changes, keep the existing visual preset and motion grammar; use scoped classes/attributes and mobile-safe CSS.\n"
            . "6. If the user asks for text, SEO, mobile, hero, menu, section, color, media, or layout changes, make only that change and leave surrounding content intact.\n"
            . "7. Direct publishing is not allowed; this is a draft refinement only.";
    }
}

function vmo_handle_content_refinement() {
    if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['security'] ?? '')), 'vmo_builder_nonce')) {
        wp_send_json_error(array('message' => 'Security check failed'));
        return;
    }
    
    if (!is_user_logged_in()) {
        wp_send_json_error(array('message' => 'User not logged in'));
        return;
    }
    
    // ✅ TOKEN DEDUCTION v8.1.0: Charge for content refinement
    $user_id = get_current_user_id();
    if (function_exists('vmo_deduct_tokens')) {
        $token_cost = function_exists('vmo_get_token_cost') ? (int) vmo_get_token_cost('refinement') : 225;
        if (!vmo_deduct_tokens($user_id, $token_cost, 'refinement', 'Content refinement')) {
            wp_send_json_error(array('message' => 'Insufficient tokens. Please top up your balance.'));
            return;
        }
    }
    
    $prompt = trim((string) wp_unslash($_POST['prompt'] ?? ''));
    $current_html = trim((string) wp_unslash($_POST['current_html'] ?? ''));
    $component_state_raw = (string) wp_unslash($_POST['component_state'] ?? '{}');
    $component_state = json_decode($component_state_raw, true);
    if (!is_array($component_state)) {
        $component_state = array();
    }
    $current_page = sanitize_key((string) wp_unslash($_POST['current_page'] ?? ''));
    $post_id = intval($_POST['post_id'] ?? ($_POST['project_id'] ?? 0));
    $section_index = intval($_POST['section_index']);
    $agent_type = sanitize_text_field(wp_unslash($_POST['agent_type'] ?? 'website_refinement_assistant'));
    
    if (empty($prompt)) {
        wp_send_json_error(array('message' => 'Refinement prompt is required'));
        return;
    }

    if ($post_id > 0 && vmo_refinement_requires_canonical_regeneration($prompt, $agent_type) && class_exists('VMO_Canonical_Website_Pipeline')) {
        $canonical_result = VMO_Canonical_Website_Pipeline::get_instance()->run_regeneration($post_id, array(
            'source' => 'website_refinement_assistant',
            'prompt' => $prompt,
            'instruction' => $prompt,
            'section_index' => $section_index,
            'current_page' => $current_page,
            'component_state' => $component_state,
        ));
        if (is_wp_error($canonical_result)) {
            wp_send_json_error(array('message' => $canonical_result->get_error_message()));
            return;
        }
        wp_send_json_success(array_merge(is_array($canonical_result) ? $canonical_result : array(), array(
            'canonical_pipeline' => true,
            'section_index' => $section_index,
            'agent_type' => 'website_refinement_assistant',
        )));
        return;
    }
    
    $enhanced_prompt = vmo_build_component_refinement_prompt($prompt, $current_html, $component_state, $section_index, $agent_type, $current_page);
    
    // Get OpenAI API key
    $api_key = get_option('vmo_openai_api_key', '');
    if ((!defined('VMO_CLIENT_MODE') || VMO_CLIENT_MODE !== true) && empty($api_key)) {
        wp_send_json_error(array('message' => 'AI API key not configured'));
        return;
    }
    
    // Rate limit AI calls
    $rate_check = vmo_check_ai_rate_limit($user_id, 'refine', 5, 30);
    if (is_wp_error($rate_check)) { wp_send_json_error(array('message' => $rate_check->get_error_message())); return; }
    
    $model = function_exists('vmo_get_website_agent_model')
        ? vmo_get_website_agent_model('website_refinement_assistant')
        : (function_exists('vmo_get_model') ? vmo_get_model('strategic') : VMO_OPENAI_MODEL);
    $messages = array(
        array(
            'role' => 'system',
            'content' => 'You are the v2.0 VMO website refinement assistant. Preserve component state, VMO builder/template ownership, standalone HTML/CSS/JS portability, media queue markers, dashboard tracking, and premium visual consistency. WordPress editors are not the editing surface.'
        ),
        array(
            'role' => 'user',
            'content' => $enhanced_prompt
        ),
    );
    $response = function_exists('vmo_openai_chat')
        ? vmo_openai_chat($messages, $model, array('timeout' => 55, 'max_tokens' => 4000, 'temperature' => 0.25, 'vmo_user_id' => $user_id))
        : new WP_Error('vmo_openai_helper_missing', 'AI refinement helper is unavailable.');
    
    if (is_wp_error($response)) {
        wp_send_json_error(array('message' => $response->get_error_message() ?: 'Failed to connect to AI service'));
        return;
    }
    
    $body = wp_remote_retrieve_body($response);
    $data = json_decode($body, true);
    
    if (isset($data['choices'][0]['message']['content'])) {
        $refined_html = vmo_refinement_strip_code_fences($data['choices'][0]['message']['content']);
        if (vmo_refinement_has_forbidden_runtime_dependency($refined_html)) {
            wp_send_json_error(array('message' => 'Refinement was rejected because it introduced WordPress runtime dependencies.'));
            return;
        }
        update_user_meta($user_id, 'vmo_last_website_refinement_contract', array(
            'agent' => 'website_refinement_assistant',
            'model' => $model,
            'section_index' => $section_index,
            'current_page' => $current_page,
            'component_state_present' => !empty($component_state),
            'updated_at' => current_time('mysql'),
        ));
        
        wp_send_json_success(array(
            'html' => $refined_html,
            'section_index' => $section_index,
            'agent_type' => 'website_refinement_assistant',
            'model' => $model,
            'component_state_preserved' => true,
            'direct_publish_allowed' => false
        ));
    } else {
        wp_send_json_error(array('message' => 'Failed to refine content'));
    }
}

// Media Integration Agent
add_action('wp_ajax_vmo_integrate_media', 'vmo_handle_media_integration');
function vmo_handle_media_integration() {
    if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['security'] ?? '')), 'vmo_builder_nonce')) {
        wp_send_json_error(array('message' => 'Security check failed'));
        return;
    }
    
    if (!is_user_logged_in()) {
        wp_send_json_error(array('message' => 'User not logged in'));
        return;
    }
    
    // ✅ TOKEN DEDUCTION v8.1.0: Charge for media integration
    $user_id = get_current_user_id();
    if (function_exists('vmo_deduct_tokens')) {
        $token_cost = function_exists('vmo_get_token_cost') ? (int) vmo_get_token_cost('media_integration') : 900;
        if (!vmo_deduct_tokens($user_id, $token_cost, 'media_integration', 'Media integration')) {
            wp_send_json_error(array('message' => 'Insufficient tokens. Please top up your balance.'));
            return;
        }
    }
    
    $prompt = sanitize_textarea_field(wp_unslash($_POST['prompt'] ?? ''));
    $media_url = esc_url_raw(wp_unslash($_POST['media_url'] ?? ''));
    $media_type = sanitize_key((string) wp_unslash($_POST['media_type'] ?? 'image'));
    $section_index = intval($_POST['section_index'] ?? 0);
    $agent_type = sanitize_text_field(wp_unslash($_POST['agent_type'] ?? 'media_director'));
    
    if (empty($prompt)) {
        wp_send_json_error(array('message' => 'Prompt is required'));
        return;
    }

    $alt_text = sanitize_text_field(wp_unslash($_POST['alt_text'] ?? $prompt));
    if ($media_url !== '') {
        if ($media_type === 'video') {
            $integrated_html = sprintf(
                '<figure class="vmo-media vmo-media-video" data-vmo-media="resolved" data-vmo-section-index="%d"><video src="%s" controls playsinline preload="metadata"></video><figcaption>%s</figcaption></figure>',
                $section_index,
                esc_url($media_url),
                esc_html($alt_text)
            );
        } else {
            $integrated_html = sprintf(
                '<figure class="vmo-media vmo-media-image" data-vmo-media="resolved" data-vmo-section-index="%d"><img src="%s" alt="%s" loading="lazy" decoding="async" /></figure>',
                $section_index,
                esc_url($media_url),
                esc_attr($alt_text)
            );
        }
    } else {
        $integrated_html = sprintf(
            '<div class="vmo-media-placeholder" data-vmo-generate="true" data-vmo-prompt="%s" data-vmo-placement="builder_media_integration" data-vmo-section-index="%d" aria-label="%s"></div>',
            esc_attr($prompt),
            $section_index,
            esc_attr($alt_text)
        );
    }

    wp_send_json_success(array(
        'html' => $integrated_html,
        'media_url' => $media_url,
        'media_type' => $media_type,
        'section_index' => $section_index,
        'agent_type' => $agent_type,
        'standalone_runtime_required' => true,
        'wordpress_editor_usage' => 'disabled_for_generated_sites'
    ));
}

/* --------------------------------------------------------------
   AI-DRIVEN DASHBOARD BACKEND FUNCTIONS
---------------------------------------------------------------- */

// SEO Data Analytics Function
function vmo_get_seo_data() {
    if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['nonce'] ?? $_REQUEST['security'] ?? '')), 'vmo_nonce')) { wp_send_json_error(['message' => 'Security check failed'], 403); }
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if ((isset($_POST['security']) || isset($_POST['nonce'])) && !vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Invalid nonce', 'status' => 'invalid_nonce']);
    }

    global $wpdb;
    $user_id = get_current_user_id();
    $consultation_profile = function_exists('vmo_get_consultation_profile') ? vmo_get_consultation_profile($user_id) : [];
    $consultation_completed = !empty($consultation_profile);

    if (!$consultation_completed) {
        wp_send_json_success([
            'seo_score' => '—',
            'indexed_pages' => '—',
            'errors' => '—',
            'warnings' => '—',
            'onpage_score' => '—',
            'technical_score' => '—',
            'content_score' => '—',
            'backlink_authority' => '—',
            'domain_authority' => '—',
            'keyword_opportunity' => '—',
            'status' => 'consultation_pending',
            'message' => 'SEO analysis available after consultation completion'
        ]);
    }

    // AI-driven SEO analysis based on consultation profile and industry research
    $industry_insights = vmo_get_cached_industry_insights($user_id);
    $website_analysis = vmo_analyze_website_seo($user_id, $consultation_profile);
    
    wp_send_json_success([
        'seo_score' => $website_analysis['overall_score'] ?? 75,
        'indexed_pages' => $website_analysis['indexed_pages'] ?? 0,
        'errors' => $website_analysis['errors'] ?? 0,
        'warnings' => $website_analysis['warnings'] ?? 0,
        'onpage_score' => $website_analysis['onpage_score'] ?? 70,
        'technical_score' => $website_analysis['technical_score'] ?? 80,
        'content_score' => $website_analysis['content_score'] ?? 65,
        'backlink_authority' => $website_analysis['backlink_authority'] ?? 45,
        'domain_authority' => $website_analysis['domain_authority'] ?? 35,
        'keyword_opportunity' => $website_analysis['keyword_opportunity'] ?? 85,
        'recommendations' => $website_analysis['recommendations'] ?? [],
        'status' => 'active'
    ]);
}

// SEO Recommendations Function
function vmo_get_seo_recommendations() {
    if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['nonce'] ?? $_REQUEST['security'] ?? '')), 'vmo_nonce')) { wp_send_json_error(['message' => 'Security check failed'], 403); }
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    
    $user_id = get_current_user_id();
    $consultation_profile = function_exists('vmo_get_consultation_profile') ? vmo_get_consultation_profile($user_id) : [];
    $industry_insights = vmo_get_cached_industry_insights($user_id);
    
    $recommendations = vmo_generate_ai_seo_recommendations($user_id, $consultation_profile, $industry_insights);
    
    wp_send_json_success([
        'keywords' => $recommendations['keywords'] ?? [],
        'blog_topics' => $recommendations['blog_topics'] ?? [],
        'technical_actions' => $recommendations['technical_actions'] ?? [],
        'content_gaps' => $recommendations['content_gaps'] ?? []
    ]);
}

// Email Marketing Metrics Function
function vmo_get_email_metrics() {
    if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['nonce'] ?? $_REQUEST['security'] ?? '')), 'vmo_nonce')) { wp_send_json_error(['message' => 'Security check failed'], 403); }
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }

    global $wpdb;
    $user_id = get_current_user_id();
    $consultation_profile = function_exists('vmo_get_consultation_profile') ? vmo_get_consultation_profile($user_id) : [];
    
    if (empty($consultation_profile)) {
        wp_send_json_success([
            'emails_sent' => '—',
            'open_rate' => '—',
            'click_rate' => '—',
            'campaigns_count' => '—',
            'status' => 'consultation_pending'
        ]);
    }

    // Get email campaign data from AI-driven campaigns
    $email_table = $wpdb->prefix . 'vmo_email_campaigns';
    $has_email_table = vmo_table_exists($email_table);
    
    if (!$has_email_table) {
        // Create email campaigns table for tracking
        vmo_create_email_campaigns_table();
    }

    $from = isset($_POST['from']) ? sanitize_text_field(wp_unslash($_POST['from'])) : date('Y-m-d', strtotime('-30 days'));
    $to = isset($_POST['to']) ? sanitize_text_field(wp_unslash($_POST['to'])) : date('Y-m-d');

    $campaign_stats = vmo_get_email_campaign_stats($user_id, $from, $to);
    
    wp_send_json_success([
        'emails_sent' => $campaign_stats['sent'] ?? 0,
        'open_rate' => $campaign_stats['open_rate'] ?? 0,
        'click_rate' => $campaign_stats['click_rate'] ?? 0,
        'campaigns_count' => $campaign_stats['campaigns'] ?? 0,
        'revenue_generated' => $campaign_stats['revenue'] ?? 0,
        'chart_data' => $campaign_stats['chart'] ?? [],
        'status' => 'active'
    ]);
}

// Email Campaigns List Function
function vmo_get_email_campaigns() {
    if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['nonce'] ?? $_REQUEST['security'] ?? '')), 'vmo_nonce')) { wp_send_json_error(['message' => 'Security check failed'], 403); }
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    
    $user_id = get_current_user_id();
    $campaigns = vmo_get_user_email_campaigns($user_id);
    
    wp_send_json_success([
        'campaigns' => $campaigns,
        'total' => count($campaigns)
    ]);
}

// ========================================
// EMAIL CREDENTIALS MANAGEMENT (User-Provided IMAP/SMTP)
// Replaces legacy auto-provisioning with user-provided email accounts
// ========================================

/**
 * Save email credentials securely (IMAP/SMTP user-provided accounts)
 * Encrypts password before storage
 */
function vmo_save_email_credentials() {
    if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['nonce'] ?? $_REQUEST['security'] ?? '')), 'vmo_nonce')) { wp_send_json_error(['message' => 'Security check failed'], 403); }
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    
    // Verify nonce
    $nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : '';
    if (!wp_verify_nonce($nonce, 'vmo_nonce')) {
        wp_send_json_error(['message' => 'Invalid security token'], 403);
    }
    
    $user_id = get_current_user_id();
    $session_id = sanitize_text_field($_POST['session_id'] ?? '');
    $category = sanitize_text_field($_POST['category'] ?? '');
    $email = sanitize_email($_POST['email'] ?? '');
    $password = wp_unslash($_POST['password'] ?? ''); // wp_unslash only — sanitize_text_field strips valid password chars
    $display_name = sanitize_text_field($_POST['display_name'] ?? '');
    $provider_preset = sanitize_text_field($_POST['provider_preset'] ?? 'custom');
    $imap_host = sanitize_text_field($_POST['imap_host'] ?? '');
    $imap_port = absint($_POST['imap_port'] ?? 993);
    $smtp_host = sanitize_text_field($_POST['smtp_host'] ?? '');
    $smtp_port = absint($_POST['smtp_port'] ?? 587);
    
    // Validate required fields
    if (empty($category) || empty($email) || empty($password)) {
        wp_send_json_error(['message' => 'Missing required fields'], 400);
    }
    
    // Validate email format
    if (!is_email($email)) {
        wp_send_json_error(['message' => 'Invalid email format'], 400);
    }
    
    // Enforce domain validation - email must match the enforced domain
    $enforced_domain = vmo_get_enforced_domain();
    $email_domain = substr(strrchr($email, "@"), 1);
    
    // Check for protected/government domains
    $protected_domains = ['gov', 'mil', 'edu', 'fbi.gov', 'cia.gov', 'state.gov', 'whitehouse.gov'];
    foreach ($protected_domains as $pd) {
        if (str_ends_with($email_domain, $pd) || $email_domain === $pd) {
            wp_send_json_error(['message' => 'Protected/government domain - cannot be used'], 400);
        }
    }
    
    if ($enforced_domain && $email_domain !== $enforced_domain && !str_ends_with($email_domain, '.' . $enforced_domain)) {
        wp_send_json_error(['message' => "Email must be @{$enforced_domain}"], 400);
    }
    
    // Encrypt password before storage
    $encrypted_password = vmo_encrypt_credential($password);
    if (!$encrypted_password) {
        wp_send_json_error(['message' => 'Failed to secure credentials'], 500);
    }
    
    global $wpdb;
    $table_name = $wpdb->prefix . 'vmo_email_accounts';
    
    // Ensure table exists
    vmo_ensure_email_accounts_table();
    
    // Check if account already exists for this category
    $existing = $wpdb->get_row($wpdb->prepare(
        "SELECT id FROM {$table_name} WHERE user_id = %d AND category = %s",
        $user_id, $category
    ));
    
    $account_data = [
        'user_id' => $user_id,
        'session_id' => $session_id,
        'category' => $category,
        'role' => $category,
        'email' => $email,
        'email_address' => $email,
        'display_name' => $display_name,
        'provider_preset' => $provider_preset,
        'provider_type' => 'manual',
        'imap_host' => $imap_host,
        'imap_port' => $imap_port,
        'smtp_host' => $smtp_host,
        'smtp_port' => $smtp_port,
        'smtp_secure' => 'tls',
        'encrypted_password' => $encrypted_password,
        'verified' => 0,
        'domain' => $email_domain,
        'status' => 'active',
        'updated_at' => current_time('mysql')
    ];
    
    if ($existing) {
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $wpdb->update($table_name, $account_data, ['id' => $existing->id]);
    } else {
        $account_data['created_at'] = current_time('mysql');
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $wpdb->insert($table_name, $account_data);
    }
    
    // ✅ SYNC v9.0.2: Also sync to email_sender_identities for dashboard integration
    vmo_sync_email_to_sender_identities($user_id, $email, $category, $display_name);
    
    wp_send_json_success([
        'message' => 'Email account saved successfully',
        'category' => $category,
        'email' => $email
    ]);
}

/**
 * Novice-friendly flow: test IMAP/SMTP reachability first, then save credentials.
 * - If IMAP extension exists, requires successful IMAP auth before saving.
 * - If IMAP extension is unavailable, saves but marks verification as pending.
 */
function vmo_test_and_save_email_credentials() {
    if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['nonce'] ?? $_REQUEST['security'] ?? '')), 'vmo_nonce')) { wp_send_json_error(['message' => 'Security check failed'], 403); }
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }

    $nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : '';
    if (!wp_verify_nonce($nonce, 'vmo_nonce')) {
        wp_send_json_error(['message' => 'Invalid security token'], 403);
    }

    $user_id = get_current_user_id();
    $session_id = sanitize_text_field($_POST['session_id'] ?? '');
    $category = sanitize_text_field($_POST['category'] ?? '');
    $email = sanitize_email($_POST['email'] ?? '');
    $password = wp_unslash($_POST['password'] ?? ''); // wp_unslash only — sanitize_text_field strips valid password chars
    $display_name = sanitize_text_field($_POST['display_name'] ?? '');
    $provider_preset = sanitize_text_field($_POST['provider_preset'] ?? 'custom');
    $imap_host = sanitize_text_field($_POST['imap_host'] ?? '');
    $imap_port = absint($_POST['imap_port'] ?? 993);
    $smtp_host = sanitize_text_field($_POST['smtp_host'] ?? '');
    $smtp_port = absint($_POST['smtp_port'] ?? 587);

    if (empty($category) || empty($email) || empty($password)) {
        wp_send_json_error(['message' => 'Missing required fields'], 400);
    }
    if (empty($imap_host) || empty($smtp_host)) {
        wp_send_json_error(['message' => 'Missing mail server settings. Please choose a provider preset or enter advanced settings.'], 400);
    }
    if (!is_email($email)) {
        wp_send_json_error(['message' => 'Invalid email format'], 400);
    }

    $enforced_domain = vmo_get_enforced_domain();
    $email_domain = substr(strrchr($email, "@"), 1);

    $protected_domains = ['gov', 'mil', 'edu', 'fbi.gov', 'cia.gov', 'state.gov', 'whitehouse.gov'];
    foreach ($protected_domains as $pd) {
        if (str_ends_with($email_domain, $pd) || $email_domain === $pd) {
            wp_send_json_error(['message' => 'Protected/government domain - cannot be used'], 400);
        }
    }

    if ($enforced_domain && $email_domain !== $enforced_domain && !str_ends_with($email_domain, '.' . $enforced_domain)) {
        wp_send_json_error(['message' => "Email must be @{$enforced_domain}"], 400);
    }

    // Basic SMTP reachability check (not authentication)
    $smtp_ok = false;
    $smtp_error = '';
    if (!empty($smtp_host) && !empty($smtp_port)) {
        $timeout = 5;
        $errno = 0;
        $errstr = '';
        $socket = @fsockopen($smtp_host, $smtp_port, $errno, $errstr, $timeout);
        if ($socket) {
            $smtp_ok = true;
            fclose($socket);
        } else {
            $smtp_error = $errstr ?: 'Unable to reach SMTP server.';
        }
    }

    // IMAP authentication check when possible
    $imap_ok = false;
    $imap_pending = false;
    $imap_error = '';
    if (function_exists('imap_open')) {
        $mailbox_base = '{' . $imap_host . ':' . $imap_port . '/imap';
        $candidates = [];
        if ($imap_port === 993) {
            $candidates[] = $mailbox_base . '/ssl}INBOX';
            $candidates[] = $mailbox_base . '/ssl/novalidate-cert}INBOX';
        } else {
            $candidates[] = $mailbox_base . '/tls}INBOX';
            $candidates[] = $mailbox_base . '/notls}INBOX';
            $candidates[] = $mailbox_base . '/novalidate-cert}INBOX';
        }

        foreach ($candidates as $mailbox) {
            $connection = @imap_open($mailbox, $email, $password, OP_READONLY, 1);
            if ($connection) {
                imap_close($connection);
                $imap_ok = true;
                break;
            }
            $imap_error = imap_last_error() ?: $imap_error;
        }

        if (!$imap_ok) {
            wp_send_json_error([
                'message' => 'Could not sign in to IMAP with those credentials. ' . ($imap_error ? ('(' . $imap_error . ')') : 'Please double-check your email/password.'),
                'needs_advanced' => true,
                'smtp_reachable' => $smtp_ok,
                'smtp_error' => $smtp_error,
            ], 400);
        }
    } else {
        $imap_pending = true;
    }

    if (!$smtp_ok) {
        wp_send_json_error([
            'message' => 'Could not reach the SMTP server at the provided address/port. ' . ($smtp_error ? ('(' . $smtp_error . ')') : ''),
            'needs_advanced' => true,
            'smtp_reachable' => false,
            'smtp_error' => $smtp_error,
        ], 400);
    }

    $encrypted_password = vmo_encrypt_credential($password);
    if (!$encrypted_password) {
        wp_send_json_error(['message' => 'Failed to secure credentials'], 500);
    }

    global $wpdb;
    $table_name = $wpdb->prefix . 'vmo_email_accounts';
    vmo_ensure_email_accounts_table();

    $existing = $wpdb->get_row($wpdb->prepare(
        "SELECT id FROM {$table_name} WHERE user_id = %d AND category = %s",
        $user_id, $category
    ));

    $account_data = [
        'user_id' => $user_id,
        'session_id' => $session_id,
        'category' => $category,
        'role' => $category,
        'email' => $email,
        'email_address' => $email,
        'display_name' => $display_name,
        'provider_preset' => $provider_preset,
        'provider_type' => 'manual',
        'imap_host' => $imap_host,
        'imap_port' => $imap_port,
        'smtp_host' => $smtp_host,
        'smtp_port' => $smtp_port,
        'smtp_secure' => 'tls',
        'encrypted_password' => $encrypted_password,
        'verified' => $imap_pending ? 0 : 1,
        'domain' => $email_domain,
        'status' => 'active',
        'updated_at' => current_time('mysql')
    ];

    if ($existing) {
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $wpdb->update($table_name, $account_data, ['id' => $existing->id]);
    } else {
        $account_data['created_at'] = current_time('mysql');
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $wpdb->insert($table_name, $account_data);
    }

    vmo_sync_email_to_sender_identities($user_id, $email, $category, $display_name);

    wp_send_json_success([
        'message' => $imap_pending ? 'Email account saved. Automatic verification is not available on this server.' : 'Email account verified and saved successfully.',
        'category' => $category,
        'email' => $email,
        'verified' => $imap_pending ? 'pending' : true,
    ]);
}

/**
 * ✅ NEW v9.0.2: Sync consultation email accounts to sender identities for dashboard
 */
function vmo_sync_email_to_sender_identities($user_id, $email, $category, $display_name = '', $verification_status = 'pending') {
    global $wpdb;
    $identities_table = $wpdb->prefix . 'vmo_email_sender_identities';
    
    // Check if identities table exists
    if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $identities_table)) !== $identities_table) {
        // Create the table if it doesn't exist
        vmo_ensure_sender_identities_table();
    }
    
    // Map consultation categories to email roles
    $role_map = [
        'general' => 'info',
        'info' => 'info',
        'support' => 'support',
        'sales' => 'sales',
        'marketing' => 'marketing',
        'transactions' => 'billing',
        'billing' => 'billing',
        'customercare' => 'support',
        'customer_care' => 'support',
        'bizdev' => 'partnerships',
        'business_development' => 'partnerships',
        'newsletter' => 'newsletter',
        'noreply' => 'noreply',
        'personal' => 'primary',
        'primary' => 'primary'
    ];
    $email_role = $role_map[$category] ?? $category;
    $email_domain = '';
    if (is_string($email) && strpos($email, '@') !== false) {
        $email_domain = substr(strrchr($email, '@'), 1);
    }
    $allowed_statuses = ['pending', 'verified', 'failed', 'manual', 'fallback'];
    $verification_status = in_array($verification_status, $allowed_statuses, true) ? $verification_status : 'pending';
    $columns = $wpdb->get_col("SHOW COLUMNS FROM {$identities_table}", 0);
    $columns = is_array($columns) ? $columns : [];
    
    // Check if email already exists for this user
    $existing = $wpdb->get_row($wpdb->prepare(
        "SELECT id FROM {$identities_table} WHERE user_id = %d AND email_address = %s",
        $user_id, $email
    ));
    
    $identity_data = [
        'user_id' => $user_id,
        'email_address' => $email,
        'email_role' => $email_role,
        'display_name' => $display_name ?: '',
        'verification_status' => $verification_status,
        'is_primary' => ($category === 'primary' || $category === 'personal') ? 1 : 0,
        'updated_at' => current_time('mysql')
    ];
    if (in_array('domain', $columns, true)) {
        $identity_data['domain'] = $email_domain;
    }
    if ($verification_status === 'verified' && in_array('verified_at', $columns, true)) {
        $identity_data['verified_at'] = current_time('mysql');
    }
    
    if ($existing) {
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $wpdb->update($identities_table, $identity_data, ['id' => $existing->id]);
    } else {
        $identity_data['created_at'] = current_time('mysql');
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $wpdb->insert($identities_table, $identity_data);
    }
}

/**
 * ✅ NEW v9.0.2: Ensure sender identities table exists
 */
function vmo_ensure_sender_identities_table() {
    global $wpdb;
    $table_name = $wpdb->prefix . 'vmo_email_sender_identities';
    $charset_collate = $wpdb->get_charset_collate();
    
    $sql = "CREATE TABLE IF NOT EXISTS {$table_name} (
        id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
        user_id BIGINT(20) UNSIGNED NOT NULL,
        domain VARCHAR(255) DEFAULT '',
        email_address VARCHAR(255) NOT NULL,
        email_role VARCHAR(50) DEFAULT 'primary',
        display_name VARCHAR(255) DEFAULT '',
        verification_status VARCHAR(50) DEFAULT 'pending',
        verified_at DATETIME DEFAULT NULL,
        is_primary TINYINT(1) DEFAULT 0,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        PRIMARY KEY (id),
        KEY user_id (user_id),
        KEY email_address (email_address),
        KEY domain (domain),
        UNIQUE KEY user_email (user_id, email_address)
    ) {$charset_collate};";
    
    require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
    dbDelta($sql);
}

/**
 * Verify email credentials by testing IMAP connection
 */
function vmo_verify_email_credentials() {
    if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['nonce'] ?? $_REQUEST['security'] ?? '')), 'vmo_nonce')) { wp_send_json_error(['message' => 'Security check failed'], 403); }
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    
    $user_id = get_current_user_id();
    $category = sanitize_text_field($_POST['category'] ?? '');
    
    global $wpdb;
    $table_name = $wpdb->prefix . 'vmo_email_accounts';

    vmo_ensure_email_accounts_table();
    
    $account = $wpdb->get_row($wpdb->prepare(
        "SELECT * FROM {$table_name} WHERE user_id = %d AND category = %s",
        $user_id, $category
    ));
    
    if (!$account) {
        wp_send_json_error(['message' => 'Account not found'], 404);
    }
    
    // Decrypt password
    $password = vmo_decrypt_credential($account->encrypted_password);
    if (!$password) {
        wp_send_json_error(['message' => 'Failed to decrypt credentials'], 500);
    }
    
    // Test IMAP connection
    if (function_exists('imap_open')) {
        $mailbox = '{' . $account->imap_host . ':' . $account->imap_port . '/imap/ssl}INBOX';
        $connection = @imap_open($mailbox, $account->email, $password, OP_READONLY, 1);
        
        if ($connection) {
            imap_close($connection);
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $wpdb->update($table_name, ['verified' => 1], ['id' => $account->id]);
            wp_send_json_success(['message' => 'Email account verified successfully', 'verified' => true]);
        } else {
            $error = imap_last_error();
            wp_send_json_error(['message' => 'Connection failed: ' . $error, 'verified' => false], 400);
        }
    } else {
        // IMAP extension not available, mark as pending verification
        wp_send_json_success(['message' => 'IMAP verification not available on server', 'verified' => 'pending']);
    }
}

/**
 * Get all email accounts for current user
 */
function vmo_get_email_accounts() {
    if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['nonce'] ?? $_REQUEST['security'] ?? '')), 'vmo_nonce')) { wp_send_json_error(['message' => 'Security check failed'], 403); }
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    
    $user_id = get_current_user_id();
    
    global $wpdb;
    $table_name = $wpdb->prefix . 'vmo_email_accounts';

    vmo_ensure_email_accounts_table();
    
    // Return empty if table doesn't exist yet
    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
    if ($wpdb->get_var("SHOW TABLES LIKE '{$table_name}'") !== $table_name) {
        wp_send_json_success(['accounts' => [], 'total' => 0]);
    }
    
    $accounts = $wpdb->get_results($wpdb->prepare(
        "SELECT id, category, email, display_name, provider_preset, imap_host, smtp_host, verified, created_at 
         FROM {$table_name} WHERE user_id = %d ORDER BY created_at ASC",
        $user_id
    ));
    
    wp_send_json_success([
        'accounts' => $accounts,
        'total' => count($accounts),
        'enforced_domain' => vmo_get_enforced_domain()
    ]);
}

/**
 * Delete an email account
 */
function vmo_delete_email_account() {
    if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['nonce'] ?? $_REQUEST['security'] ?? '')), 'vmo_nonce')) { wp_send_json_error(['message' => 'Security check failed'], 403); }
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    
    $user_id = get_current_user_id();
    $category = sanitize_text_field($_POST['category'] ?? '');
    
    global $wpdb;
    $table_name = $wpdb->prefix . 'vmo_email_accounts';

    vmo_ensure_email_accounts_table();
    
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    $deleted = $wpdb->delete($table_name, [
        'user_id' => $user_id,
        'category' => $category
    ]);
    
    if ($deleted) {
        wp_send_json_success(['message' => 'Email account deleted']);
    } else {
        wp_send_json_error(['message' => 'Failed to delete account'], 400);
    }
}

/**
 * Save bulk email provider configuration (Brevo, MailerLite)
 */
function vmo_save_bulk_email_provider() {
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    
    $user_id = get_current_user_id();
    $provider = sanitize_text_field($_POST['provider'] ?? '');
    $api_key = sanitize_text_field($_POST['api_key'] ?? '');
    
    if (empty($provider) || empty($api_key)) {
        wp_send_json_error(['message' => 'Missing provider or API key'], 400);
    }
    
    $valid_providers = ['brevo', 'mailerlite'];
    if (!in_array($provider, $valid_providers)) {
        wp_send_json_error(['message' => 'Invalid provider'], 400);
    }
    
    // Encrypt API key
    $encrypted_key = vmo_encrypt_credential($api_key);
    
    // Store in user meta
    update_user_meta($user_id, 'vmo_bulk_email_provider', $provider);
    update_user_meta($user_id, 'vmo_bulk_email_api_key', $encrypted_key);
    update_user_meta($user_id, 'vmo_bulk_email_verified', 0);
    
    wp_send_json_success(['message' => 'Bulk email provider saved', 'provider' => $provider]);
}

/**
 * Verify bulk email provider API key
 */
function vmo_verify_bulk_email_provider() {
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    
    $user_id = get_current_user_id();
    $provider = get_user_meta($user_id, 'vmo_bulk_email_provider', true);
    $encrypted_key = get_user_meta($user_id, 'vmo_bulk_email_api_key', true);
    
    if (empty($provider) || empty($encrypted_key)) {
        wp_send_json_error(['message' => 'No bulk email provider configured'], 400);
    }
    
    $api_key = vmo_decrypt_credential($encrypted_key);
    
    $verified = false;
    $error_message = '';
    
    switch ($provider) {
        case 'brevo':
            // Test Brevo API
            $response = wp_remote_get('https://api.brevo.com/v3/account', [
                'timeout' => 10,
                'headers' => ['api-key' => $api_key]
            ]);
            if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
                $verified = true;
            } else {
                $error_message = is_wp_error($response) ? $response->get_error_message() : 'Invalid API key';
            }
            break;
            
        case 'mailerlite':
            // Test MailerLite API
            $response = wp_remote_get('https://connect.mailerlite.com/api/subscribers?limit=1', [
                'timeout' => 10,
                'headers' => ['Authorization' => 'Bearer ' . $api_key]
            ]);
            if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
                $verified = true;
            } else {
                $error_message = is_wp_error($response) ? $response->get_error_message() : 'Invalid API key';
            }
            break;
    }
    
    if ($verified) {
        update_user_meta($user_id, 'vmo_bulk_email_verified', 1);
        wp_send_json_success(['message' => 'API key verified', 'verified' => true, 'provider' => $provider]);
    } else {
        wp_send_json_error(['message' => $error_message, 'verified' => false], 400);
    }
}

/**
 * Get enforced domain for this WordPress installation
 */
function vmo_get_enforced_domain() {
    $site_url = get_site_url();
    $host = wp_parse_url($site_url, PHP_URL_HOST);
    $host = strtolower(preg_replace('/^www\./', '', $host ?? ''));
    
    // Detect staging/dev environments
    $staging_patterns = [
        'kinsta.cloud', 'wpengine.com', 'flywheel.com', 'pantheonsite.io',
        'azurewebsites.net', 'herokuapp.com', 'vercel.app', 'netlify.app',
        'ngrok.io', 'localhost', '127.0.0.1', 'test', 'staging', 'dev'
    ];
    
    foreach ($staging_patterns as $pattern) {
        if (stripos($host, $pattern) !== false) {
            return null; // Staging environment, domain enforcement relaxed
        }
    }
    
    return $host;
}

/**
 * Encrypt credential for secure storage
 */
function vmo_encrypt_credential($plaintext) {
    if (empty($plaintext)) return '';
    
    $key = wp_salt('auth');
    $method = 'aes-256-cbc';
    $iv_length = openssl_cipher_iv_length($method);
    $iv = openssl_random_pseudo_bytes($iv_length);
    
    $encrypted = openssl_encrypt($plaintext, $method, $key, 0, $iv);
    if ($encrypted === false) return '';
    
    return base64_encode($iv . $encrypted); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- encryption/decryption
}

/**
 * Decrypt credential from secure storage
 */
function vmo_decrypt_credential($encrypted_string) {
    if (empty($encrypted_string)) return '';
    
    $key = wp_salt('auth');
    $method = 'aes-256-cbc';
    $iv_length = openssl_cipher_iv_length($method);
    
    $decoded = base64_decode($encrypted_string); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- encryption/decryption
    if ($decoded === false) return '';
    
    $iv = substr($decoded, 0, $iv_length);
    $encrypted = substr($decoded, $iv_length);
    
    $decrypted = openssl_decrypt($encrypted, $method, $key, 0, $iv);
    return $decrypted !== false ? $decrypted : '';
}

/**
 * Ensure email accounts table exists
 */
function vmo_ensure_email_accounts_table() {
    global $wpdb;
    $table_name = $wpdb->prefix . 'vmo_email_accounts';
    $charset_collate = $wpdb->get_charset_collate();

    $sql = "CREATE TABLE {$table_name} (
        id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
        user_id bigint(20) UNSIGNED NOT NULL,
        session_id varchar(100) DEFAULT '',
        category varchar(50) DEFAULT 'general',
        role varchar(50) DEFAULT 'general',
        email varchar(255) NOT NULL,
        email_address varchar(255) NOT NULL,
        display_name varchar(255) DEFAULT '',
        provider_preset varchar(50) DEFAULT 'custom',
        provider_type varchar(50) DEFAULT 'manual',
        imap_host varchar(255) DEFAULT '',
        imap_port int(5) DEFAULT 993,
        smtp_host varchar(255) DEFAULT '',
        smtp_port int(5) DEFAULT 587,
        smtp_secure varchar(20) DEFAULT 'tls',
        encrypted_password text NULL,
        verified tinyint(1) DEFAULT 0,
        domain varchar(255) DEFAULT '',
        provisioned_at datetime DEFAULT NULL,
        provisioned_via varchar(50) DEFAULT '',
        external_id varchar(255) DEFAULT NULL,
        status varchar(20) DEFAULT 'active',
        last_sync_uid bigint(20) UNSIGNED DEFAULT 0,
        last_synced_at datetime DEFAULT NULL,
        last_sync_error text DEFAULT NULL,
        created_at datetime DEFAULT CURRENT_TIMESTAMP,
        updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        PRIMARY KEY (id),
        KEY user_id (user_id),
        KEY category (category),
        KEY role (role),
        KEY email (email),
        KEY email_address (email_address),
        KEY domain (domain),
        KEY status (status),
        UNIQUE KEY user_email (user_id, email)
    ) {$charset_collate};";

    require_once ABSPATH . 'wp-admin/includes/upgrade.php';
    dbDelta($sql);

    if ($wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table_name)) !== $table_name) {
        return;
    }

    $columns = $wpdb->get_col("SHOW COLUMNS FROM {$table_name}", 0);
    $columns = is_array($columns) ? array_map('strval', $columns) : array();
    $missing = function($column) use ($columns) {
        return !in_array($column, $columns, true);
    };

    $alterations = array(
        'user_id' => "ADD COLUMN user_id bigint(20) UNSIGNED NOT NULL DEFAULT 0 AFTER id",
        'session_id' => "ADD COLUMN session_id varchar(100) DEFAULT '' AFTER user_id",
        'category' => "ADD COLUMN category varchar(50) DEFAULT 'general' AFTER session_id",
        'role' => "ADD COLUMN role varchar(50) DEFAULT 'general' AFTER category",
        'email' => "ADD COLUMN email varchar(255) NOT NULL DEFAULT '' AFTER role",
        'email_address' => "ADD COLUMN email_address varchar(255) NOT NULL DEFAULT '' AFTER email",
        'display_name' => "ADD COLUMN display_name varchar(255) DEFAULT '' AFTER email_address",
        'provider_preset' => "ADD COLUMN provider_preset varchar(50) DEFAULT 'custom' AFTER display_name",
        'provider_type' => "ADD COLUMN provider_type varchar(50) DEFAULT 'manual' AFTER provider_preset",
        'imap_host' => "ADD COLUMN imap_host varchar(255) DEFAULT '' AFTER provider_type",
        'imap_port' => "ADD COLUMN imap_port int(5) DEFAULT 993 AFTER imap_host",
        'smtp_host' => "ADD COLUMN smtp_host varchar(255) DEFAULT '' AFTER imap_port",
        'smtp_port' => "ADD COLUMN smtp_port int(5) DEFAULT 587 AFTER smtp_host",
        'smtp_secure' => "ADD COLUMN smtp_secure varchar(20) DEFAULT 'tls' AFTER smtp_port",
        'encrypted_password' => "ADD COLUMN encrypted_password text NULL AFTER smtp_secure",
        'verified' => "ADD COLUMN verified tinyint(1) DEFAULT 0 AFTER encrypted_password",
        'domain' => "ADD COLUMN domain varchar(255) DEFAULT '' AFTER verified",
        'provisioned_at' => "ADD COLUMN provisioned_at datetime DEFAULT NULL AFTER domain",
        'provisioned_via' => "ADD COLUMN provisioned_via varchar(50) DEFAULT '' AFTER provisioned_at",
        'external_id' => "ADD COLUMN external_id varchar(255) DEFAULT NULL AFTER provisioned_via",
        'status' => "ADD COLUMN status varchar(20) DEFAULT 'active' AFTER external_id",
        'last_sync_uid' => "ADD COLUMN last_sync_uid bigint(20) UNSIGNED DEFAULT 0 AFTER status",
        'last_synced_at' => "ADD COLUMN last_synced_at datetime DEFAULT NULL AFTER last_sync_uid",
        'last_sync_error' => "ADD COLUMN last_sync_error text DEFAULT NULL AFTER last_synced_at",
        'created_at' => "ADD COLUMN created_at datetime DEFAULT CURRENT_TIMESTAMP AFTER last_sync_error",
        'updated_at' => "ADD COLUMN updated_at datetime DEFAULT CURRENT_TIMESTAMP AFTER created_at",
    );

    foreach ($alterations as $column => $alter_sql) {
        if ($missing($column)) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $wpdb->query("ALTER TABLE {$table_name} {$alter_sql}");
        }
    }

    // Normalize legacy rows so both old and new code can read the same account.
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    $wpdb->query("UPDATE {$table_name} SET email = email_address WHERE (email IS NULL OR email = '') AND email_address IS NOT NULL AND email_address <> ''");
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    $wpdb->query("UPDATE {$table_name} SET email_address = email WHERE (email_address IS NULL OR email_address = '') AND email IS NOT NULL AND email <> ''");
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    $wpdb->query("UPDATE {$table_name} SET role = category WHERE (role IS NULL OR role = '') AND category IS NOT NULL AND category <> ''");
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    $wpdb->query("UPDATE {$table_name} SET category = role WHERE (category IS NULL OR category = '') AND role IS NOT NULL AND role <> ''");
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    $wpdb->query("UPDATE {$table_name} SET domain = SUBSTRING_INDEX(COALESCE(NULLIF(email, ''), NULLIF(email_address, '')), '@', -1) WHERE (domain IS NULL OR domain = '') AND (email LIKE '%@%' OR email_address LIKE '%@%')");
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    $wpdb->query("UPDATE {$table_name} SET provider_preset = provisioned_via WHERE (provider_preset IS NULL OR provider_preset = '' OR provider_preset = 'custom') AND provisioned_via IS NOT NULL AND provisioned_via <> ''");
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    $wpdb->query("UPDATE {$table_name} SET provider_type = CASE WHEN provisioned_via IN ('mailcow','zoho','cpanel','plesk','directadmin') THEN 'provisioned' ELSE provider_type END WHERE provisioned_via IS NOT NULL AND provisioned_via <> ''");
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    $wpdb->query("UPDATE {$table_name} SET status = 'active' WHERE status IS NULL OR status = ''");
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    $wpdb->query("UPDATE {$table_name} SET created_at = provisioned_at WHERE (created_at IS NULL OR created_at = '0000-00-00 00:00:00') AND provisioned_at IS NOT NULL");
}

function vmo_upsert_email_account($user_id, array $data) {
    global $wpdb;

    $user_id = intval($user_id);
    if ($user_id <= 0) {
        return new WP_Error('invalid_user', 'Invalid user for email account.');
    }

    vmo_ensure_email_accounts_table();
    $table_name = $wpdb->prefix . 'vmo_email_accounts';
    if ($wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table_name)) !== $table_name) {
        return new WP_Error('email_accounts_table_missing', 'VMO Email accounts table is not available.');
    }

    $email = sanitize_email((string) ($data['email'] ?? $data['email_address'] ?? ''));
    if (!$email || !is_email($email)) {
        return new WP_Error('invalid_email', 'A valid email address is required.');
    }

    $category = sanitize_key((string) ($data['category'] ?? $data['role'] ?? 'general'));
    $role = sanitize_key((string) ($data['role'] ?? $category ?: 'general'));
    $domain = sanitize_text_field((string) ($data['domain'] ?? ''));
    if ($domain === '' && strpos($email, '@') !== false) {
        $domain = strtolower((string) substr(strrchr($email, '@'), 1));
    }

    $now = current_time('mysql');
    $payload = array(
        'user_id' => $user_id,
        'session_id' => sanitize_text_field((string) ($data['session_id'] ?? '')),
        'category' => $category ?: 'general',
        'role' => $role ?: ($category ?: 'general'),
        'email' => $email,
        'email_address' => $email,
        'display_name' => sanitize_text_field((string) ($data['display_name'] ?? '')),
        'provider_preset' => sanitize_key((string) ($data['provider_preset'] ?? $data['provisioned_via'] ?? 'custom')),
        'provider_type' => sanitize_key((string) ($data['provider_type'] ?? (!empty($data['provisioned_via']) ? 'provisioned' : 'manual'))),
        'imap_host' => sanitize_text_field((string) ($data['imap_host'] ?? '')),
        'imap_port' => intval($data['imap_port'] ?? 993),
        'smtp_host' => sanitize_text_field((string) ($data['smtp_host'] ?? '')),
        'smtp_port' => intval($data['smtp_port'] ?? 587),
        'smtp_secure' => sanitize_key((string) ($data['smtp_secure'] ?? 'tls')),
        'encrypted_password' => (string) ($data['encrypted_password'] ?? ''),
        'verified' => !empty($data['verified']) ? 1 : 0,
        'domain' => $domain,
        'provisioned_at' => !empty($data['provisioned_at']) ? sanitize_text_field((string) $data['provisioned_at']) : (!empty($data['provisioned_via']) ? $now : null),
        'provisioned_via' => sanitize_key((string) ($data['provisioned_via'] ?? '')),
        'external_id' => isset($data['external_id']) && $data['external_id'] !== '' ? sanitize_text_field((string) $data['external_id']) : null,
        'status' => sanitize_key((string) ($data['status'] ?? 'active')),
        'last_sync_uid' => intval($data['last_sync_uid'] ?? 0),
        'last_synced_at' => !empty($data['last_synced_at']) ? sanitize_text_field((string) $data['last_synced_at']) : null,
        'last_sync_error' => isset($data['last_sync_error']) && $data['last_sync_error'] !== '' ? sanitize_textarea_field((string) $data['last_sync_error']) : null,
        'updated_at' => $now,
    );

    $columns = $wpdb->get_col("SHOW COLUMNS FROM {$table_name}", 0);
    $columns = is_array($columns) ? array_map('strval', $columns) : array();
    $payload = array_intersect_key($payload, array_flip($columns));

    $existing_id = $wpdb->get_var($wpdb->prepare(
        "SELECT id FROM {$table_name} WHERE user_id = %d AND (email = %s OR email_address = %s) LIMIT 1",
        $user_id,
        $email,
        $email
    ));
    if (!$existing_id && !empty($payload['external_id'])) {
        $existing_id = $wpdb->get_var($wpdb->prepare(
            "SELECT id FROM {$table_name} WHERE user_id = %d AND external_id = %s LIMIT 1",
            $user_id,
            $payload['external_id']
        ));
    }

    if ($existing_id) {
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $updated = $wpdb->update($table_name, $payload, array('id' => intval($existing_id)));
        if ($updated === false) {
            return new WP_Error('email_account_update_failed', $wpdb->last_error ?: 'Could not update email account.');
        }
        $account_id = intval($existing_id);
    } else {
        $payload['created_at'] = $now;
        $payload = array_intersect_key($payload, array_flip($columns));
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $inserted = $wpdb->insert($table_name, $payload);
        if (!$inserted) {
            return new WP_Error('email_account_insert_failed', $wpdb->last_error ?: 'Could not save email account.');
        }
        $account_id = intval($wpdb->insert_id);
    }

    if (function_exists('vmo_sync_email_to_sender_identities')) {
        $verification_status = !empty($payload['verified']) ? 'verified' : 'pending';
        vmo_sync_email_to_sender_identities($user_id, $email, $payload['category'] ?? 'general', $payload['display_name'] ?? '', $verification_status);
    }

    return $account_id;
}

// Payment/Revenue Data Function
function vmo_get_payment_data() {
    if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['nonce'] ?? $_REQUEST['security'] ?? '')), 'vmo_nonce')) { wp_send_json_error(['message' => 'Security check failed'], 403); }
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }

    global $wpdb;
    $user_id = get_current_user_id();
    $consultation_profile = function_exists('vmo_get_consultation_profile') ? vmo_get_consultation_profile($user_id) : [];
    
    if (empty($consultation_profile)) {
        wp_send_json_success([
            'day_revenue' => '—',
            'week_revenue' => '—',
            'month_revenue' => '—',
            'year_revenue' => '—',
            'mom_change' => '—',
            'status' => 'consultation_pending'
        ]);
    }

    // Get revenue data from AI-managed store/services
    $revenue_data = vmo_calculate_revenue_metrics($user_id);
    
    wp_send_json_success([
        'day_revenue' => vmo_format_currency($revenue_data['today'] ?? 0),
        'week_revenue' => vmo_format_currency($revenue_data['week'] ?? 0),
        'month_revenue' => vmo_format_currency($revenue_data['month'] ?? 0),
        'year_revenue' => vmo_format_currency($revenue_data['year'] ?? 0),
        'mom_change' => ($revenue_data['mom_change'] ?? 0) . '%',
        'chart_data' => $revenue_data['chart'] ?? [],
        'transactions' => $revenue_data['transactions'] ?? [],
        'status' => 'active'
    ]);
}

// Revenue Analytics Function
function vmo_get_revenue_analytics() {
    if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['nonce'] ?? $_REQUEST['security'] ?? '')), 'vmo_nonce')) { wp_send_json_error(['message' => 'Security check failed'], 403); }
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    
    $user_id = get_current_user_id();
    $period = sanitize_text_field($_POST['period'] ?? 'month');
    
    $analytics = vmo_generate_revenue_analytics($user_id, $period);
    
    wp_send_json_success([
        'analytics' => $analytics,
        'period' => $period
    ]);
}

// CRM Data Function
function vmo_get_crm_data() {
    if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['nonce'] ?? $_REQUEST['security'] ?? '')), 'vmo_nonce')) { wp_send_json_error(['message' => 'Security check failed'], 403); }
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }

    global $wpdb;
    $user_id = get_current_user_id();
    $consultation_profile = function_exists('vmo_get_consultation_profile') ? vmo_get_consultation_profile($user_id) : [];
    
    if (empty($consultation_profile)) {
        wp_send_json_success([
            'total_contacts' => '—',
            'leads_count' => '—',
            'conversion_rate' => '—',
            'status' => 'consultation_pending'
        ]);
    }

    // Get CRM data from AI-managed lead system
    $crm_data = vmo_get_crm_analytics($user_id);
    
    wp_send_json_success([
        'total_contacts' => $crm_data['contacts'] ?? 0,
        'leads_count' => $crm_data['leads'] ?? 0,
        'conversion_rate' => ($crm_data['conversion_rate'] ?? 0) . '%',
        'pipeline_value' => vmo_format_currency($crm_data['pipeline_value'] ?? 0),
        'recent_activities' => $crm_data['activities'] ?? [],
        'lead_sources' => $crm_data['sources'] ?? [],
        'status' => 'active'
    ]);
}

// Leads Data Function
function vmo_get_leads_data() {
    if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['nonce'] ?? $_REQUEST['security'] ?? '')), 'vmo_nonce')) { wp_send_json_error(['message' => 'Security check failed'], 403); }
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    
    $user_id = get_current_user_id();
    $search = sanitize_text_field($_POST['search'] ?? '');
    $offset = (int) wp_unslash($_POST['offset'] ?? 0);
    $limit = (int) wp_unslash($_POST['limit'] ?? 20);
    
    $leads = vmo_get_user_leads($user_id, $search, $offset, $limit);
    
    wp_send_json_success([
        'leads' => $leads['data'] ?? [],
        'total' => $leads['total'] ?? 0,
        'has_more' => ($offset + $limit) < ($leads['total'] ?? 0)
    ]);
}

// AI Recommendations Function
function vmo_get_ai_recommendations() {
    if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['nonce'] ?? $_REQUEST['security'] ?? '')), 'vmo_nonce')) { wp_send_json_error(['message' => 'Security check failed'], 403); }
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }

    $user_id = get_current_user_id();
    $consultation_profile = function_exists('vmo_get_consultation_profile') ? vmo_get_consultation_profile($user_id) : [];
    $industry_insights = vmo_get_cached_industry_insights($user_id);
    
    // Generate AI-driven recommendations based on weekly research and roadmap
    $recommendations = vmo_generate_ai_recommendations($user_id, $consultation_profile, $industry_insights);
    
    wp_send_json_success([
        'recommendations' => $recommendations['actions'] ?? [],
        'priority_score' => $recommendations['priority'] ?? 0,
        'estimated_impact' => $recommendations['impact'] ?? 'medium',
        'implementation_time' => $recommendations['time_estimate'] ?? '1-2 weeks',
        'requires_approval' => true
    ]);
}

// AI Action Approval Function
function vmo_approve_ai_action() {
    if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['nonce'] ?? $_REQUEST['security'] ?? '')), 'vmo_nonce')) { wp_send_json_error(['message' => 'Security check failed'], 403); }
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }

    $action_id = sanitize_text_field($_POST['action_id'] ?? '');
    $approval_status = sanitize_text_field($_POST['status'] ?? ''); // 'approved', 'rejected', 'modified'
    
    if (empty($action_id)) {
        wp_send_json_error(['message' => 'Action ID required']);
    }

    $user_id = get_current_user_id();
    $result = vmo_process_ai_action_approval($user_id, $action_id, $approval_status);
    
    if ($result['success']) {
        wp_send_json_success([
            'message' => 'Action ' . $approval_status,
            'next_steps' => $result['next_steps'] ?? [],
            'tracking_id' => $result['tracking_id'] ?? null
        ]);
    } else {
        wp_send_json_error(['message' => $result['error'] ?? 'Failed to process approval']);
    }
}

// Industry Insights Function
function vmo_get_industry_insights() {
    if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_REQUEST['nonce'] ?? $_REQUEST['security'] ?? '')), 'vmo_nonce')) { wp_send_json_error(['message' => 'Security check failed'], 403); }
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    
    $user_id = get_current_user_id();
    $insights = vmo_get_cached_industry_insights($user_id);
    
    wp_send_json_success([
        'insights' => $insights['data'] ?? [],
        'last_scan' => $insights['last_scan'] ?? null,
        'next_scan' => $insights['next_scan'] ?? null,
        'industry' => $insights['industry'] ?? 'General',
        'competitors_monitored' => $insights['competitors_count'] ?? 0
    ]);
}

// Detailed Analytics Function
function vmo_get_detailed_analytics() {
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }

    $user_id = get_current_user_id();
    $type = sanitize_text_field($_POST['type'] ?? 'referrers'); // referrers, activities, pages
    $period = sanitize_text_field($_POST['period'] ?? 'week');
    
    $analytics = vmo_get_detailed_user_analytics($user_id, $type, $period);
    
    wp_send_json_success([
        'type' => $type,
        'data' => $analytics['data'] ?? [],
        'total' => $analytics['total'] ?? 0,
        'period' => $period
    ]);
}

// Geographical Data Function  
function vmo_get_geographical_data() {
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    
    $user_id = get_current_user_id();
    $type = sanitize_text_field($_POST['type'] ?? 'countries'); // countries, states
    
    $geo_data = vmo_get_visitor_geography($user_id, $type);
    
    wp_send_json_success([
        'type' => $type,
        'data' => $geo_data['data'] ?? [],
        'top_locations' => $geo_data['top'] ?? []
    ]);
}

/* --------------------------------------------------------------
   AI-DRIVEN DATA ANALYSIS HELPER FUNCTIONS
---------------------------------------------------------------- */

// Get cached industry insights from weekly research scans
function vmo_get_cached_industry_insights($user_id) {
    $cache_key = 'vmo_industry_insights_' . $user_id;
    $cached = get_transient($cache_key);
    
    if ($cached !== false) {
        return $cached;
    }
    
    // Generate new insights based on consultation profile
    $consultation_profile = function_exists('vmo_get_consultation_profile') ? vmo_get_consultation_profile($user_id) : [];
    
    if (empty($consultation_profile)) {
        return ['data' => [], 'industry' => 'General'];
    }
    
    // AI-driven industry research and competitive analysis
    $insights = [
        'data' => [
            'market_trends' => vmo_analyze_market_trends($consultation_profile['industry'] ?? 'General'),
            'competitor_moves' => vmo_track_competitor_activities($user_id),
            'opportunity_gaps' => vmo_identify_opportunities($consultation_profile),
            'risk_factors' => vmo_assess_market_risks($consultation_profile['industry'] ?? 'General')
        ],
        'industry' => $consultation_profile['industry'] ?? 'General',
        'last_scan' => current_time('mysql'),
        'next_scan' => date('Y-m-d H:i:s', strtotime('+7 days')),
        'competitors_count' => vmo_count_monitored_competitors($user_id)
    ];
    
    // Cache for 24 hours
    set_transient($cache_key, $insights, DAY_IN_SECONDS);
    
    return $insights;
}

// Analyze website SEO based on actual site data and consultation profile
function vmo_analyze_website_seo($user_id, $consultation_profile) {
    global $wpdb;
    
    $analysis = [
        'overall_score' => 0,
        'indexed_pages' => 0,
        'errors' => 0,
        'warnings' => 0,
        'onpage_score' => 0,
        'technical_score' => 0,
        'content_score' => 0,
        'local_score' => 0,
        'backlink_authority' => 0,
        'domain_authority' => 0,
        'keyword_opportunity' => 0,
        'recommendations' => [],
        'metric_sources' => [],
        'metric_labels' => []
    ];
    
    $industry = $consultation_profile['industry'] ?? 'General';
    $target_keywords = function_exists('vmo_seo_target_keywords') ? vmo_seo_target_keywords($user_id, (array) $consultation_profile) : ($consultation_profile['target_keywords'] ?? []);
    
    // Count real published content
    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
    $pages_count = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_status='publish' AND post_type IN ('post','page')");
    $analysis['indexed_pages'] = max(1, $pages_count);
    $analysis['metric_labels']['indexed_pages'] = 'WordPress content proxy';

    // ── On-page score: check for common SEO elements in published posts ──
    $onpage = 30; // base
    // Check if posts have meta descriptions (via popular SEO plugins or post_meta)
    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
    $has_meta = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->postmeta} pm INNER JOIN {$wpdb->posts} p ON pm.post_id = p.ID WHERE p.post_status='publish' AND pm.meta_key IN ('vmo_seo_description','_vmo_seo_description','_yoast_wpseo_metadesc','_aioseo_description','_rank_math_description') AND pm.meta_value != '' LIMIT 1");
    if ($has_meta) $onpage += 15;
    // Check if pages have featured images
    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
    $has_thumbs = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->postmeta} pm INNER JOIN {$wpdb->posts} p ON pm.post_id = p.ID WHERE p.post_status='publish' AND pm.meta_key = '_thumbnail_id' LIMIT 1");
    if ($has_thumbs) $onpage += 10;
    // Bonus for having enough content
    if ($pages_count >= 5) $onpage += 10;
    if ($pages_count >= 15) $onpage += 10;
    // Bonus for keywords configured
    if (count($target_keywords) >= 3) $onpage += 10;
    $analysis['onpage_score'] = min(95, $onpage);

    // ── Technical score: check site fundamentals ──
    $tech = 40; // base (WordPress provides decent technical foundation)
    // Check SSL
    if (is_ssl()) $tech += 15;
    // Check for XML sitemap (common paths)
    // Loopback to own site; cert validation skipped because some staging hosts use self-signed loopback.
    $sitemap_check = wp_remote_head(get_site_url() . '/sitemap.xml', ['timeout' => 3, 'sslverify' => false]);
    $analysis['sitemap_found'] = (!is_wp_error($sitemap_check) && wp_remote_retrieve_response_code($sitemap_check) === 200);
    if (!empty($analysis['sitemap_found'])) $tech += 10;
    // Check robots.txt
    // Loopback to own site (see note above).
    $robots_check = wp_remote_head(get_site_url() . '/robots.txt', ['timeout' => 3, 'sslverify' => false]);
    $analysis['robots_found'] = (!is_wp_error($robots_check) && wp_remote_retrieve_response_code($robots_check) === 200);
    if (!empty($analysis['robots_found'])) $tech += 10;
    // WordPress + good theme = decent mobile responsiveness
    $tech += 10;
    $analysis['technical_score'] = min(95, $tech);

    // ── Content score ──
    $content = 25;
    // Average word count of published posts
    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
    $avg_words = (int) $wpdb->get_var("SELECT AVG(LENGTH(post_content) - LENGTH(REPLACE(post_content, ' ', '')) + 1) FROM {$wpdb->posts} WHERE post_status='publish' AND post_type IN ('post','page') AND post_content != ''");
    if ($avg_words >= 300) $content += 15;
    if ($avg_words >= 800) $content += 15;
    if ($pages_count >= 10) $content += 10;
    if (count($target_keywords) > 0) $content += 10;
    $analysis['content_score'] = min(95, $content);

    // ── Local score ──
    $local = 35;
    if (!empty($consultation_profile['location'])) $local += 20;
    if (!empty($consultation_profile['business_name'])) $local += 10;
    if (!empty($consultation_profile['phone']) || !empty($consultation_profile['business_phone'])) $local += 10;
    if (!empty($consultation_profile['address'])) $local += 10;
    $has_local_schema = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->postmeta} pm INNER JOIN {$wpdb->posts} p ON pm.post_id = p.ID WHERE p.post_status='publish' AND pm.meta_key IN ('vmo_seo_json_ld','_vmo_seo_json_ld') AND pm.meta_value LIKE '%LocalBusiness%' LIMIT 1");
    if ($has_local_schema) $local += 10;
    $analysis['local_score'] = min(95, $local);

    // ── Backlink / domain authority ──
    $has_ahrefs = !empty(get_user_meta($user_id, 'vmo_ahrefs_api_key', true));
    $has_semrush = !empty(get_user_meta($user_id, 'vmo_semrush_api_key', true));
    $provider_metrics = get_user_meta($user_id, 'vmo_seo_provider_metrics', true);
    $provider_metrics = is_array($provider_metrics) ? $provider_metrics : [];
    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
    $oldest_post = $wpdb->get_var("SELECT MIN(post_date) FROM {$wpdb->posts} WHERE post_status='publish'");
    $site_age_months = $oldest_post ? max(1, (time() - strtotime($oldest_post)) / (30 * 86400)) : 0;
    $backlink_proxy = min(40, (int) ($site_age_months * 0.5) + ($pages_count * 0.5));
    $domain_proxy = min(45, (int) ($site_age_months * 0.5) + ($pages_count * 0.3) + 5);
    if (isset($provider_metrics['ahrefs']['domain_authority']) && is_numeric($provider_metrics['ahrefs']['domain_authority'])) {
        $analysis['domain_authority'] = (float) $provider_metrics['ahrefs']['domain_authority'];
        $analysis['metric_sources']['domain_authority'] = 'Ahrefs API';
        $analysis['metric_labels']['domain_authority'] = 'Provider-enriched';
    } else {
        $analysis['domain_authority'] = $domain_proxy;
        $analysis['metric_sources']['domain_authority'] = ($has_ahrefs || $has_semrush) ? 'Proxy estimate until enrichment syncs' : 'Proxy estimate from site age and content';
        $analysis['metric_labels']['domain_authority'] = 'Estimated from site content';
    }
    if (isset($provider_metrics['ahrefs']['backlinks']) && is_numeric($provider_metrics['ahrefs']['backlinks'])) {
        $analysis['backlink_count'] = (int) $provider_metrics['ahrefs']['backlinks'];
        $analysis['backlink_authority'] = min(90, (int) round(log10(max(1, $analysis['backlink_count'])) * 22));
        $analysis['metric_sources']['backlinks'] = 'Ahrefs API';
        $analysis['metric_labels']['backlink_authority'] = 'Provider-enriched';
    } else {
        $analysis['backlink_authority'] = $backlink_proxy;
        $analysis['metric_sources']['backlinks'] = ($has_ahrefs || $has_semrush) ? 'Proxy estimate until enrichment syncs' : 'Proxy estimate from site age and content';
        $analysis['metric_labels']['backlink_authority'] = 'Estimated from site content';
    }
    if (isset($provider_metrics['semrush']['organic_keywords']) && is_numeric($provider_metrics['semrush']['organic_keywords'])) {
        $analysis['organic_keywords'] = (int) $provider_metrics['semrush']['organic_keywords'];
        $analysis['metric_sources']['keyword_visibility'] = 'SEMrush API';
        $analysis['metric_labels']['keyword_visibility'] = 'Provider-enriched';
    } else {
        $analysis['metric_sources']['keyword_visibility'] = 'VMO keyword targets and rank history proxy';
        $analysis['metric_labels']['keyword_visibility'] = $has_semrush ? 'Estimated until SEMrush syncs' : 'External ranking data not connected';
    }
    $analysis['metric_sources']['index_coverage'] = 'WordPress published content proxy';
    $analysis['metric_labels']['keyword_opportunity'] = 'Estimated from consultation and site content';

    // ── Keyword opportunity ──
    $analysis['keyword_opportunity'] = min(95, 50 + (count($target_keywords) * 5) + ($pages_count * 1));

    // ── Overall score (weighted average) ──
    $analysis['overall_score'] = (int) round(
        ($analysis['onpage_score'] * 0.28) +
        ($analysis['technical_score'] * 0.22) +
        ($analysis['content_score'] * 0.22) +
        ($analysis['backlink_authority'] * 0.1) +
        ($analysis['keyword_opportunity'] * 0.1) +
        ($analysis['local_score'] * 0.08)
    );

    // ── Errors & warnings ──
    $analysis['errors'] = 0;
    $analysis['warnings'] = 0;
    if (!is_ssl()) $analysis['errors']++;
    if ($pages_count < 5) $analysis['warnings']++;
    if (!$has_meta) $analysis['warnings']++;
    if ($avg_words < 300) $analysis['warnings']++;
    if (count($target_keywords) === 0) $analysis['warnings']++;
    if (empty($analysis['sitemap_found'])) $analysis['warnings']++;

    // Generate recommendations
    $analysis['recommendations'] = vmo_generate_ai_seo_recommendations($user_id, $consultation_profile, []);
    
    return $analysis;
}

// Generate AI-driven SEO recommendations
function vmo_generate_ai_seo_recommendations($user_id, $consultation_profile, $industry_insights) {
    $recommendations = [
        'keywords' => [],
        'blog_topics' => [],
        'technical_actions' => [],
        'content_gaps' => [],
        'service_pages' => [],
        'location_pages' => [],
        'support_pages' => []
    ];
    
    if (empty($consultation_profile)) {
        return $recommendations;
    }
    
    $industry = $consultation_profile['industry'] ?? 'General';
    $target_audience = $consultation_profile['target_audience'] ?? 'General';
    $business_name = $consultation_profile['business_name'] ?? '';
    $business_goals = $consultation_profile['business_goals'] ?? [];
    $location = $consultation_profile['location'] ?? '';

    // Check cache first (AI results cached for 24 hours)
    $cache_key = 'vmo_seo_rec_' . md5($industry . $target_audience . $business_name);
    $cached = get_transient($cache_key);
    if ($cached && is_array($cached)) {
        return $cached;
    }

    // Try AI-powered recommendations
    $api_key = get_option('vmo_openai_api_key', '');
    if (!empty($api_key)) {
        $prompt = "You are an SEO expert. Generate actionable SEO recommendations for this business:
- Business: {$business_name}
- Industry: {$industry}
- Target Audience: {$target_audience}
- Location: {$location}
- Goals: " . implode(', ', (array) $business_goals) . "

Return valid JSON only (no markdown, no code blocks) with this exact structure:
{
  \"keywords\": [{\"keyword\": \"term\", \"difficulty\": 10-90, \"volume\": 100-50000, \"opportunity\": 10-100}],
  \"blog_topics\": [\"Topic 1\", \"Topic 2\", \"Topic 3\", \"Topic 4\", \"Topic 5\"],
  \"technical_actions\": [\"Action 1\", \"Action 2\", \"Action 3\", \"Action 4\", \"Action 5\"],
    \"content_gaps\": [\"Gap 1\", \"Gap 2\", \"Gap 3\", \"Gap 4\", \"Gap 5\"],
    \"service_pages\": [\"Service page 1\", \"Service page 2\"],
    \"location_pages\": [\"Location page 1\", \"Location page 2\"],
    \"support_pages\": [\"FAQ/support page 1\", \"FAQ/support page 2\"]
}
Provide 5-8 keywords with realistic difficulty/volume estimates for the specific industry and location. Make blog topics and page opportunities specific to the actual business, not generic.";

        $model = function_exists('vmo_get_model') ? vmo_get_model('nano') : 'gpt-5.5-nano';
        $token_param = function_exists('vmo_get_token_param') ? vmo_get_token_param($model, 1500) : ['max_completion_tokens' => 1500];

        $response = wp_remote_post('https://api.openai.com/v1/chat/completions', [
            'headers' => [
                'Authorization' => 'Bearer ' . $api_key,
                'Content-Type' => 'application/json',
            ],
            'body' => wp_json_encode(array_merge([
                'model' => $model,
                'messages' => [
                    ['role' => 'user', 'content' => $prompt]
                ],
                'temperature' => 0.6,
            ], $token_param)),
            'timeout' => 30,
        ]);

        if (!is_wp_error($response)) {
            $body = json_decode(wp_remote_retrieve_body($response), true);
            $content = $body['choices'][0]['message']['content'] ?? '';
            // Strip code block fences if present
            $content = preg_replace('/^```(?:json)?\s*|\s*```$/s', '', trim($content));
            $parsed = json_decode($content, true);
            if (is_array($parsed) && !empty($parsed['keywords'])) {
                $recommendations = array_merge($recommendations, $parsed);
                set_transient($cache_key, $recommendations, DAY_IN_SECONDS);
                return $recommendations;
            }
        }
    }

    // Fallback: template-based recommendations if AI unavailable
    $recommendations['keywords'] = [
        ['keyword' => strtolower($industry) . ' services near me', 'difficulty' => 35, 'volume' => 4000, 'opportunity' => 85],
        ['keyword' => 'best ' . strtolower($industry) . ' companies', 'difficulty' => 55, 'volume' => 3000, 'opportunity' => 70],
        ['keyword' => strtolower($industry) . ' solutions for ' . strtolower($target_audience), 'difficulty' => 30, 'volume' => 1500, 'opportunity' => 90],
    ];
    $recommendations['blog_topics'] = [
        "How to Choose the Right {$industry} Partner in " . date('Y'),
        "Top {$industry} Trends {$target_audience} Should Know",
        "{$industry} Guide: Everything {$target_audience} Needs to Get Started",
        "Why {$industry} Matters for {$target_audience} in " . date('Y'),
        "Common {$industry} Mistakes and How to Avoid Them"
    ];
    $recommendations['technical_actions'] = [
        'Optimize page loading speed (target < 3s)',
        'Implement structured data / schema markup',
        'Fix mobile responsiveness issues',
        'Improve internal linking structure',
        'Optimize images with alt text and compression'
    ];
    $recommendations['content_gaps'] = [
        "Missing content addressing {$target_audience} pain points",
        "No content targeting local {$industry} search terms",
        "Lack of comparison content vs competitors",
        "Missing FAQ section for common {$industry} queries",
        "No case studies or testimonials page"
    ];
    $recommendations['service_pages'] = [
        "Primary {$industry} service page for {$target_audience}",
        "Pricing and consultation page for {$industry} buyers"
    ];
    $recommendations['location_pages'] = $location ? [
        "{$industry} services in {$location}",
        "Local {$industry} help near {$location}"
    ] : [];
    $recommendations['support_pages'] = [
        "Frequently asked questions for {$industry} customers",
        "Customer onboarding and next steps page"
    ];
    
    return $recommendations;
}

// Create email campaigns tracking table
function vmo_create_email_campaigns_table() {
    global $wpdb;
    
    $table_name = $wpdb->prefix . 'vmo_email_campaigns';
    
    $charset_collate = $wpdb->get_charset_collate();
    
    $sql = "CREATE TABLE $table_name (
        id mediumint(9) NOT NULL AUTO_INCREMENT,
        user_id bigint(20) NOT NULL,
        campaign_name varchar(255) NOT NULL,
        subject varchar(255) NOT NULL,
        content longtext NOT NULL,
        recipients_count int(11) DEFAULT 0,
        sent_count int(11) DEFAULT 0,
        opened_count int(11) DEFAULT 0,
        clicked_count int(11) DEFAULT 0,
        revenue_generated decimal(10,2) DEFAULT 0,
        status varchar(20) DEFAULT 'draft',
        created_at datetime DEFAULT CURRENT_TIMESTAMP,
        sent_at datetime NULL,
        PRIMARY KEY (id),
        KEY user_id (user_id),
        KEY status (status)
    ) $charset_collate;";
    
    require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
    dbDelta($sql);
}

// Get email campaign statistics
function vmo_get_email_campaign_stats($user_id, $from, $to) {
    global $wpdb;
    $table_name = $wpdb->prefix . 'vmo_email_campaigns';
    
    $stats = [
        'sent' => 0,
        'open_rate' => 0,
        'click_rate' => 0,
        'campaigns' => 0,
        'revenue' => 0,
        'chart' => []
    ];
    
    // Check if table exists
    $table_exists = vmo_table_exists($table_name);
    if (!$table_exists) {
        return $stats;
    }
    
    $campaign_data = $wpdb->get_row($wpdb->prepare("
        SELECT 
            SUM(sent_count) as total_sent,
            SUM(opened_count) as total_opened,
            SUM(clicked_count) as total_clicked,
            SUM(revenue_generated) as total_revenue,
            COUNT(*) as campaign_count
        FROM {$table_name} 
        WHERE user_id = %d AND sent_at BETWEEN %s AND %s
    ", $user_id, $from, $to));
    
    if ($campaign_data) {
        $stats['sent'] = (int) $campaign_data->total_sent;
        $stats['open_rate'] = $campaign_data->total_sent > 0 ? round(($campaign_data->total_opened / $campaign_data->total_sent) * 100, 1) : 0;
        $stats['click_rate'] = $campaign_data->total_opened > 0 ? round(($campaign_data->total_clicked / $campaign_data->total_opened) * 100, 1) : 0;
        $stats['campaigns'] = (int) $campaign_data->campaign_count;
        $stats['revenue'] = (float) $campaign_data->total_revenue;
    }
    
    // Generate chart data for the last 7 days
    for ($i = 6; $i >= 0; $i--) {
        $date = date('Y-m-d', strtotime("-{$i} days"));
        $daily_sent = $wpdb->get_var($wpdb->prepare("
            SELECT SUM(sent_count) 
            FROM {$table_name} 
            WHERE user_id = %d AND DATE(sent_at) = %s
        ", $user_id, $date));
        
        $stats['chart'][] = [
            'date' => $date,
            'sent' => (int) ($daily_sent ?? 0)
        ];
    }
    
    return $stats;
}

// Get user email campaigns
function vmo_get_user_email_campaigns($user_id, $limit = 10) {
    global $wpdb;
    $table_name = $wpdb->prefix . 'vmo_email_campaigns';
    
    $table_exists = vmo_table_exists($table_name);
    if (!$table_exists) {
        return [];
    }
    
    $campaigns = $wpdb->get_results($wpdb->prepare("
        SELECT * FROM {$table_name} 
        WHERE user_id = %d 
        ORDER BY created_at DESC 
        LIMIT %d
    ", $user_id, $limit));
    
    return $campaigns ?? [];
}

// Calculate revenue metrics
function vmo_calculate_revenue_metrics($user_id) {
    global $wpdb;
    
    $revenue_data = [
        'today' => 0,
        'week' => 0,
        'month' => 0,
        'year' => 0,
        'mom_change' => 0,
        'chart' => [],
        'transactions' => [],
        'paid_revenue' => 0,
        'pending_fulfillment_revenue' => 0,
        'completed_fulfilled_revenue' => 0,
        'refunds' => 0,
        'fees' => 0,
        'taxes' => 0,
        'net_revenue' => 0,
        'fulfillment_summary' => []
    ];
    
    $now = current_time('mysql');
    $today = date('Y-m-d');
    $week_ago = date('Y-m-d', strtotime('-7 days'));
    $month_ago = date('Y-m-d', strtotime('-30 days'));
    $year_ago = date('Y-m-d', strtotime('-365 days'));
    $last_month = date('Y-m-d', strtotime('-60 days'));

    $orders_table = $wpdb->prefix . 'vmo_store_orders';
    if (vmo_table_exists($orders_table)) {
        $revenue_data['today'] = round(((float) $wpdb->get_var($wpdb->prepare(
            "SELECT COALESCE(SUM(amount_total), 0) FROM {$orders_table} WHERE user_id = %d AND status = 'paid' AND DATE(created_at) = %s",
            $user_id,
            $today
        ))) / 100, 2);
        $revenue_data['week'] = round(((float) $wpdb->get_var($wpdb->prepare(
            "SELECT COALESCE(SUM(amount_total), 0) FROM {$orders_table} WHERE user_id = %d AND status = 'paid' AND created_at >= %s",
            $user_id,
            $week_ago
        ))) / 100, 2);
        $revenue_data['month'] = round(((float) $wpdb->get_var($wpdb->prepare(
            "SELECT COALESCE(SUM(amount_total), 0) FROM {$orders_table} WHERE user_id = %d AND status = 'paid' AND created_at >= %s",
            $user_id,
            $month_ago
        ))) / 100, 2);
        $revenue_data['year'] = round(((float) $wpdb->get_var($wpdb->prepare(
            "SELECT COALESCE(SUM(amount_total), 0) FROM {$orders_table} WHERE user_id = %d AND status = 'paid' AND created_at >= %s",
            $user_id,
            $year_ago
        ))) / 100, 2);

        $last_month_revenue = round(((float) $wpdb->get_var($wpdb->prepare(
            "SELECT COALESCE(SUM(amount_total), 0) FROM {$orders_table} WHERE user_id = %d AND status = 'paid' AND created_at BETWEEN %s AND %s",
            $user_id,
            $last_month,
            $month_ago
        ))) / 100, 2);
        if ($last_month_revenue > 0) {
            $revenue_data['mom_change'] = round((($revenue_data['month'] - $last_month_revenue) / $last_month_revenue) * 100, 1);
        }

        for ($day_offset = 29; $day_offset >= 0; $day_offset--) {
            $date = date('Y-m-d', strtotime("-{$day_offset} days"));
            $daily_revenue = $wpdb->get_var($wpdb->prepare(
                "SELECT COALESCE(SUM(amount_total), 0) FROM {$orders_table} WHERE user_id = %d AND status = 'paid' AND DATE(created_at) = %s",
                $user_id,
                $date
            ));
            $revenue_data['chart'][] = [
                'date' => $date,
                'revenue' => round(((float) ($daily_revenue ?? 0)) / 100, 2)
            ];
        }

        $revenue_data['transactions'] = $wpdb->get_results($wpdb->prepare(
            "SELECT id, created_at, amount_total, currency, status, metadata FROM {$orders_table} WHERE user_id = %d AND status = 'paid' ORDER BY created_at DESC LIMIT 10",
            $user_id
        )) ?? [];

        if (function_exists('vmo_business_flow_revenue_summary')) {
            $summary = vmo_business_flow_revenue_summary($user_id, 365);
            $revenue_data['fulfillment_summary'] = $summary;
            $revenue_data['paid_revenue'] = round(((int) ($summary['paid_revenue'] ?? 0)) / 100, 2);
            $revenue_data['pending_fulfillment_revenue'] = round(((int) ($summary['pending_fulfillment_revenue'] ?? 0)) / 100, 2);
            $revenue_data['completed_fulfilled_revenue'] = round(((int) ($summary['completed_fulfilled_revenue'] ?? 0)) / 100, 2);
            $revenue_data['refunds'] = round(((int) ($summary['refunds'] ?? 0)) / 100, 2);
            $revenue_data['fees'] = round(((int) ($summary['fees'] ?? 0)) / 100, 2);
            $revenue_data['taxes'] = round(((int) ($summary['taxes'] ?? 0)) / 100, 2);
            $revenue_data['net_revenue'] = round(((int) ($summary['net_revenue'] ?? 0)) / 100, 2);
        }
    }

    $purchases_table = $wpdb->prefix . 'vmo_contact_purchases';
    $used_purchase_fallback = false;
    if ($revenue_data['year'] <= 0 && vmo_table_exists($purchases_table)) {
        $used_purchase_fallback = true;
        $revenue_data['today'] = (float) $wpdb->get_var($wpdb->prepare(
            "SELECT COALESCE(SUM(amount), 0) FROM {$purchases_table} WHERE user_id = %d AND status = 'completed' AND DATE(purchase_date) = %s",
            $user_id,
            $today
        ));
        $revenue_data['week'] = (float) $wpdb->get_var($wpdb->prepare(
            "SELECT COALESCE(SUM(amount), 0) FROM {$purchases_table} WHERE user_id = %d AND status = 'completed' AND purchase_date >= %s",
            $user_id,
            $week_ago
        ));
        $revenue_data['month'] = (float) $wpdb->get_var($wpdb->prepare(
            "SELECT COALESCE(SUM(amount), 0) FROM {$purchases_table} WHERE user_id = %d AND status = 'completed' AND purchase_date >= %s",
            $user_id,
            $month_ago
        ));
        $revenue_data['year'] = (float) $wpdb->get_var($wpdb->prepare(
            "SELECT COALESCE(SUM(amount), 0) FROM {$purchases_table} WHERE user_id = %d AND status = 'completed' AND purchase_date >= %s",
            $user_id,
            $year_ago
        ));

        $last_month_revenue = (float) $wpdb->get_var($wpdb->prepare(
            "SELECT COALESCE(SUM(amount), 0) FROM {$purchases_table} WHERE user_id = %d AND status = 'completed' AND purchase_date BETWEEN %s AND %s",
            $user_id,
            $last_month,
            $month_ago
        ));
        if ($last_month_revenue > 0) {
            $revenue_data['mom_change'] = round((($revenue_data['month'] - $last_month_revenue) / $last_month_revenue) * 100, 1);
        }

        $revenue_data['chart'] = [];
        for ($day_offset = 29; $day_offset >= 0; $day_offset--) {
            $date = date('Y-m-d', strtotime("-{$day_offset} days"));
            $daily_revenue = $wpdb->get_var($wpdb->prepare(
                "SELECT COALESCE(SUM(amount), 0) FROM {$purchases_table} WHERE user_id = %d AND status = 'completed' AND DATE(purchase_date) = %s",
                $user_id,
                $date
            ));
            $revenue_data['chart'][] = [
                'date' => $date,
                'revenue' => (float) ($daily_revenue ?? 0)
            ];
        }

        $revenue_data['transactions'] = $wpdb->get_results($wpdb->prepare(
            "SELECT id, purchase_date AS created_at, amount, currency, status, product_service, transaction_id FROM {$purchases_table} WHERE user_id = %d AND status = 'completed' ORDER BY purchase_date DESC LIMIT 10",
            $user_id
        )) ?? [];
    }

    $ledger_table = $wpdb->prefix . 'vmo_financial_ledger';
    if (!$used_purchase_fallback && vmo_table_exists($ledger_table)) {
        $outside_sources = "'manual_entry','jarvis_instruction','phone_call','website_chat','form','crm_update','uploaded_receipt','bank_statement'";
        $revenue_data['today'] += (float) $wpdb->get_var($wpdb->prepare(
            "SELECT COALESCE(SUM(amount), 0) FROM {$ledger_table} WHERE user_id = %d AND status = 'posted' AND direction = 'income' AND source IN ({$outside_sources}) AND DATE(transaction_date) = %s",
            $user_id,
            $today
        ));
        $revenue_data['week'] += (float) $wpdb->get_var($wpdb->prepare(
            "SELECT COALESCE(SUM(amount), 0) FROM {$ledger_table} WHERE user_id = %d AND status = 'posted' AND direction = 'income' AND source IN ({$outside_sources}) AND transaction_date >= %s",
            $user_id,
            $week_ago
        ));
        $revenue_data['month'] += (float) $wpdb->get_var($wpdb->prepare(
            "SELECT COALESCE(SUM(amount), 0) FROM {$ledger_table} WHERE user_id = %d AND status = 'posted' AND direction = 'income' AND source IN ({$outside_sources}) AND transaction_date >= %s",
            $user_id,
            $month_ago
        ));
        $revenue_data['year'] += (float) $wpdb->get_var($wpdb->prepare(
            "SELECT COALESCE(SUM(amount), 0) FROM {$ledger_table} WHERE user_id = %d AND status = 'posted' AND direction = 'income' AND source IN ({$outside_sources}) AND transaction_date >= %s",
            $user_id,
            $year_ago
        ));

        if (!empty($revenue_data['chart'])) {
            $ledger_daily = $wpdb->get_results($wpdb->prepare(
                "SELECT DATE(transaction_date) AS day, COALESCE(SUM(amount), 0) AS revenue FROM {$ledger_table} WHERE user_id = %d AND status = 'posted' AND direction = 'income' AND source IN ({$outside_sources}) AND transaction_date >= %s GROUP BY DATE(transaction_date)",
                $user_id,
                date('Y-m-d', strtotime('-29 days'))
            ), ARRAY_A) ?: [];
            $ledger_by_day = [];
            foreach ($ledger_daily as $row) {
                $ledger_by_day[$row['day']] = (float) $row['revenue'];
            }
            foreach ($revenue_data['chart'] as &$chart_row) {
                $date_key = $chart_row['date'] ?? '';
                if ($date_key && isset($ledger_by_day[$date_key])) {
                    $chart_row['revenue'] = round((float) $chart_row['revenue'] + $ledger_by_day[$date_key], 2);
                }
            }
            unset($chart_row);
        }

        $ledger_transactions = $wpdb->get_results($wpdb->prepare(
            "SELECT id, transaction_date AS created_at, amount, currency, status, product_service, transaction_id, source FROM {$ledger_table} WHERE user_id = %d AND status = 'posted' AND direction = 'income' AND source IN ({$outside_sources}) ORDER BY transaction_date DESC LIMIT 10",
            $user_id
        )) ?: [];
        if ($ledger_transactions) {
            $revenue_data['transactions'] = array_slice(array_merge($ledger_transactions, (array) $revenue_data['transactions']), 0, 10);
        }
    }
    
    return $revenue_data;
}

// Format currency helper
function vmo_format_currency($amount, $currency = 'USD') {
    return '$' . number_format((float) $amount, 2);
}

// Get CRM analytics
function vmo_get_crm_analytics($user_id) {
    global $wpdb;
    $contacts_table = $wpdb->prefix . 'vmo_contacts';
    
    $crm_data = [
        'contacts' => 0,
        'leads' => 0,
        'conversion_rate' => 0,
        'pipeline_value' => 0,
        'activities' => [],
        'sources' => []
    ];
    
    // Check if contacts table exists
    $table_exists = vmo_table_exists($contacts_table);
    if (!$table_exists) {
        return $crm_data;
    }
    
    // Get total contacts
    $crm_data['contacts'] = (int) $wpdb->get_var($wpdb->prepare("
        SELECT COUNT(*) FROM {$contacts_table} WHERE user_id = %d
    ", $user_id));
    
    // Treat any non-customer contact as a lead for dashboard rollups.
    $crm_data['leads'] = (int) $wpdb->get_var($wpdb->prepare(
        "SELECT COUNT(*) FROM {$contacts_table} WHERE user_id = %d AND lifecycle_stage <> 'customer' AND primary_category <> 'website_visitor'",
        $user_id
    ));

    // Calculate conversion rate (customers / contacts)
    $converted = (int) $wpdb->get_var($wpdb->prepare(
        "SELECT COUNT(*) FROM {$contacts_table} WHERE user_id = %d AND lifecycle_stage = 'customer'",
        $user_id
    ));
    
    if ($crm_data['contacts'] > 0) {
        $crm_data['conversion_rate'] = round(($converted / $crm_data['contacts']) * 100, 1);
    }
    
    // Best-effort pipeline rollup from value_potential (0-100) for non-customers.
    // This avoids querying non-existent columns like estimated_value.
    $crm_data['pipeline_value'] = (float) $wpdb->get_var($wpdb->prepare(
        "SELECT SUM(COALESCE(value_potential, 0)) FROM {$contacts_table} WHERE user_id = %d AND lifecycle_stage <> 'customer'",
        $user_id
    ));
    
    return $crm_data;
}

// Get user leads data with pagination
function vmo_get_user_leads($user_id, $search = '', $offset = 0, $limit = 20) {
    global $wpdb;
    $contacts_table = $wpdb->prefix . 'vmo_contacts';
    
    $leads_data = [
        'data' => [],
        'total' => 0
    ];
    
    // Check if contacts table exists
    $table_exists = vmo_table_exists($contacts_table);
    if (!$table_exists) {
        return $leads_data;
    }
    
    // Lead = any contact that isn't a customer.
    $where_clause = "WHERE user_id = %d AND lifecycle_stage <> 'customer'";
    $params = [$user_id];
    
    if (!empty($search)) {
        $where_clause .= " AND (COALESCE(full_name, CONCAT(COALESCE(first_name,''),' ',COALESCE(last_name,'')), '') LIKE %s OR email LIKE %s OR company LIKE %s OR notes LIKE %s)";
        $search_param = '%' . $wpdb->esc_like($search) . '%';
        $params[] = $search_param;
        $params[] = $search_param;
        $params[] = $search_param;
        $params[] = $search_param;
    }
    
    // Get total count
    $leads_data['total'] = (int) $wpdb->get_var($wpdb->prepare("
        SELECT COUNT(*) FROM {$contacts_table} {$where_clause}
    ", ...$params));
    
    // Get paginated data
    $params_with_limit = array_merge($params, [$limit, $offset]);
    $leads_data['data'] = $wpdb->get_results(
        $wpdb->prepare(
            "SELECT *
             FROM {$contacts_table} {$where_clause}
             ORDER BY updated_at DESC, created_at DESC
             LIMIT %d OFFSET %d",
            ...$params_with_limit
        ),
        ARRAY_A
    );
    
    return $leads_data;
}

// Generate AI recommendations based on industry insights and roadmaps
function vmo_generate_ai_recommendations($user_id, $consultation_profile, $industry_insights) {
    global $wpdb;

    $recommendations = [
        'actions' => [],
        'priority' => 0,
        'impact' => 'medium',
        'time_estimate' => '1-2 weeks'
    ];

    if (empty($consultation_profile)) {
        return $recommendations;
    }

    $industry = $consultation_profile['industry'] ?? 'General';
    $business_name = $consultation_profile['business_name'] ?? '';
    $business_goals = $consultation_profile['business_goals'] ?? [];

    // ── Gather actual business state to derive recommendations ──

    // 1. Check published page count
    $page_count = (int) $wpdb->get_var($wpdb->prepare(
        "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_author = %d AND post_status = 'publish' AND post_type IN ('page','post')",
        $user_id
    ));

    // 2. Check SEO basics
    $has_ssl = is_ssl();
    $has_yoast = is_plugin_active('wordpress-seo/wp-seo.php') || is_plugin_active('wordpress-seo-premium/wp-seo-premium.php');
    $has_seo_plugin = $has_yoast || is_plugin_active('all-in-one-seo-pack/all_in_one_seo_pack.php') || is_plugin_active('seo-by-rank-math/rank-math.php');

    // 3. Check contacts / leads
    $contacts_table = $wpdb->prefix . 'vmo_contacts';
    $lead_count = 0;
    if (vmo_table_exists($contacts_table)) {
        $lead_count = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$contacts_table} WHERE user_id = %d", $user_id
        ));
    }

    // 4. Check phone system
    $has_phone = !empty(get_user_meta($user_id, 'vmo_twilio_phone_number', true));

    // 5. Check VMO Pay / store
    $has_store = !empty(get_user_meta($user_id, 'vmo_pay_account_id', true));

    // 6. Check roadmap progress
    $roadmap_table = $wpdb->prefix . 'vmo_business_roadmaps';
    $roadmap_completion = 0;
    if (vmo_table_exists($roadmap_table)) {
        $roadmap_completion = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT AVG(progress_percent) FROM {$roadmap_table} WHERE user_id = %d AND status = 'active'",
            $user_id
        ));
    }

    // 7. Check recent content freshness
    $last_post = $wpdb->get_var($wpdb->prepare(
        "SELECT MAX(post_date) FROM {$wpdb->posts} WHERE post_author = %d AND post_status = 'publish'",
        $user_id
    ));
    $days_since_publish = $last_post ? (int) ((time() - strtotime($last_post)) / 86400) : 999;

    // ── Build recommendations from actual gaps ──
    $recs = [];

    // Content recommendations
    if ($page_count < 5) {
        $recs[] = [
            'title' => "Create foundational website pages (About, Services, Contact, FAQ, Blog)",
            'description' => "Your site has only {$page_count} published pages. Search engines and visitors need more content to understand your {$industry} business.",
            'priority' => 9,
            'estimated_hours' => 8,
            'expected_impact' => ['conversion_rate' => '+30%', 'traffic' => '+50%']
        ];
    } elseif ($page_count < 15) {
        $recs[] = [
            'title' => "Expand content with service detail pages and blog posts",
            'description' => "With {$page_count} pages you have a good start. Adding detailed service pages and regular blog content will significantly boost SEO.",
            'priority' => 6,
            'estimated_hours' => 12,
            'expected_impact' => ['conversion_rate' => '+15%', 'traffic' => '+25%']
        ];
    }

    if ($days_since_publish > 30 && $page_count > 0) {
        $recs[] = [
            'title' => "Publish fresh content to maintain search rankings",
            'description' => "Your last published content was {$days_since_publish} days ago. Search engines reward sites with regular updates.",
            'priority' => 7,
            'estimated_hours' => 4,
            'expected_impact' => ['conversion_rate' => '+5%', 'traffic' => '+15%']
        ];
    }

    // SEO recommendations
    if (!$has_seo_plugin) {
        $recs[] = [
            'title' => "Set up SEO optimization for all pages",
            'description' => "No SEO plugin detected. Adding meta descriptions, title tags, and structured data will improve search visibility for your {$industry} business.",
            'priority' => 8,
            'estimated_hours' => 4,
            'expected_impact' => ['conversion_rate' => '+10%', 'traffic' => '+35%']
        ];
    }

    if (!$has_ssl) {
        $recs[] = [
            'title' => "Enable SSL certificate for website security",
            'description' => "Your site is not using HTTPS. This hurts search rankings and customer trust. SSL is essential for any business website.",
            'priority' => 10,
            'estimated_hours' => 2,
            'expected_impact' => ['conversion_rate' => '+20%', 'traffic' => '+10%']
        ];
    }

    // Lead generation recommendations
    if ($lead_count === 0) {
        $recs[] = [
            'title' => "Implement lead capture to start building your customer pipeline",
            'description' => "You have no contacts in your CRM yet. Add contact forms, lead magnets, or call-to-action buttons to start capturing leads.",
            'priority' => 8,
            'estimated_hours' => 6,
            'expected_impact' => ['conversion_rate' => '+25%', 'traffic' => '+5%']
        ];
    } elseif ($lead_count < 20) {
        $recs[] = [
            'title' => "Grow your lead pipeline with targeted outreach",
            'description' => "You have {$lead_count} contacts. Consider email campaigns, social media promotion, or paid advertising to grow your pipeline.",
            'priority' => 5,
            'estimated_hours' => 8,
            'expected_impact' => ['conversion_rate' => '+15%', 'traffic' => '+20%']
        ];
    }

    // Phone system
    if (!$has_phone) {
        $recs[] = [
            'title' => "Set up business phone system for professional communication",
            'description' => "A dedicated business phone number with IVR, call recording, and voicemail increases credibility and captures phone leads automatically.",
            'priority' => 5,
            'estimated_hours' => 2,
            'expected_impact' => ['conversion_rate' => '+15%', 'traffic' => '+0%']
        ];
    }

    // E-commerce / payments
    if (!$has_store) {
        $has_products = !empty($consultation_profile['products'] ?? $consultation_profile['offerings'] ?? []);
        if ($has_products || in_array(strtolower($industry), ['retail', 'ecommerce', 'food', 'fashion', 'beauty'])) {
            $recs[] = [
                'title' => "Enable VMO Pay to accept online payments",
                'description' => "Connect Stripe to start selling products/services directly from your website. Online payment capability can significantly increase revenue.",
                'priority' => 7,
                'estimated_hours' => 3,
                'expected_impact' => ['conversion_rate' => '+20%', 'traffic' => '+5%']
            ];
        }
    }

    // Roadmap progress
    if ($roadmap_completion > 0 && $roadmap_completion < 50) {
        $recs[] = [
            'title' => "Complete outstanding roadmap milestones",
            'description' => "Your business roadmap is {$roadmap_completion}% complete. Finishing active milestones will accelerate growth and keep your strategy on track.",
            'priority' => 4,
            'estimated_hours' => 10,
            'expected_impact' => ['conversion_rate' => '+10%', 'traffic' => '+10%']
        ];
    }

    // Add business-goal-specific recommendations
    foreach ($business_goals as $goal) {
        $goal_lower = strtolower((string) $goal);
        if (strpos($goal_lower, 'ecommerce') !== false || strpos($goal_lower, 'sell') !== false) {
            if (!$has_store) {
                $recs[] = [
                    'title' => "Launch your online store to achieve your e-commerce goals",
                    'description' => "You listed e-commerce as a business goal but don't have payments set up yet. Connect VMO Pay and add product listings.",
                    'priority' => 8,
                    'estimated_hours' => 6,
                    'expected_impact' => ['conversion_rate' => '+25%', 'traffic' => '+15%']
                ];
            }
        }
        if (strpos($goal_lower, 'brand') !== false || strpos($goal_lower, 'awareness') !== false) {
            $recs[] = [
                'title' => "Build brand awareness through content marketing",
                'description' => "Create consistent branded content, social media profiles, and local directory listings to increase {$business_name} visibility.",
                'priority' => 5,
                'estimated_hours' => 12,
                'expected_impact' => ['conversion_rate' => '+10%', 'traffic' => '+30%']
            ];
        }
    }

    // Ensure at least one recommendation
    if (empty($recs)) {
        $recs[] = [
            'title' => "Review and optimize your existing website content",
            'description' => "Your {$industry} business setup is looking good. Review existing pages for accuracy, update outdated information, and ensure all calls-to-action are working.",
            'priority' => 3,
            'estimated_hours' => 4,
            'expected_impact' => ['conversion_rate' => '+5%', 'traffic' => '+5%']
        ];
    }

    // Sort by priority descending, cap at 6 recommendations
    usort($recs, function($a, $b) { return $b['priority'] - $a['priority']; });
    $recs = array_slice($recs, 0, 6);

    // Build final actions array with stable IDs
    foreach ($recs as $rec) {
        $recommendations['actions'][] = array_merge($rec, [
            'id' => 'rec_' . md5($rec['title'] . $user_id),
        ]);
    }

    $recommendations['priority'] = array_sum(array_column($recommendations['actions'], 'priority'));

    // Set overall impact and time estimate based on top recommendation
    $top_priority = $recommendations['actions'][0]['priority'] ?? 0;
    $recommendations['impact'] = $top_priority >= 8 ? 'high' : ($top_priority >= 5 ? 'medium' : 'low');
    $total_hours = array_sum(array_column($recommendations['actions'], 'estimated_hours'));
    $recommendations['time_estimate'] = $total_hours <= 10 ? '1 week' : ($total_hours <= 30 ? '2-3 weeks' : '4+ weeks');

    return $recommendations;
}

// Process AI action approval
function vmo_process_ai_action_approval($user_id, $action_id, $approval_status) {
    global $wpdb;
    
    // Create AI actions table if it doesn't exist
    $actions_table = $wpdb->prefix . 'vmo_ai_actions';
    $table_exists = vmo_table_exists($actions_table);
    
    if (!$table_exists) {
        vmo_create_ai_actions_table();
    }
    
    $result = [
        'success' => false,
        'error' => null,
        'next_steps' => [],
        'tracking_id' => null
    ];
    
    // Insert or update action approval
    $tracking_id = 'track_' . uniqid();
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    $inserted = $wpdb->insert(
        $actions_table,
        [
            'user_id' => $user_id,
            'action_id' => $action_id,
            'status' => $approval_status,
            'tracking_id' => $tracking_id,
            'created_at' => current_time('mysql'),
            'updated_at' => current_time('mysql')
        ],
        ['%d', '%s', '%s', '%s', '%s', '%s']
    );
    
    if ($inserted) {
        $result['success'] = true;
        $result['tracking_id'] = $tracking_id;
        
        if ($approval_status === 'approved') {
            $result['next_steps'] = [
                'AI will begin implementation within 1 hour',
                'Progress updates will be sent to your dashboard',
                'Results will be tracked and reported automatically'
            ];
        }
    } else {
        $result['error'] = 'Failed to process approval';
    }
    
    return $result;
}

// Create AI actions tracking table
function vmo_create_ai_actions_table() {
    global $wpdb;
    
    $table_name = $wpdb->prefix . 'vmo_ai_actions';
    
    $charset_collate = $wpdb->get_charset_collate();
    
    $sql = "CREATE TABLE $table_name (
        id mediumint(9) NOT NULL AUTO_INCREMENT,
        user_id bigint(20) NOT NULL,
        action_id varchar(100) NOT NULL,
        status varchar(20) DEFAULT 'pending',
        tracking_id varchar(100) NOT NULL,
        results longtext NULL,
        created_at datetime DEFAULT CURRENT_TIMESTAMP,
        updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        PRIMARY KEY (id),
        KEY user_id (user_id),
        KEY action_id (action_id),
        KEY status (status)
    ) $charset_collate;";
    
    require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
    dbDelta($sql);
}

// Get detailed user analytics
function vmo_get_detailed_user_analytics($user_id, $type, $period) {
    global $wpdb;

    $user_id = (int) $user_id;
    $type = sanitize_key((string) $type);
    $period = sanitize_key((string) $period);

    $analytics = ['data' => [], 'total' => 0];

    $interactions_table = $wpdb->prefix . 'vmo_contact_interactions';
    $table_exists = vmo_table_exists($interactions_table);
    if (!$table_exists) {
        return $analytics;
    }

    $interval = '30 DAY';
    if (in_array($period, ['7d', '7days', 'week'], true)) $interval = '7 DAY';
    elseif (in_array($period, ['14d', '14days'], true)) $interval = '14 DAY';
    elseif (in_array($period, ['30d', '30days', 'month'], true)) $interval = '30 DAY';
    elseif (in_array($period, ['90d', '90days', 'quarter'], true)) $interval = '90 DAY';
    elseif (in_array($period, ['365d', '365days', 'year', '12m'], true)) $interval = '365 DAY';
    elseif (in_array($period, ['all', 'lifetime'], true)) $interval = null;

    $since_sql = $interval ? " AND created_at >= DATE_SUB(NOW(), INTERVAL {$interval})" : '';

    if ($type === 'referrers') {
        $rows = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT referrer, COUNT(*) AS visits FROM (
                    SELECT COALESCE(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(full_details,'$.referrer')),''),'(direct)') AS referrer
                    FROM {$interactions_table}
                    WHERE user_id=%d AND interaction_type='website_visit' {$since_sql}
                ) t
                GROUP BY referrer
                ORDER BY visits DESC
                LIMIT 20",
                $user_id
            ),
            ARRAY_A
        );

        // Fallback if JSON functions not supported
        if ($wpdb->last_error) {
            $wpdb->last_error = '';
            $rows_raw = $wpdb->get_results(
                $wpdb->prepare(
                    "SELECT summary FROM {$interactions_table} WHERE user_id=%d AND interaction_type='website_visit' {$since_sql} ORDER BY created_at DESC LIMIT 2000",
                    $user_id
                ),
                ARRAY_A
            );
            $counts = [];
            foreach (($rows_raw ?: []) as $rr) {
                $s = (string) ($rr['summary'] ?? '');
                $ref = '(direct)';
                if (strpos($s, 'Visitor from ') === 0) {
                    $ref = trim(substr($s, strlen('Visitor from ')));
                    if ($ref === '') $ref = '(direct)';
                }
                $counts[$ref] = ($counts[$ref] ?? 0) + 1;
            }
            arsort($counts);
            $rows = [];
            foreach (array_slice($counts, 0, 20, true) as $ref => $visits) {
                $rows[] = ['referrer' => $ref, 'visits' => (int) $visits];
            }
        }

        $total = 0;
        foreach (($rows ?: []) as $r) $total += (int) ($r['visits'] ?? 0);
        $analytics['total'] = $total;
        foreach (($rows ?: []) as $r) {
            $visits = (int) ($r['visits'] ?? 0);
            $analytics['data'][] = [
                'source' => (string) ($r['referrer'] ?? '(direct)'),
                'visits' => $visits,
                'percentage' => $total > 0 ? (int) round(($visits / $total) * 100) : 0,
            ];
        }
        return $analytics;
    }

    if ($type === 'pages') {
        $rows = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT interaction_subtype AS page, COUNT(*) AS views, COUNT(DISTINCT contact_id) AS unique_visitors
                 FROM {$interactions_table}
                 WHERE user_id=%d AND interaction_type='website_visit' {$since_sql}
                 GROUP BY interaction_subtype
                 ORDER BY views DESC
                 LIMIT 50",
                $user_id
            ),
            ARRAY_A
        );

        $total = 0;
        foreach (($rows ?: []) as $r) $total += (int) ($r['views'] ?? 0);
        $analytics['total'] = $total;
        foreach (($rows ?: []) as $r) {
            $analytics['data'][] = [
                'page' => (string) ($r['page'] ?? ''),
                'views' => (int) ($r['views'] ?? 0),
                'unique' => (int) ($r['unique_visitors'] ?? 0),
            ];
        }
        return $analytics;
    }

    if ($type === 'activities') {
        $rows = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT interaction_type AS activity, COUNT(*) AS cnt
                 FROM {$interactions_table}
                 WHERE user_id=%d {$since_sql}
                 GROUP BY interaction_type
                 ORDER BY cnt DESC
                 LIMIT 20",
                $user_id
            ),
            ARRAY_A
        );

        $total = 0;
        foreach (($rows ?: []) as $r) $total += (int) ($r['cnt'] ?? 0);
        $analytics['total'] = $total;
        foreach (($rows ?: []) as $r) {
            $analytics['data'][] = [
                'activity' => (string) ($r['activity'] ?? ''),
                'count' => (int) ($r['cnt'] ?? 0),
            ];
        }
        return $analytics;
    }

    return $analytics;
}

// Get visitor geography data
function vmo_get_visitor_geography($user_id, $type) {
    global $wpdb;

    $user_id = (int) $user_id;
    $type = sanitize_key((string) $type);

    $geo_data = ['data' => [], 'top' => []];

    $interactions_table = $wpdb->prefix . 'vmo_contact_interactions';
    $table_exists = vmo_table_exists($interactions_table);
    if (!$table_exists) {
        return $geo_data;
    }

    $json_key = ($type === 'countries') ? '$.country' : '$.region';

    $rows = $wpdb->get_results(
        $wpdb->prepare(
            "SELECT COALESCE(NULLIF(JSON_UNQUOTE(JSON_EXTRACT(metadata,%s)),'') ,'(unknown)') AS name,
                    COUNT(*) AS visitors
             FROM {$interactions_table}
             WHERE user_id=%d AND interaction_type='website_visit'
             GROUP BY name
             ORDER BY visitors DESC
             LIMIT 20",
            $json_key,
            $user_id
        ),
        ARRAY_A
    );

    // Fallback if JSON functions not supported
    if ($wpdb->last_error) {
        $wpdb->last_error = '';
        $raw = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT metadata FROM {$interactions_table} WHERE user_id=%d AND interaction_type='website_visit' ORDER BY created_at DESC LIMIT 2000",
                $user_id
            ),
            ARRAY_A
        );
        $counts = [];
        $field = ($type === 'countries') ? 'country' : 'region';
        foreach (($raw ?: []) as $rr) {
            $m = json_decode((string) ($rr['metadata'] ?? ''), true);
            $name = '(unknown)';
            if (is_array($m) && !empty($m[$field])) $name = (string) $m[$field];
            $counts[$name] = ($counts[$name] ?? 0) + 1;
        }
        arsort($counts);
        $rows = [];
        foreach (array_slice($counts, 0, 20, true) as $name => $visitors) {
            $rows[] = ['name' => $name, 'visitors' => (int) $visitors];
        }
    }

    $total = 0;
    foreach (($rows ?: []) as $r) $total += (int) ($r['visitors'] ?? 0);

    foreach (($rows ?: []) as $r) {
        $visitors = (int) ($r['visitors'] ?? 0);
        $geo_data['data'][] = [
            'name' => (string) ($r['name'] ?? '(unknown)'),
            'visitors' => $visitors,
            'percentage' => $total > 0 ? (int) round(($visitors / $total) * 100) : 0,
        ];
    }

    $geo_data['top'] = array_slice($geo_data['data'], 0, 5);
    return $geo_data;
}

// Generate revenue analytics
function vmo_generate_revenue_analytics($user_id, $period) {
    global $wpdb;

    $user_id = (int) $user_id;
    $period = sanitize_key((string) $period);

    $analytics = [
        'revenue_trend' => [],
        'top_products' => [],
        'customer_segments' => [],
        'growth_metrics' => []
    ];

    $orders_table = $wpdb->prefix . 'vmo_store_orders';
    $products_table = $wpdb->prefix . 'vmo_store_products';
    $has_orders = vmo_table_exists($orders_table);
    if (!$has_orders) {
        return $analytics;
    }

    $interval = '30 DAY';
    if (in_array($period, ['7d', '7days', 'week'], true)) $interval = '7 DAY';
    elseif (in_array($period, ['14d', '14days'], true)) $interval = '14 DAY';
    elseif (in_array($period, ['30d', '30days', 'month'], true)) $interval = '30 DAY';
    elseif (in_array($period, ['90d', '90days', 'quarter'], true)) $interval = '90 DAY';
    elseif (in_array($period, ['365d', '365days', 'year', '12m'], true)) $interval = '365 DAY';
    elseif (in_array($period, ['all', 'lifetime'], true)) $interval = null;

    $since_sql = $interval ? " AND created_at >= DATE_SUB(NOW(), INTERVAL {$interval})" : '';

    // Revenue trend
    $bucket = ($interval === '365 DAY') ? "DATE_FORMAT(created_at,'%%Y-%%m')" : "DATE(created_at)";
    $trend_rows = $wpdb->get_results(
        $wpdb->prepare(
            "SELECT {$bucket} AS period_label, SUM(amount_total) AS revenue_cents, COUNT(*) AS orders
             FROM {$orders_table}
             WHERE user_id=%d AND status='paid' {$since_sql}
             GROUP BY period_label
             ORDER BY period_label ASC
             LIMIT 100",
            $user_id
        ),
        ARRAY_A
    );

    foreach (($trend_rows ?: []) as $r) {
        $analytics['revenue_trend'][] = [
            'period' => (string) ($r['period_label'] ?? ''),
            'revenue' => (float) (((int) ($r['revenue_cents'] ?? 0)) / 100),
            'orders' => (int) ($r['orders'] ?? 0),
        ];
    }

    // Top products (best-effort: relies on metadata.product_id)
    $has_products = vmo_table_exists($products_table);
    if ($has_products) {
        $top_rows = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata,'$.product_id')) AS UNSIGNED) AS product_id,
                        SUM(amount_total) AS revenue_cents,
                        COUNT(*) AS units
                 FROM {$orders_table}
                 WHERE user_id=%d AND status='paid' {$since_sql}
                 AND JSON_UNQUOTE(JSON_EXTRACT(metadata,'$.product_id')) IS NOT NULL
                 GROUP BY product_id
                 ORDER BY revenue_cents DESC
                 LIMIT 10",
                $user_id
            ),
            ARRAY_A
        );

        if (!$wpdb->last_error) {
            foreach (($top_rows ?: []) as $tr) {
                $pid = (int) ($tr['product_id'] ?? 0);
                if ($pid <= 0) continue;
                $name = (string) $wpdb->get_var($wpdb->prepare("SELECT name FROM {$products_table} WHERE id=%d", $pid));
                if ($name === '') $name = 'Product #' . $pid;
                $analytics['top_products'][] = [
                    'name' => $name,
                    'revenue' => (float) (((int) ($tr['revenue_cents'] ?? 0)) / 100),
                    'units' => (int) ($tr['units'] ?? 0),
                ];
            }
        } else {
            $wpdb->last_error = '';
        }
    }

    // Customer segments (new vs returning vs guest) based on paid orders
    $cust_rows = $wpdb->get_results(
        $wpdb->prepare(
            "SELECT buyer_user_id, COUNT(*) AS orders, SUM(amount_total) AS revenue_cents
             FROM {$orders_table}
             WHERE user_id=%d AND status='paid' {$since_sql}
             GROUP BY buyer_user_id",
            $user_id
        ),
        ARRAY_A
    );

    $seg = [
        'new' => ['orders' => 0, 'revenue_cents' => 0],
        'returning' => ['orders' => 0, 'revenue_cents' => 0],
        'guest' => ['orders' => 0, 'revenue_cents' => 0],
    ];

    foreach (($cust_rows ?: []) as $cr) {
        $buyer = isset($cr['buyer_user_id']) ? (int) $cr['buyer_user_id'] : 0;
        $orders = (int) ($cr['orders'] ?? 0);
        $rev = (int) ($cr['revenue_cents'] ?? 0);
        if ($buyer <= 0) {
            $seg['guest']['orders'] += $orders;
            $seg['guest']['revenue_cents'] += $rev;
        } elseif ($orders <= 1) {
            $seg['new']['orders'] += $orders;
            $seg['new']['revenue_cents'] += $rev;
        } else {
            $seg['returning']['orders'] += $orders;
            $seg['returning']['revenue_cents'] += $rev;
        }
    }

    $total_rev = $seg['new']['revenue_cents'] + $seg['returning']['revenue_cents'] + $seg['guest']['revenue_cents'];
    $mk = function($label, $bucket) use ($total_rev) {
        $rev_cents = (int) ($bucket['revenue_cents'] ?? 0);
        return [
            'segment' => $label,
            'percentage' => $total_rev > 0 ? (int) round(($rev_cents / $total_rev) * 100) : 0,
            'revenue' => (float) ($rev_cents / 100),
        ];
    };
    $analytics['customer_segments'] = [
        $mk('New Customers', $seg['new']),
        $mk('Returning Customers', $seg['returning']),
        $mk('Guest Checkout', $seg['guest']),
    ];

    // Growth metrics: current vs previous interval (if interval known)
    $now = current_time('mysql');
    $curr_rev = (int) $wpdb->get_var($wpdb->prepare("SELECT COALESCE(SUM(amount_total),0) FROM {$orders_table} WHERE user_id=%d AND status='paid' {$since_sql}", $user_id));
    $curr_orders = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$orders_table} WHERE user_id=%d AND status='paid' {$since_sql}", $user_id));
    $avg = $curr_orders > 0 ? ($curr_rev / $curr_orders) : 0;

    $prev_rev = 0;
    if ($interval) {
        $prev_rev = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COALESCE(SUM(amount_total),0) FROM {$orders_table}
             WHERE user_id=%d AND status='paid'
             AND created_at < DATE_SUB(NOW(), INTERVAL {$interval})
             AND created_at >= DATE_SUB(NOW(), INTERVAL " . ($interval === '7 DAY' ? '14 DAY' : ($interval === '14 DAY' ? '28 DAY' : ($interval === '30 DAY' ? '60 DAY' : ($interval === '90 DAY' ? '180 DAY' : '730 DAY')))) . ")",
            $user_id
        ));
    }
    $change = 0.0;
    if ($prev_rev > 0) $change = round((($curr_rev - $prev_rev) / $prev_rev) * 100, 2);
    elseif ($curr_rev > 0) $change = 100.0;

    $analytics['growth_metrics'] = [
        'revenue_total' => (float) ($curr_rev / 100),
        'orders_total' => $curr_orders,
        'avg_order_value' => (float) ($avg / 100),
        'revenue_change_percent' => $change,
        'as_of' => $now,
    ];

    return $analytics;
}

// Market Intelligence functions — derive insights from consultation profile,
// phase outputs, and CRM data instead of returning empty arrays.

function vmo_analyze_market_trends($industry) {
    if (empty($industry) || $industry === 'General') return [];

    $user_id = get_current_user_id();
    global $wpdb;

    // Check phase_outputs for market_research data first
    $table = $wpdb->prefix . 'vmo_phase_outputs';
    if (vmo_table_exists($table)) {
        $latest = $wpdb->get_var($wpdb->prepare(
            "SELECT output_json FROM {$table} WHERE user_id = %d AND output_type = 'market_research' AND is_active = 1 ORDER BY created_at DESC LIMIT 1",
            $user_id
        ));
        if ($latest) {
            $data = json_decode($latest, true);
            if (is_array($data) && !empty($data['trends'])) {
                return $data['trends'];
            }
        }
    }

    // Derive basic trends from industry profile
    $industry_trends = [
        'Technology' => [
            ['trend' => 'AI integration in business workflows', 'direction' => 'up', 'impact' => 'high'],
            ['trend' => 'Cloud-first operations', 'direction' => 'up', 'impact' => 'high'],
            ['trend' => 'Cybersecurity investment increasing', 'direction' => 'up', 'impact' => 'medium'],
        ],
        'Healthcare' => [
            ['trend' => 'Telehealth adoption growing', 'direction' => 'up', 'impact' => 'high'],
            ['trend' => 'Patient data privacy regulations tightening', 'direction' => 'up', 'impact' => 'high'],
            ['trend' => 'AI-assisted diagnostics', 'direction' => 'up', 'impact' => 'medium'],
        ],
        'Finance' => [
            ['trend' => 'Fintech disruption accelerating', 'direction' => 'up', 'impact' => 'high'],
            ['trend' => 'Embedded finance growing', 'direction' => 'up', 'impact' => 'medium'],
            ['trend' => 'Regulatory compliance costs rising', 'direction' => 'up', 'impact' => 'medium'],
        ],
        'Retail' => [
            ['trend' => 'Omnichannel commerce essential', 'direction' => 'up', 'impact' => 'high'],
            ['trend' => 'Social commerce expansion', 'direction' => 'up', 'impact' => 'medium'],
            ['trend' => 'Sustainable products demand', 'direction' => 'up', 'impact' => 'medium'],
        ],
        'Education' => [
            ['trend' => 'Online learning platforms growing', 'direction' => 'up', 'impact' => 'high'],
            ['trend' => 'Micro-credentials and certification demand', 'direction' => 'up', 'impact' => 'medium'],
            ['trend' => 'AI tutoring tools adoption', 'direction' => 'up', 'impact' => 'medium'],
        ],
    ];

    return $industry_trends[$industry] ?? [
        ['trend' => 'Digital transformation accelerating', 'direction' => 'up', 'impact' => 'high'],
        ['trend' => 'Customer experience focus increasing', 'direction' => 'up', 'impact' => 'medium'],
        ['trend' => 'AI adoption across industries', 'direction' => 'up', 'impact' => 'high'],
    ];
}

function vmo_track_competitor_activities($user_id) {
    global $wpdb;

    // Pull competitor data from phase_outputs market_research
    $table = $wpdb->prefix . 'vmo_phase_outputs';
    if (vmo_table_exists($table)) {
        $latest = $wpdb->get_var($wpdb->prepare(
            "SELECT output_json FROM {$table} WHERE user_id = %d AND output_type = 'market_research' AND is_active = 1 ORDER BY created_at DESC LIMIT 1",
            $user_id
        ));
        if ($latest) {
            $data = json_decode($latest, true);
            if (is_array($data) && !empty($data['competitors'])) {
                return $data['competitors'];
            }
        }
    }

    return [];
}

function vmo_identify_opportunities($profile) {
    if (empty($profile)) return [];

    $user_id = get_current_user_id();
    global $wpdb;

    // Check phase_outputs
    $table = $wpdb->prefix . 'vmo_phase_outputs';
    if (vmo_table_exists($table)) {
        $latest = $wpdb->get_var($wpdb->prepare(
            "SELECT output_json FROM {$table} WHERE user_id = %d AND output_type = 'market_research' AND is_active = 1 ORDER BY created_at DESC LIMIT 1",
            $user_id
        ));
        if ($latest) {
            $data = json_decode($latest, true);
            if (is_array($data) && !empty($data['opportunities'])) {
                return $data['opportunities'];
            }
        }
    }

    // Derive from profile
    $industry = $profile['industry'] ?? 'General';
    $target = $profile['target_audience'] ?? '';
    $opportunities = [];
    if (!empty($target)) {
        $opportunities[] = ['opportunity' => "Underserved {$target} segment in {$industry}", 'potential' => 'high'];
    }
    $opportunities[] = ['opportunity' => 'AI-powered automation for competitive advantage', 'potential' => 'high'];
    $opportunities[] = ['opportunity' => 'Digital-first customer engagement', 'potential' => 'medium'];

    return $opportunities;
}

function vmo_assess_market_risks($industry) {
    if (empty($industry) || $industry === 'General') return [];

    $user_id = get_current_user_id();
    global $wpdb;

    // Check phase_outputs
    $table = $wpdb->prefix . 'vmo_phase_outputs';
    if (vmo_table_exists($table)) {
        $latest = $wpdb->get_var($wpdb->prepare(
            "SELECT output_json FROM {$table} WHERE user_id = %d AND output_type = 'market_research' AND is_active = 1 ORDER BY created_at DESC LIMIT 1",
            $user_id
        ));
        if ($latest) {
            $data = json_decode($latest, true);
            if (is_array($data) && !empty($data['threats'])) {
                return $data['threats'];
            }
        }
    }

    // Standard risks per industry
    $base_risks = [
        ['risk' => 'Market saturation in ' . $industry, 'severity' => 'medium'],
        ['risk' => 'Emerging low-cost competitors', 'severity' => 'medium'],
        ['risk' => 'Regulatory changes', 'severity' => 'low'],
    ];

    return $base_risks;
}

function vmo_count_monitored_competitors($user_id) {
    global $wpdb;

    // Count from phase_outputs market_research competitors
    $table = $wpdb->prefix . 'vmo_phase_outputs';
    if (vmo_table_exists($table)) {
        $latest = $wpdb->get_var($wpdb->prepare(
            "SELECT output_json FROM {$table} WHERE user_id = %d AND output_type = 'market_research' AND is_active = 1 ORDER BY created_at DESC LIMIT 1",
            $user_id
        ));
        if ($latest) {
            $data = json_decode($latest, true);
            if (is_array($data) && !empty($data['competitors'])) {
                return count($data['competitors']);
            }
        }
    }

    return 0;
}

/* --------------------------------------------------------------
   SHIPPING & FULFILLMENT BACKEND FUNCTIONS
---------------------------------------------------------------- */

// Get Active Orders awaiting shipment
function vmo_get_active_orders() {
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if (!function_exists('vmo_verify_admin_security') || !vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Security check failed'], 403);
    }

    global $wpdb;
    $user_id = get_current_user_id();

    $flow_tasks_table = $wpdb->prefix . 'vmo_fulfillment_tasks';
    if (function_exists('vmo_table_exists') && vmo_table_exists($flow_tasks_table)) {
        $flow_orders = $wpdb->get_results($wpdb->prepare(
            "SELECT id, payment_order_id, customer_name, customer_email, amount_total, currency, current_status, owner_action, created_at FROM {$flow_tasks_table}
             WHERE user_id = %d AND current_status NOT IN ('completed','fulfilled','delivered','cancelled','refunded')
             ORDER BY created_at DESC LIMIT 20",
            $user_id
        ));
        if (!empty($flow_orders)) {
            $formatted_orders = [];
            foreach ($flow_orders as $order) {
                $formatted_orders[] = [
                    'id' => (int) $order->id,
                    'payment_order_id' => (int) $order->payment_order_id,
                    'customer_name' => $order->customer_name ?: 'Unknown Customer',
                    'customer_email' => $order->customer_email ?: '',
                    'total' => number_format(((int) $order->amount_total) / 100, 2),
                    'currency' => $order->currency ?: 'usd',
                    'items_count' => 1,
                    'shipping_status' => $order->current_status ?: 'awaiting_fulfillment',
                    'owner_action' => $order->owner_action ?: '',
                    'created_at' => $order->created_at,
                ];
            }
            wp_send_json_success([
                'orders' => $formatted_orders,
                'total' => count($formatted_orders),
                'source' => 'business_flow_tasks',
            ]);
            return;
        }
    }

    // Ensure expected schema exists (dbDelta is safe to call repeatedly)
    if (function_exists('vmo_create_internal_store_orders_table')) {
        vmo_create_internal_store_orders_table();
    }
    
    // Check for orders table
    $orders_table = $wpdb->prefix . 'vmo_internal_store_orders';
    $table_exists = $wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $orders_table));
    
    if (!$table_exists) {
        // If table doesn't exist, return empty list instead of error
        wp_send_json_success([
            'orders' => [],
            'total' => 0
        ]);
        return;
    }
    
    // Get orders that are paid but not yet shipped
    $orders = $wpdb->get_results($wpdb->prepare("
        SELECT 
            id,
            customer_name,
            customer_email,
            total_amount as amount,
            items_count,
            shipping_status,
            created_at
        FROM {$orders_table} 
        WHERE user_id = %d 
        AND status = 'paid' 
        AND (shipping_status IS NULL OR shipping_status IN ('pending', 'processing'))
        ORDER BY created_at DESC
        LIMIT 20
    ", $user_id));
    
    // If no orders found, return empty array
    if (empty($orders)) {
        wp_send_json_success([
            'orders' => [],
            'total' => 0
        ]);
        return;
    }
    
    // Format orders for display
    $formatted_orders = [];
    foreach ($orders as $order) {
        $formatted_orders[] = [
            'id' => $order->id,
            'customer_name' => $order->customer_name ?: 'Unknown Customer',
            'customer_email' => $order->customer_email ?: '',
            'total' => number_format((float) $order->amount, 2),
            'items_count' => $order->items_count ?: 1,
            'shipping_status' => $order->shipping_status ?: 'pending',
            'created_at' => $order->created_at
        ];
    }
    
    wp_send_json_success([
        'orders' => $formatted_orders,
        'total' => count($formatted_orders)
    ]);
}

// Approve Shipment for an order
function vmo_approve_shipment() {
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if (!function_exists('vmo_verify_admin_security') || !vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Security check failed'], 403);
    }

    $order_id = sanitize_text_field($_POST['order_id'] ?? '');
    if (empty($order_id)) {
        wp_send_json_error(['message' => 'Order ID required']);
    }

    global $wpdb;
    $user_id = get_current_user_id();
    $orders_table = $wpdb->prefix . 'vmo_internal_store_orders';

    // Ensure expected schema exists (dbDelta is safe to call repeatedly)
    if (function_exists('vmo_create_internal_store_orders_table')) {
        vmo_create_internal_store_orders_table();
    }
    
    // Verify order belongs to user
    $order = $wpdb->get_row($wpdb->prepare("
        SELECT * FROM {$orders_table} 
        WHERE id = %s AND user_id = %d AND status = 'paid'
    ", $order_id, $user_id));
    
    if (!$order) {
        $flow_tasks_table = $wpdb->prefix . 'vmo_fulfillment_tasks';
        if (function_exists('vmo_table_exists') && vmo_table_exists($flow_tasks_table)) {
            $flow_task = $wpdb->get_row($wpdb->prepare(
                "SELECT * FROM {$flow_tasks_table} WHERE id = %d AND user_id = %d LIMIT 1",
                (int) $order_id,
                (int) $user_id
            ));
            if ($flow_task && function_exists('vmo_business_flow_mark_order_status')) {
                $target_status = sanitize_text_field($_POST['status'] ?? 'completed');
                if ($target_status === 'approved') {
                    $target_status = 'packed';
                }
                $marked = vmo_business_flow_mark_order_status(
                    (int) $user_id,
                    (int) $flow_task->payment_order_id,
                    $target_status,
                    'Updated from the dashboard fulfillment queue.',
                    !empty($_POST['notify_buyer'])
                );
                if (is_array($marked) && !is_wp_error($marked)) {
                    wp_send_json_success([
                        'message' => 'Business flow updated to ' . $target_status . '.',
                        'order_id' => (int) $flow_task->payment_order_id,
                        'fulfillment_task_id' => (int) $flow_task->id,
                    ]);
                }
                wp_send_json_error(['message' => is_wp_error($marked) ? $marked->get_error_message() : ($marked['message'] ?? 'Failed to update business flow task')]);
            }
        }
        wp_send_json_error(['message' => 'Order not found or unauthorized']);
    }
    
    // Update shipping status to approved
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    $updated = $wpdb->update(
        $orders_table,
        [
            'shipping_status' => 'approved',
            'shipping_approved_at' => current_time('mysql')
        ],
        ['id' => $order_id, 'user_id' => $user_id],
        ['%s', '%s'],
        ['%d', '%d']
    );
    
    if ($updated) {
        if (function_exists('vmo_business_flow_mark_from_internal_order')) {
            vmo_business_flow_mark_from_internal_order((int) $order_id, $user_id, 'packed', 'Shipment approved from the legacy fulfillment queue.');
        }
        // Automated label purchase / tracking is not implemented.
        // Users can contact carriers directly to arrange contracts and shipping workflows.
        wp_send_json_success([
            'message' => 'Shipment approved. Arrange carrier pickup/labels directly with your selected shipping company.',
            'order_id' => (int) $order_id,
        ]);
    } else {
        wp_send_json_error(['message' => 'Failed to approve shipment']);
    }
}

/**
 * Immediately fulfill an order: Generate Label -> Save Tracking -> Email Customer
 */
// Helper: Generate Tracking Number
function vmo_generate_tracking_number($order_id, $carrier) {
    // Tracking numbers must come from real carrier API label purchases (e.g., EasyPost, ShipStation, Shippo).
    // This function checks if the user has carrier API credentials configured and attempts to create a label.
    _deprecated_function(__FUNCTION__, '1.0', 'Carrier API label purchase via vmo_fulfill_order_now()');

    $user_id = get_current_user_id();
    $settings = get_user_meta($user_id, 'vmo_shipping_settings', true);

    // Attempt to find configured carrier API for this carrier
    if (is_array($settings) && !empty($settings['carriers'])) {
        foreach ($settings['carriers'] as $c) {
            $c_name = strtolower($c['name'] ?? $c['id'] ?? '');
            if ($c_name === strtolower($carrier) && !empty($c['api_key'])) {
                // Carrier API key found but no SDK integration yet — log and return empty
                if (defined('WP_DEBUG') && WP_DEBUG) error_log("[VMO] vmo_generate_tracking_number: Carrier '{$carrier}' has API key but SDK integration is pending for order #{$order_id}");
                return '';
            }
        }
    }

    // No carrier API configured
    return '';
}

function vmo_fulfill_order_now($order_id, $order) {
    global $wpdb;
    $table = $wpdb->prefix . 'vmo_internal_store_orders';

    // Validate the order exists and is pending
    if (!$order || empty($order->id)) {
        return ['success' => false, 'message' => 'Invalid order data.'];
    }
    if (($order->shipping_status ?? '') === 'shipped') {
        return ['success' => false, 'message' => 'Order has already been shipped.'];
    }

    // Determine carrier from order or user settings
    $carrier = $order->carrier ?? '';
    if (empty($carrier)) {
        $user_id = $order->user_id ?? get_current_user_id();
        $settings = get_user_meta($user_id, 'vmo_shipping_settings', true);
        if (is_array($settings) && !empty($settings['carriers'])) {
            foreach ($settings['carriers'] as $c) {
                if (!empty($c['enabled'])) { $carrier = $c['name'] ?? $c['id'] ?? ''; break; }
            }
        }
    }

    // Update order to shipped
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    $updated = $wpdb->update(
        $table,
        [
            'shipping_status' => 'shipped',
            'carrier' => $carrier ?: 'Manual',
            'shipped_at' => current_time('mysql'),
        ],
        ['id' => (int) $order_id],
        ['%s', '%s', '%s'],
        ['%d']
    );

    if ($updated === false) {
        return ['success' => false, 'message' => 'Database update failed: ' . $wpdb->last_error];
    }

    // Log to shipping analytics
    $analytics_table = $wpdb->prefix . 'vmo_shipping_analytics';
    if (vmo_table_exists($analytics_table)) {
        $created = $order->created_at ?? current_time('mysql');
        $hours = max(0.1, (time() - strtotime($created)) / 3600);
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $wpdb->insert($analytics_table, [
            'user_id' => $order->user_id ?? get_current_user_id(),
            'order_id' => (string) $order_id,
            'carrier' => $carrier ?: 'Manual',
            'service_type' => 'Standard',
            'shipping_cost' => 0,
            'fulfillment_hours' => round($hours, 2),
            'on_time_delivery' => ($hours <= 48) ? 1 : 0,
            'shipped_date' => current_time('mysql'),
        ], ['%d','%s','%s','%s','%s','%s','%d','%s']);
    }

    if (function_exists('vmo_business_flow_mark_from_internal_order')) {
        vmo_business_flow_mark_from_internal_order((int) $order_id, (int) ($order->user_id ?? get_current_user_id()), 'shipped', 'Marked shipped from the legacy fulfillment queue.');
    }

    return ['success' => true, 'message' => 'Order #' . $order_id . ' marked as shipped via ' . ($carrier ?: 'Manual') . '.'];
}

// Get Order Details (for the Shipping UI modal)
function vmo_get_order_details() {
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if (!vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Invalid nonce', 'status' => 'invalid_nonce'], 403);
    }

    $order_id = sanitize_text_field($_POST['order_id'] ?? '');
    if ($order_id === '') {
        wp_send_json_error(['message' => 'Order ID required']);
    }

    global $wpdb;
    $user_id = get_current_user_id();
    $orders_table = $wpdb->prefix . 'vmo_internal_store_orders';

    if (function_exists('vmo_create_internal_store_orders_table')) {
        vmo_create_internal_store_orders_table();
    }

    $order = $wpdb->get_row($wpdb->prepare(
        "SELECT id, customer_name, customer_email, shipping_address, total_amount, currency, status, carrier, tracking_number, created_at, shipped_at, shipping_status FROM {$orders_table} WHERE id = %d AND user_id = %d",
        (int) $order_id,
        (int) $user_id
    ));

    if (!$order) {
        $flow_tasks_table = $wpdb->prefix . 'vmo_fulfillment_tasks';
        if (function_exists('vmo_table_exists') && vmo_table_exists($flow_tasks_table)) {
            $flow_task = $wpdb->get_row($wpdb->prepare(
                "SELECT id, payment_order_id, customer_name, customer_email, shipping_address, amount_total, currency, current_status, owner_action, created_at, updated_at FROM {$flow_tasks_table} WHERE id = %d AND user_id = %d LIMIT 1",
                (int) $order_id,
                (int) $user_id
            ));
            if ($flow_task) {
                wp_send_json_success([
                    'order' => [
                        'id' => (int) $flow_task->id,
                        'payment_order_id' => (int) $flow_task->payment_order_id,
                        'customer_name' => $flow_task->customer_name ?: 'Unknown Customer',
                        'customer_email' => $flow_task->customer_email ?: '',
                        'shipping_address' => $flow_task->shipping_address ?: '',
                        'total_amount' => round(((int) $flow_task->amount_total) / 100, 2),
                        'currency' => $flow_task->currency ?: 'usd',
                        'status' => $flow_task->current_status ?: 'awaiting_fulfillment',
                        'shipping_status' => $flow_task->current_status ?: 'awaiting_fulfillment',
                        'owner_action' => $flow_task->owner_action ?: '',
                        'created_at' => $flow_task->created_at,
                        'updated_at' => $flow_task->updated_at,
                        'source' => 'business_flow_task',
                    ],
                ]);
            }
        }
        wp_send_json_error(['message' => 'Order not found']);
    }

    wp_send_json_success([
        'id' => (int) $order->id,
        'customer_name' => (string) ($order->customer_name ?? ''),
        'customer_email' => (string) ($order->customer_email ?? ''),
        'shipping_address' => (string) ($order->shipping_address ?? ''),
        'total' => number_format((float) ($order->total_amount ?? 0), 2),
        'currency' => (string) ($order->currency ?? 'USD'),
        'status' => (string) ($order->status ?? ''),
        'shipping_status' => (string) ($order->shipping_status ?? ''),
        'carrier' => (string) ($order->carrier ?? ''),
        'tracking_number' => (string) ($order->tracking_number ?? ''),
        'created_at' => (string) ($order->created_at ?? ''),
        'shipped_at' => (string) ($order->shipped_at ?? ''),
    ]);
}

// Get Carrier Config (per-user)
function vmo_get_carrier_config() {
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if (!vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Invalid nonce', 'status' => 'invalid_nonce'], 403);
    }

    $carrier_id = sanitize_key($_POST['carrier_id'] ?? '');
    if ($carrier_id === '') {
        wp_send_json_error(['message' => 'carrier_id required']);
    }

    $user_id = get_current_user_id();
    $configs = get_user_meta($user_id, 'vmo_carrier_configs', true);
    if (!is_array($configs)) {
        $configs = [];
    }

    $cfg = $configs[$carrier_id] ?? [];
    wp_send_json_success([
        'carrier_id' => $carrier_id,
        'api_key' => (string) ($cfg['api_key'] ?? ''),
        'secret' => (string) ($cfg['secret'] ?? ''),
        'service' => (string) ($cfg['service'] ?? ''),
        'updated_at' => (string) ($cfg['updated_at'] ?? ''),
    ]);
}

// Save Carrier Config (per-user)
function vmo_save_carrier_config() {
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if (!vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Invalid nonce', 'status' => 'invalid_nonce'], 403);
    }

    $carrier_id = sanitize_key($_POST['carrier_id'] ?? '');
    if ($carrier_id === '') {
        wp_send_json_error(['message' => 'carrier_id required']);
    }

    $api_key = sanitize_text_field($_POST['api_key'] ?? '');
    $secret = sanitize_text_field($_POST['secret'] ?? '');
    $service = sanitize_text_field($_POST['service'] ?? '');

    $user_id = get_current_user_id();
    $configs = get_user_meta($user_id, 'vmo_carrier_configs', true);
    if (!is_array($configs)) {
        $configs = [];
    }

    $configs[$carrier_id] = [
        'api_key' => $api_key,
        'secret' => $secret,
        'service' => $service,
        'updated_at' => current_time('mysql')
    ];

    update_user_meta($user_id, 'vmo_carrier_configs', $configs);
    wp_send_json_success(['message' => 'Carrier configuration saved', 'carrier_id' => $carrier_id]);
}

// Save Fulfillment Config (per-user)
function vmo_save_fulfillment_config() {
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if (!vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Invalid nonce', 'status' => 'invalid_nonce'], 403);
    }

    $provider = sanitize_text_field($_POST['provider'] ?? '');
    if ($provider === '') {
        wp_send_json_error(['message' => 'provider required']);
    }

    $sync_frequency = sanitize_text_field($_POST['sync_frequency'] ?? '');
    $auto_fulfill = !empty($_POST['auto_fulfill']) && $_POST['auto_fulfill'] !== '0';

    $user_id = get_current_user_id();
    $configs = get_user_meta($user_id, 'vmo_fulfillment_configs', true);
    if (!is_array($configs)) {
        $configs = [];
    }

    $configs[$provider] = [
        'sync_frequency' => $sync_frequency,
        'auto_fulfill' => $auto_fulfill,
        'updated_at' => current_time('mysql')
    ];

    update_user_meta($user_id, 'vmo_fulfillment_configs', $configs);
    wp_send_json_success(['message' => 'Fulfillment settings saved', 'provider' => $provider]);
}

// Bulk approve shipments (used by Bulk Process modal)
function vmo_bulk_approve_shipments() {
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if (!vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Invalid nonce', 'status' => 'invalid_nonce'], 403);
    }

    $order_ids_raw = wp_unslash($_POST['order_ids'] ?? '[]');
    if (is_string($order_ids_raw)) {
        $order_ids = json_decode($order_ids_raw, true);
    } else {
        $order_ids = $order_ids_raw;
    }
    if (!is_array($order_ids)) {
        wp_send_json_error(['message' => 'Invalid order_ids']);
    }

    $order_ids = array_values(array_filter(array_map('intval', $order_ids), fn($v) => $v > 0));
    if (!$order_ids) {
        wp_send_json_success(['processed' => 0, 'errors' => []]);
    }

    global $wpdb;
    $user_id = get_current_user_id();
    $orders_table = $wpdb->prefix . 'vmo_internal_store_orders';

    if (function_exists('vmo_create_internal_store_orders_table')) {
        vmo_create_internal_store_orders_table();
    }

    $processed = 0;
    $errors = [];
    foreach ($order_ids as $order_id) {
        $order = $wpdb->get_row($wpdb->prepare(
            "SELECT * FROM {$orders_table} WHERE id = %d AND user_id = %d AND status = 'paid'",
            (int) $order_id,
            (int) $user_id
        ));

        if (!$order) {
            $flow_tasks_table = $wpdb->prefix . 'vmo_fulfillment_tasks';
            if (function_exists('vmo_table_exists') && vmo_table_exists($flow_tasks_table)) {
                $flow_task = $wpdb->get_row($wpdb->prepare(
                    "SELECT id, payment_order_id FROM {$flow_tasks_table} WHERE id = %d AND user_id = %d LIMIT 1",
                    (int) $order_id,
                    (int) $user_id
                ));
                if ($flow_task && function_exists('vmo_business_flow_mark_order_status')) {
                    $marked = vmo_business_flow_mark_order_status(
                        (int) $user_id,
                        (int) $flow_task->payment_order_id,
                        'packed',
                        'Bulk approved from the dashboard fulfillment queue.',
                        false
                    );
                    if (is_array($marked) && !is_wp_error($marked)) {
                        $processed++;
                        continue;
                    }
                    $errors[] = ['order_id' => $order_id, 'message' => is_wp_error($marked) ? $marked->get_error_message() : ($marked['message'] ?? 'Business flow update failed')];
                    continue;
                }
            }
            $errors[] = ['order_id' => $order_id, 'message' => 'Order not found or not eligible'];
            continue;
        }

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $wpdb->update(
            $orders_table,
            [
                'shipping_status' => 'approved',
                'shipping_approved_at' => current_time('mysql')
            ],
            ['id' => $order_id, 'user_id' => $user_id],
            ['%s', '%s'],
            ['%d', '%d']
        );

        $processed++;
        if (function_exists('vmo_business_flow_mark_from_internal_order')) {
            vmo_business_flow_mark_from_internal_order((int) $order_id, (int) $user_id, 'packed', 'Bulk approved from the legacy fulfillment queue.');
        }
    }

    wp_send_json_success(['processed' => $processed, 'errors' => $errors]);
}

// Get Shipping Settings
function vmo_get_shipping_settings() {
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if (!function_exists('vmo_verify_admin_security') || !vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Security check failed'], 403);
    }
    
    $user_id = get_current_user_id();
    
    // Get user's shipping preferences
    $shipping_settings = get_user_meta($user_id, 'vmo_shipping_settings', true);
    
    if (!$shipping_settings) {
        // Default settings
        $shipping_settings = [
            'carriers' => [
                ['id' => 'fedex', 'name' => 'FedEx Global', 'enabled' => true],
                ['id' => 'ups', 'name' => 'UPS Worldwide', 'enabled' => true],
                ['id' => 'dhl', 'name' => 'DHL Express', 'enabled' => false],
                ['id' => 'usps', 'name' => 'USPS International', 'enabled' => false],
                ['id' => 'aramex', 'name' => 'Aramex', 'enabled' => false],
                ['id' => 'tnt', 'name' => 'TNT (FedEx)', 'enabled' => false],
                ['id' => 'sf-express', 'name' => 'SF Express', 'enabled' => false],
                ['id' => 'correios', 'name' => 'Correios', 'enabled' => false],
                ['id' => 'ems', 'name' => 'EMS Global', 'enabled' => false]
            ],
            'fulfillment' => [
                ['id' => 'shipbob', 'name' => 'ShipBob Global', 'enabled' => false],
                ['id' => 'amazon-fba', 'name' => 'Amazon FBA Global', 'enabled' => false],
                ['id' => 'easyship', 'name' => 'Easyship', 'enabled' => false],
                ['id' => 'flexport', 'name' => 'Flexport', 'enabled' => false],
                ['id' => 'shipmonk', 'name' => 'ShipMonk', 'enabled' => false]
            ]
        ];
    }

    // Enrich settings with provider contact info (no placeholders; only known official contacts)
    $contacts = vmo_get_shipping_provider_contacts();
    foreach (['carriers' => 'carriers', 'fulfillment' => 'fulfillment'] as $groupKey => $contactKey) {
        if (empty($shipping_settings[$groupKey]) || !is_array($shipping_settings[$groupKey])) continue;
        foreach ($shipping_settings[$groupKey] as $i => $item) {
            $id = is_array($item) ? (string) ($item['id'] ?? '') : '';
            if ($id && isset($contacts[$contactKey][$id]) && is_array($contacts[$contactKey][$id])) {
                $shipping_settings[$groupKey][$i] = array_merge($item, $contacts[$contactKey][$id]);
            }
        }
    }
    
    wp_send_json_success($shipping_settings);
}

// Provider contact map for carriers/fulfillment (only include details that can be verified / stable)
function vmo_get_shipping_provider_contacts(): array {
    return [
        'carriers' => [
            'fedex' => [
                'support_url' => 'https://www.fedex.com/en-us/customer-support/contact-us.html',
                'support_phone' => '1-800-463-3339',
                'support_phone_label' => 'US Customer Service',
                'support_email' => 'ftn_us@fedex.com',
                'support_email_label' => 'FedEx Logistics Contact Center (US)'
            ],
            'ups' => [
                'support_url' => 'https://www.ups.com/us/en/support/contact-us.page',
            ],
            'dhl' => [
                'support_url' => 'https://www.dhl.com/us-en/home/customer-service.html',
            ],
            'usps' => [
                'support_url' => 'https://www.usps.com/help/contact-us.htm',
            ],
            'aramex' => [
                'support_url' => 'https://www.aramex.com/support/help-center',
                'office_locator_url' => 'https://www.aramex.com/find-aramex-office'
            ],
            'tnt' => [
                'support_url' => 'https://www.tnt.com/express/en_us/site/home/help-center.html',
            ],
            'sf-express' => [
                'support_url' => 'https://www.sf-international.com/us/en/',
            ],
            'correios' => [
                'support_url' => 'https://www.correios.com.br/falecomoscorreios/central-de-atendimento',
                'sales_url' => 'https://c.correios.com.br/atendimentocomercial'
            ],
            'ems' => [
                'support_url' => 'https://www.ems.post/en',
            ],
        ],
        'fulfillment' => [
            'shipbob' => [
                'support_url' => 'https://www.shipbob.com/contact/',
                'support_phone' => '1-312-313-1104',
                'support_phone_label' => 'Customer support',
                'sales_phone' => '1-844-474-4726',
                'sales_phone_label' => 'Sales'
            ],
            'amazon-fba' => [
                'support_url' => 'https://sell.amazon.com/fulfillment-by-amazon',
            ],
            'easyship' => [
                'support_url' => 'https://support.easyship.com/hc/en-us/requests/new',
                'contact_url' => 'https://www.easyship.com/contact'
            ],
            'flexport' => [
                'support_url' => 'https://www.flexport.com/get-started/',
            ],
            'shipmonk' => [
                'support_url' => 'https://www.shipmonk.com/get-a-quote',
            ],
        ],
    ];
}

// Save Shipping Settings
function vmo_save_shipping_settings() {
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if (!function_exists('vmo_verify_admin_security') || !vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Security check failed'], 403);
    }
    
    $user_id = get_current_user_id();
    $carriers = map_deep(wp_unslash($_POST['carriers'] ?? []), 'sanitize_text_field');
    $fulfillment = map_deep(wp_unslash($_POST['fulfillment'] ?? []), 'sanitize_text_field');
    
    // Format settings
    $settings = [
        'carriers' => [],
        'fulfillment' => [],
        'updated_at' => current_time('mysql')
    ];
    
    // Process carriers (covers all options in the Shipping Settings UI)
    $carrier_names = [
        'fedex' => 'FedEx Global',
        'ups' => 'UPS Worldwide',
        'dhl' => 'DHL Express',
        'usps' => 'USPS International',
        'aramex' => 'Aramex',
        'tnt' => 'TNT (FedEx)',
        'sf-express' => 'SF Express',
        'correios' => 'Correios',
        'ems' => 'EMS Global',
    ];
    foreach ($carrier_names as $id => $name) {
        $settings['carriers'][] = [
            'id' => $id,
            'name' => $name,
            'enabled' => !empty($carriers[$id])
        ];
    }

    // Process fulfillment (covers all options in the Shipping Settings UI)
    $fulfillment_names = [
        'shipbob' => 'ShipBob Global',
        'amazon-fba' => 'Amazon FBA Global',
        'easyship' => 'Easyship',
        'flexport' => 'Flexport',
        'shipmonk' => 'ShipMonk',
    ];
    foreach ($fulfillment_names as $id => $name) {
        $settings['fulfillment'][] = [
            'id' => $id,
            'name' => $name,
            'enabled' => !empty($fulfillment[$id])
        ];
    }
    
    $saved = update_user_meta($user_id, 'vmo_shipping_settings', $settings);
    
    if ($saved !== false) {
        wp_send_json_success(['message' => 'Shipping settings saved successfully']);
    } else {
        wp_send_json_error(['message' => 'Failed to save shipping settings']);
    }
}

// Helper: Test Carrier Connection
function vmo_test_carrier_connection($carrier_id = null) {
    $resolve_connection = function($resolved_carrier_id, $user_id) {
        $shipping_settings = get_user_meta($user_id, 'vmo_shipping_settings', true);
        $carrier_config = null;

        if (is_array($shipping_settings) && isset($shipping_settings['carriers']) && is_array($shipping_settings['carriers'])) {
            foreach ($shipping_settings['carriers'] as $carrier) {
                if (($carrier['id'] ?? '') === $resolved_carrier_id) {
                    $carrier_config = $carrier;
                    break;
                }
            }
        }

        if (!$carrier_config || empty($carrier_config['api_key'])) {
            return [
                'connected' => false,
                'simulated' => false,
                'message' => "No API credentials configured for {$resolved_carrier_id}. Add the provider API key before testing the connection.",
                'latency' => '0ms'
            ];
        }

        $endpoint = trim((string) ($carrier_config['endpoint'] ?? ''));
        if ($endpoint === '') {
            return [
                'connected' => false,
                'simulated' => false,
                'message' => "API credentials are present for {$resolved_carrier_id}, but no carrier endpoint is configured for live validation.",
                'latency' => '0ms'
            ];
        }

        $health_url = trailingslashit(untrailingslashit($endpoint)) . 'health';
        $started_at = microtime(true);
        $response = wp_remote_get($health_url, [
            'timeout' => 10,
            'headers' => ['Authorization' => 'Bearer ' . $carrier_config['api_key']],
        ]);
        $latency = round((microtime(true) - $started_at) * 1000) . 'ms';

        if (!is_wp_error($response)) {
            $status_code = (int) wp_remote_retrieve_response_code($response);
            if ($status_code >= 200 && $status_code < 300) {
                return [
                    'connected' => true,
                    'simulated' => false,
                    'message' => "Connected to {$resolved_carrier_id} successfully",
                    'latency' => $latency
                ];
            }

            return [
                'connected' => false,
                'simulated' => false,
                'message' => "Failed to connect to {$resolved_carrier_id}: HTTP {$status_code}",
                'latency' => $latency
            ];
        }

        return [
            'connected' => false,
            'simulated' => false,
            'message' => "Failed to connect to {$resolved_carrier_id}: " . $response->get_error_message(),
            'latency' => $latency
        ];
    };

    // AJAX handler mode
    if ($carrier_id === null) {
        if (!current_user_can('read')) {
            wp_send_json_error(['message' => 'Unauthorized'], 403);
        }
        if (!vmo_verify_admin_security()) {
            wp_send_json_error(['message' => 'Invalid nonce', 'status' => 'invalid_nonce'], 403);
        }

        $carrier_id = sanitize_text_field($_POST['carrier_id'] ?? '');
        if ($carrier_id === '') {
            wp_send_json_error(['message' => 'carrier_id required']);
        }

        wp_send_json_success($resolve_connection($carrier_id, get_current_user_id()));
    }

    // Helper mode (used internally by other endpoints)
    $result = $resolve_connection($carrier_id, get_current_user_id());
    $result['success'] = !empty($result['connected']);
    return $result;
}

// Helper: Test Fulfillment Connection
function vmo_test_fulfillment_connection($fulfillment_id) {
    // Check if the user has configured fulfillment provider credentials
    $user_id = get_current_user_id();
    $shipping_settings = get_user_meta($user_id, 'vmo_shipping_settings', true);
    $provider_config = null;

    if (is_array($shipping_settings) && !empty($shipping_settings['fulfillment_providers'])) {
        foreach ($shipping_settings['fulfillment_providers'] as $p) {
            if (($p['id'] ?? '') === $fulfillment_id && !empty($p['api_key'])) {
                $provider_config = $p;
                break;
            }
        }
    }

    if (!$provider_config) {
        return [
            'success' => false,
            'simulated' => false,
            'message' => "No API credentials configured for fulfillment provider '{$fulfillment_id}'. Please add your API key in Shipping Settings.",
            'inventory_sync' => 'not_configured'
        ];
    }

    // Attempt a real API health-check to the provider's endpoint
    $endpoint = $provider_config['endpoint'] ?? '';
    if (!empty($endpoint)) {
        $response = wp_remote_get($endpoint . '/health', [
            'timeout' => 10,
            'headers' => ['Authorization' => 'Bearer ' . $provider_config['api_key']],
        ]);
        if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) >= 200 && wp_remote_retrieve_response_code($response) < 300) {
            return [
                'success' => true,
                'simulated' => false,
                'message' => "Connected to {$fulfillment_id} successfully",
                'inventory_sync' => $provider_config['sync_enabled'] ?? 'unknown',
                'latency' => round((microtime(true) - ($_SERVER['REQUEST_TIME_FLOAT'] ?? microtime(true))) * 1000) . 'ms',
            ];
        } else {
            $err = is_wp_error($response) ? $response->get_error_message() : 'HTTP ' . wp_remote_retrieve_response_code($response);
            return [
                'success' => false,
                'simulated' => false,
                'message' => "Failed to connect to {$fulfillment_id}: {$err}",
                'inventory_sync' => 'error'
            ];
        }
    }

    return [
        'success' => false,
        'simulated' => false,
        'message' => "No endpoint URL configured for fulfillment provider '{$fulfillment_id}'.",
        'inventory_sync' => 'not_configured'
    ];
}

// Test Shipping Integration
function vmo_test_shipping_integration() {
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if (!function_exists('vmo_verify_admin_security') || !vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Security check failed'], 403);
    }
    
    $user_id = get_current_user_id();
    $shipping_settings = get_user_meta($user_id, 'vmo_shipping_settings', true);
    
    $results = [];
    $contacts = vmo_get_shipping_provider_contacts();

    // Report status (no simulation)
    if ($shipping_settings && isset($shipping_settings['carriers']) && is_array($shipping_settings['carriers'])) {
        foreach ($shipping_settings['carriers'] as $carrier) {
            if (!empty($carrier['enabled'])) {
                $cid = (string) ($carrier['id'] ?? '');
                $url = $contacts['carriers'][$cid]['support_url'] ?? '';
                $results[(string) ($carrier['name'] ?? $cid)] = [
                    'success' => false,
                    'message' => $url ? ('Manual setup required. Contact: ' . $url) : 'Manual setup required. Contact the provider directly.',
                ];
            }
        }
    }

    if ($shipping_settings && isset($shipping_settings['fulfillment']) && is_array($shipping_settings['fulfillment'])) {
        foreach ($shipping_settings['fulfillment'] as $fulfillment) {
            if (!empty($fulfillment['enabled'])) {
                $fid = (string) ($fulfillment['id'] ?? '');
                $url = $contacts['fulfillment'][$fid]['support_url'] ?? '';
                $results[(string) ($fulfillment['name'] ?? $fid)] = [
                    'success' => false,
                    'message' => $url ? ('Manual setup required. Contact: ' . $url) : 'Manual setup required. Contact the provider directly.',
                ];
            }
        }
    }

    wp_send_json_success([
        'results' => $results,
        'message' => 'No automated carrier integrations are enabled. Use the contacts provided to arrange your contracts and shipping workflows.'
    ]);
}


// Create Store Orders Table (VMO Internal Store)
function vmo_create_internal_store_orders_table() {
    global $wpdb;
    $table_name = $wpdb->prefix . 'vmo_internal_store_orders';
    $charset_collate = $wpdb->get_charset_collate();

    $sql = "CREATE TABLE $table_name (
        id mediumint(9) NOT NULL AUTO_INCREMENT,
        user_id bigint(20) NOT NULL,
        customer_name varchar(255) NOT NULL,
        customer_email varchar(255) NOT NULL,
        shipping_address text NOT NULL,
        items longtext NOT NULL,
        items_count int(11) DEFAULT 1,
        total_amount decimal(10,2) NOT NULL,
        currency varchar(3) DEFAULT 'USD',
        status varchar(50) DEFAULT 'pending',
        shipping_status varchar(50) DEFAULT 'pending',
        shipping_approved_at datetime NULL,
        payment_status varchar(50) DEFAULT 'unpaid',
        stripe_payment_intent varchar(255),
        carrier varchar(100),
        tracking_number varchar(100),
        shipped_at datetime NULL,
        created_at datetime DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY  (id),
        KEY user_id (user_id)
    ) $charset_collate;";

    require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
    dbDelta($sql);
}

// Get Pending Shipping Orders (VMO Store Only - No WooCommerce Dependency)
function vmo_get_pending_shipping_orders() {
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    
    global $wpdb;
    $user_id = get_current_user_id();
    $table_name = $wpdb->prefix . 'vmo_internal_store_orders';
    
    // Ensure table exists
    // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
    if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") != $table_name) {
        vmo_create_internal_store_orders_table();
    }
    
    // Fetch orders from VMO internal system
    $results = $wpdb->get_results($wpdb->prepare(
        "SELECT * FROM $table_name WHERE user_id = %d AND status IN ('processing', 'paid') ORDER BY created_at DESC LIMIT 20",
        $user_id
    ));
    
    $orders = [];
    if ($results) {
        foreach ($results as $order) {
            $orders[] = [
                'id' => $order->id,
                'customer' => $order->customer_name,
                'destination' => $order->shipping_address,
                'carrier' => $order->carrier ?: 'Pending carrier selection',
                'rate' => '0.00'
            ];
        }
    }
    
    wp_send_json_success(['orders' => $orders]);
}
add_action('wp_ajax_vmo_get_pending_shipping_orders', 'vmo_get_pending_shipping_orders');

// === VMO Pay (Stripe Express) Integration ===

add_action('wp_ajax_vmo_pay_connect_onboarding', 'vmo_pay_connect_onboarding');
function vmo_pay_connect_onboarding() {
    check_ajax_referer('vmo_nonce', 'nonce');
    if (!current_user_can('manage_options')) wp_send_json_error(['message' => 'Unauthorized']);

    $user_id = get_current_user_id();
    $stripe_secret = get_option('vmo_stripe_secret_key', '');

    // If Stripe API key is configured, create a real Connect Account + Account Link
    if (!empty($stripe_secret) && class_exists('Stripe\\Stripe')) {
        try {
            \Stripe\Stripe::setApiKey($stripe_secret);

            // Check if user already has a Connect account
            $existing_account_id = get_user_meta($user_id, 'vmo_pay_account_id', true);

            if (empty($existing_account_id)) {
                // Create a new Express Connect account
                $account = \Stripe\Account::create([
                    'type' => 'express',
                    'metadata' => [
                        'vmo_user_id' => $user_id,
                        'wp_site' => get_site_url(),
                    ],
                ]);
                $existing_account_id = $account->id;
                update_user_meta($user_id, 'vmo_pay_account_id', $existing_account_id);
            }

            // Create an Account Link for onboarding
            $return_url = admin_url('admin.php?page=vmo-dashboard&vmo_pay=return');
            $refresh_url = admin_url('admin.php?page=vmo-dashboard&vmo_pay=refresh');

            $account_link = \Stripe\AccountLink::create([
                'account' => $existing_account_id,
                'refresh_url' => $refresh_url,
                'return_url' => $return_url,
                'type' => 'account_onboarding',
            ]);

            update_user_meta($user_id, 'vmo_pay_status', 'onboarding');

            wp_send_json_success([
                'url' => $account_link->url,
                'message' => 'Redirecting to Stripe Connect secure onboarding...'
            ]);

        } catch (\Exception $e) {
            if (defined('WP_DEBUG') && WP_DEBUG) error_log('VMO Pay Connect Error: ' . $e->getMessage());
            wp_send_json_error([
                'message' => 'Stripe Connect error: ' . $e->getMessage()
            ]);
        }
    } else {
        // No Stripe key configured — inform the admin
        update_user_meta($user_id, 'vmo_pay_status', 'not_connected');
        wp_send_json_error([
            'message' => 'Stripe API key not configured. Go to VMO Settings → Payments and enter your Stripe Secret Key to enable VMO Pay.'
        ]);
    }
}

add_action('wp_ajax_vmo_pay_get_status', 'vmo_pay_get_status');
function vmo_pay_get_status() {
    check_ajax_referer('vmo_nonce', 'nonce');
    $user_id = get_current_user_id();
    $status = get_user_meta($user_id, 'vmo_pay_status', true) ?: 'not_connected';
    $account_id = get_user_meta($user_id, 'vmo_pay_account_id', true);
    
    wp_send_json_success([
        'status' => $status,
        'account_id' => $account_id ? '****' . substr($account_id, -4) : null
    ]);
}

// === Custom Carrier Management ===

add_action('wp_ajax_vmo_save_custom_carrier', 'vmo_save_custom_carrier');
function vmo_save_custom_carrier() {
    check_ajax_referer('vmo_nonce', 'nonce');
    if (!current_user_can('manage_options')) wp_send_json_error(['message' => 'Unauthorized']);

    $carrier_json = wp_unslash($_POST['carrier'] ?? '');
    $carrier_data = json_decode($carrier_json, true);
    
    if (!$carrier_data || empty($carrier_data['name'])) {
        wp_send_json_error(['message' => 'Invalid carrier data']);
    }

    $carriers = get_option('vmo_custom_carriers', []);
    if (!is_array($carriers)) $carriers = [];
    
    // Generate ID if new
    if (empty($carrier_data['id'])) {
        $carrier_data['id'] = 'custom_' . uniqid();
        $carrier_data['created_at'] = current_time('mysql');
        $carrier_data['enabled'] = true;
    } else {
        // Preserve existing fields if editing
        foreach ($carriers as $existing) {
            if ($existing['id'] === $carrier_data['id']) {
                $carrier_data = array_merge($existing, $carrier_data);
                break;
            }
        }
    }

    // Update or add
    $found = false;
    foreach ($carriers as $key => $c) {
        if ($c['id'] === $carrier_data['id']) {
            $carriers[$key] = $carrier_data;
            $found = true;
            break;
        }
    }
    
    if (!$found) {
        $carriers[] = $carrier_data;
    }

    update_option('vmo_custom_carriers', $carriers);
    wp_send_json_success(['message' => 'Carrier saved', 'carrier' => $carrier_data]);
}

add_action('wp_ajax_vmo_get_custom_carriers', 'vmo_get_custom_carriers');
function vmo_get_custom_carriers() {
    check_ajax_referer('vmo_nonce', 'nonce');
    $carriers = get_option('vmo_custom_carriers', []);
    if (!is_array($carriers)) $carriers = [];
    wp_send_json_success($carriers);
}

add_action('wp_ajax_vmo_test_custom_carrier_api', 'vmo_test_custom_carrier_api');
function vmo_test_custom_carrier_api() {
    check_ajax_referer('vmo_nonce', 'nonce');
    $endpoint = esc_url_raw(wp_unslash($_POST['endpoint'] ?? ''));
    
    if (empty($endpoint)) {
        wp_send_json_error(['message' => 'No endpoint provided']);
    }

    // Basic connectivity check
    $response = wp_remote_get($endpoint, ['timeout' => 10]);
    
    if (is_wp_error($response)) {
        wp_send_json_error(['message' => $response->get_error_message()]);
    }
    
    wp_send_json_success(['message' => 'Connection successful']);
}

add_action('wp_ajax_vmo_toggle_custom_carrier', 'vmo_toggle_custom_carrier');
function vmo_toggle_custom_carrier() {
    check_ajax_referer('vmo_nonce', 'nonce');
    $carrier_id = sanitize_key(wp_unslash($_POST['carrier_id'] ?? ''));
    $enabled = sanitize_text_field(wp_unslash($_POST['enabled'] ?? '')) === 'true';
    
    $carriers = get_option('vmo_custom_carriers', []);
    if (!is_array($carriers)) $carriers = [];

    foreach ($carriers as &$c) {
        if ($c['id'] === $carrier_id) {
            $c['enabled'] = $enabled;
            update_option('vmo_custom_carriers', $carriers);
            wp_send_json_success();
        }
    }
    wp_send_json_error(['message' => 'Carrier not found']);
}

add_action('wp_ajax_vmo_delete_custom_carrier', 'vmo_delete_custom_carrier');
function vmo_delete_custom_carrier() {
    check_ajax_referer('vmo_nonce', 'nonce');
    $carrier_id = sanitize_key(wp_unslash($_POST['carrier_id'] ?? ''));
    
    $carriers = get_option('vmo_custom_carriers', []);
    if (!is_array($carriers)) $carriers = [];

    $new_carriers = [];
    foreach ($carriers as $c) {
        if ($c['id'] !== $carrier_id) {
            $new_carriers[] = $c;
        }
    }
    
    update_option('vmo_custom_carriers', $new_carriers);
    wp_send_json_success();
}

// Get Shipping Analytics
function vmo_get_shipping_analytics() {
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if (!function_exists('vmo_verify_admin_security') || !vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Security check failed'], 403);
    }
    
    global $wpdb;
    $user_id = get_current_user_id();
    
    $analytics = [
        'week_shipped' => 0,
        'avg_fulfillment_time' => '—',
        'ai_savings' => 0,
        'on_time_rate' => 0,
        'cost_breakdown' => []
    ];
    
    // Check for shipping analytics table
    $shipping_table = $wpdb->prefix . 'vmo_shipping_analytics';
    $table_exists = vmo_table_exists($shipping_table);
    
    if (!$table_exists) {
        // Create shipping analytics table
        vmo_create_shipping_analytics_table();
    }
    
    // Get this week's data
    $week_start = date('Y-m-d', strtotime('monday this week'));
    $week_end = date('Y-m-d', strtotime('sunday this week'));
    
    $week_data = $wpdb->get_row($wpdb->prepare("
        SELECT 
            COUNT(*) as shipped_count,
            AVG(fulfillment_hours) as avg_hours,
            SUM(ai_savings) as total_savings,
            AVG(on_time_delivery) as on_time_rate
        FROM {$shipping_table}
        WHERE user_id = %d 
        AND shipped_date BETWEEN %s AND %s
    ", $user_id, $week_start, $week_end));
    
    if ($week_data) {
        $analytics['week_shipped'] = (int) $week_data->shipped_count;
        $analytics['avg_fulfillment_time'] = $week_data->avg_hours ? round($week_data->avg_hours, 1) . ' hours' : '—';
        $analytics['ai_savings'] = round((float) $week_data->total_savings, 2);
        $analytics['on_time_rate'] = round((float) $week_data->on_time_rate * 100, 1);
    }
    
    // Get cost breakdown by carrier
    $cost_breakdown = $wpdb->get_results($wpdb->prepare("
        SELECT 
            carrier,
            SUM(shipping_cost) as total_cost
        FROM {$shipping_table}
        WHERE user_id = %d 
        AND shipped_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
        GROUP BY carrier
    ", $user_id), OBJECT_K);
    
    foreach ($cost_breakdown as $carrier => $data) {
        $analytics['cost_breakdown'][$carrier] = (float) $data->total_cost;
    }
    
    wp_send_json_success($analytics);
}

// Get AI Shipping Recommendations
function vmo_get_shipping_ai_recommendations() {
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if (!function_exists('vmo_verify_admin_security') || !vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Security check failed'], 403);
    }

    $user_id = get_current_user_id();
    global $wpdb;

    // Load saved recommendations from user meta (cache)
    $saved = get_user_meta($user_id, 'vmo_shipping_recommendations', true);
    if (!is_array($saved)) $saved = [];

    // If no saved recommendations, generate a fresh set
    if (empty($saved)) {
        $saved = vmo_build_shipping_recommendations($user_id);
        if (!empty($saved)) {
            update_user_meta($user_id, 'vmo_shipping_recommendations', $saved);
        }
    }

    wp_send_json_success([
        'recommendations' => $saved,
        'generated_at' => get_user_meta($user_id, 'vmo_shipping_recs_generated_at', true) ?: current_time('mysql'),
        'message' => ''
    ]);
}

// Generate New Shipping Recommendations (force re-generate)
function vmo_generate_shipping_recommendations() {
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if (!function_exists('vmo_verify_admin_security') || !vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Security check failed'], 403);
    }

    $user_id = get_current_user_id();
    $recs = vmo_build_shipping_recommendations($user_id);
    update_user_meta($user_id, 'vmo_shipping_recommendations', $recs);
    update_user_meta($user_id, 'vmo_shipping_recs_generated_at', current_time('mysql'));

    wp_send_json_success([
        'recommendations' => $recs,
        'generated_at' => current_time('mysql'),
        'message' => 'Recommendations regenerated based on current order and carrier data.'
    ]);
}

// Approve Shipping Recommendation
function vmo_approve_shipping_recommendation() {
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if (!function_exists('vmo_verify_admin_security') || !vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Security check failed'], 403);
    }

    $rec_id = sanitize_text_field($_POST['recommendation_id'] ?? '');
    if (empty($rec_id)) {
        wp_send_json_error(['message' => 'Recommendation ID required']);
    }

    $user_id = get_current_user_id();
    $recs = get_user_meta($user_id, 'vmo_shipping_recommendations', true);
    if (!is_array($recs)) {
        wp_send_json_error(['message' => 'No recommendations found']);
    }

    $found = false;
    foreach ($recs as &$rec) {
        if (($rec['id'] ?? '') === $rec_id) {
            $rec['status'] = 'approved';
            $rec['approved_at'] = current_time('mysql');
            $found = true;
            break;
        }
    }
    unset($rec);

    if (!$found) {
        wp_send_json_error(['message' => 'Recommendation not found']);
    }

    update_user_meta($user_id, 'vmo_shipping_recommendations', $recs);
    wp_send_json_success(['message' => 'Recommendation approved', 'recommendations' => $recs]);
}

// Apply All Shipping Optimizations (batch-approve all pending)
function vmo_apply_all_shipping_optimizations() {
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if (!function_exists('vmo_verify_admin_security') || !vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Security check failed'], 403);
    }

    $user_id = get_current_user_id();
    $recs = get_user_meta($user_id, 'vmo_shipping_recommendations', true);
    if (!is_array($recs) || empty($recs)) {
        wp_send_json_error(['message' => 'No recommendations to apply']);
    }

    $applied = 0;
    foreach ($recs as &$rec) {
        if (($rec['status'] ?? 'pending') === 'pending') {
            $rec['status'] = 'approved';
            $rec['approved_at'] = current_time('mysql');
            $applied++;
        }
    }
    unset($rec);

    update_user_meta($user_id, 'vmo_shipping_recommendations', $recs);
    wp_send_json_success([
        'message' => "{$applied} optimization(s) approved and applied.",
        'applied_count' => $applied,
        'recommendations' => $recs
    ]);
}

/**
 * Build shipping recommendations by analyzing real order data, carrier settings, and analytics.
 */
function vmo_build_shipping_recommendations($user_id) {
    global $wpdb;
    $recs = [];

    // ── Gather order data ──
    $orders_table = $wpdb->prefix . 'vmo_internal_store_orders';
    $has_orders = vmo_table_exists($orders_table);
    $pending_count = 0;
    $total_order_value = 0;
    $distinct_destinations = 0;
    $orders_without_carrier = 0;
    if ($has_orders) {
        $pending_count = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$orders_table} WHERE user_id = %d AND shipping_status = 'pending'", $user_id
        ));
        $total_order_value = (float) $wpdb->get_var($wpdb->prepare(
            "SELECT COALESCE(SUM(total_amount), 0) FROM {$orders_table} WHERE user_id = %d AND status IN ('processing','paid')", $user_id
        ));
        $distinct_destinations = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(DISTINCT shipping_address) FROM {$orders_table} WHERE user_id = %d AND status IN ('processing','paid') AND shipping_status = 'pending'", $user_id
        ));
        $orders_without_carrier = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$orders_table} WHERE user_id = %d AND shipping_status = 'pending' AND (carrier IS NULL OR carrier = '')", $user_id
        ));
    }

    // ── Gather analytics data ──
    $analytics_table = $wpdb->prefix . 'vmo_shipping_analytics';
    $has_analytics = vmo_table_exists($analytics_table);
    $avg_cost = 0;
    $avg_fulfillment_hrs = 0;
    $on_time_pct = 0;
    $top_carrier = '';
    if ($has_analytics) {
        $stats = $wpdb->get_row($wpdb->prepare(
            "SELECT AVG(shipping_cost) as avg_cost, AVG(fulfillment_hours) as avg_hrs, AVG(on_time_delivery)*100 as on_time
             FROM {$analytics_table} WHERE user_id = %d AND shipped_date >= DATE_SUB(CURDATE(), INTERVAL 90 DAY)", $user_id
        ));
        if ($stats) {
            $avg_cost = round((float) $stats->avg_cost, 2);
            $avg_fulfillment_hrs = round((float) $stats->avg_hrs, 1);
            $on_time_pct = round((float) $stats->on_time, 1);
        }
        $top_carrier_row = $wpdb->get_var($wpdb->prepare(
            "SELECT carrier FROM {$analytics_table} WHERE user_id = %d GROUP BY carrier ORDER BY COUNT(*) DESC LIMIT 1", $user_id
        ));
        $top_carrier = $top_carrier_row ?: '';
    }

    // ── Gather carrier settings ──
    $shipping_settings = get_user_meta($user_id, 'vmo_shipping_settings', true);
    $enabled_carriers = [];
    if (is_array($shipping_settings) && isset($shipping_settings['carriers'])) {
        foreach ($shipping_settings['carriers'] as $c) {
            if (!empty($c['enabled'])) $enabled_carriers[] = $c['name'] ?? $c['id'] ?? '';
        }
    }

    // ── Build recommendations based on data patterns ──
    $rec_id = 1;

    // 1. Pending orders need attention
    if ($pending_count > 0) {
        $recs[] = [
            'id' => 'ship_rec_' . $rec_id++,
            'type' => 'fulfillment',
            'priority' => $pending_count > 5 ? 'high' : 'medium',
            'title' => "Process {$pending_count} pending shipment(s)",
            'description' => "You have {$pending_count} order(s) awaiting shipment" . ($orders_without_carrier > 0 ? " ({$orders_without_carrier} without a carrier assigned)" : '') . '. Review and approve them to maintain delivery promises.',
            'estimated_savings' => 0,
            'status' => 'pending',
        ];
    }

    // 2. No carriers configured
    if (empty($enabled_carriers)) {
        $recs[] = [
            'id' => 'ship_rec_' . $rec_id++,
            'type' => 'setup',
            'priority' => 'high',
            'title' => 'Configure shipping carriers',
            'description' => 'No shipping carriers are enabled. Go to Shipping Settings and enable at least one carrier (FedEx, UPS, DHL, USPS, etc.) to start processing shipments.',
            'estimated_savings' => 0,
            'status' => 'pending',
        ];
    }

    // 3. Carrier diversification
    if (count($enabled_carriers) === 1 && $has_analytics && $avg_cost > 0) {
        $recs[] = [
            'id' => 'ship_rec_' . $rec_id++,
            'type' => 'cost_optimization',
            'priority' => 'medium',
            'title' => 'Diversify shipping carriers for better rates',
            'description' => "You're only using {$enabled_carriers[0]}. Adding a second carrier could save 10-20% on shipping costs. Your current average cost is \${$avg_cost} per shipment.",
            'estimated_savings' => round($avg_cost * 0.15 * max(1, $pending_count), 2),
            'status' => 'pending',
        ];
    }

    // 4. Fulfillment speed
    if ($avg_fulfillment_hrs > 48) {
        $recs[] = [
            'id' => 'ship_rec_' . $rec_id++,
            'type' => 'speed',
            'priority' => 'medium',
            'title' => 'Reduce fulfillment time',
            'description' => "Your average fulfillment time is {$avg_fulfillment_hrs} hours. Industry standard is under 24 hours. Consider batch-processing orders daily or exploring fulfillment partners (ShipBob, Easyship).",
            'estimated_savings' => 0,
            'status' => 'pending',
        ];
    }

    // 5. On-time delivery
    if ($has_analytics && $on_time_pct > 0 && $on_time_pct < 90) {
        $recs[] = [
            'id' => 'ship_rec_' . $rec_id++,
            'type' => 'reliability',
            'priority' => 'high',
            'title' => 'Improve on-time delivery rate',
            'description' => "Your on-time delivery rate is {$on_time_pct}% (target: 95%+). Consider upgrading shipping service tiers or switching carriers for underperforming routes.",
            'estimated_savings' => 0,
            'status' => 'pending',
        ];
    }

    // 6. Multiple destinations — consolidation
    if ($distinct_destinations > 3) {
        $recs[] = [
            'id' => 'ship_rec_' . $rec_id++,
            'type' => 'cost_optimization',
            'priority' => 'low',
            'title' => 'Consider regional fulfillment',
            'description' => "You ship to {$distinct_destinations} distinct destinations. Regional fulfillment centers or a multi-warehouse provider like ShipBob could cut transit times and costs.",
            'estimated_savings' => round($total_order_value * 0.02, 2),
            'status' => 'pending',
        ];
    }

    // 7. High-value orders need insurance
    if ($total_order_value > 500) {
        $recs[] = [
            'id' => 'ship_rec_' . $rec_id++,
            'type' => 'risk',
            'priority' => 'low',
            'title' => 'Add shipping insurance for high-value orders',
            'description' => 'You have $' . number_format($total_order_value, 2) . ' in pending order value. Shipping insurance typically costs 1-3% and protects against loss or damage.',
            'estimated_savings' => 0,
            'status' => 'pending',
        ];
    }

    // If nothing to recommend, give a positive status message
    if (empty($recs) && $has_orders) {
        $recs[] = [
            'id' => 'ship_rec_' . $rec_id++,
            'type' => 'status',
            'priority' => 'low',
            'title' => 'Shipping operations look good',
            'description' => 'No immediate optimizations needed. Your shipping workflow is running smoothly. Check back after processing more orders for data-driven suggestions.',
            'estimated_savings' => 0,
            'status' => 'info',
        ];
    }

    return $recs;
}

/* --------------------------------------------------------------
   SHIPPING HELPER FUNCTIONS
---------------------------------------------------------------- */

// Create shipping task for AI processing
function vmo_create_shipping_task($user_id, $order_id, $order) {
    global $wpdb;
    
    $tasks_table = $wpdb->prefix . 'vmo_ai_tasks';
    
    // Create AI tasks table if it doesn't exist
    $table_exists = vmo_table_exists($tasks_table);
    if (!$table_exists) {
        vmo_create_ai_tasks_table();
    }
    
    // Insert shipping task
    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
    $wpdb->insert(
        $tasks_table,
        [
            'user_id' => $user_id,
            'task_type' => 'shipping',
            'task_data' => json_encode([
                'order_id' => $order_id,
                'customer_name' => $order->customer_name,
                'customer_email' => $order->customer_email,
                'amount' => $order->amount,
                'shipping_address' => $order->shipping_address ?? ''
            ]),
            'status' => 'pending',
            'priority' => 'high',
            'created_at' => current_time('mysql')
        ],
        ['%d', '%s', '%s', '%s', '%s', '%s']
    );
}

// Create AI tasks table
function vmo_create_ai_tasks_table() {
    global $wpdb;
    
    $table_name = $wpdb->prefix . 'vmo_ai_tasks';
    
    $charset_collate = $wpdb->get_charset_collate();
    
    $sql = "CREATE TABLE $table_name (
        id mediumint(9) NOT NULL AUTO_INCREMENT,
        user_id bigint(20) NOT NULL,
        task_type varchar(50) NOT NULL,
        task_data longtext NOT NULL,
        status varchar(20) DEFAULT 'pending',
        priority varchar(20) DEFAULT 'medium',
        results longtext NULL,
        created_at datetime DEFAULT CURRENT_TIMESTAMP,
        updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        completed_at datetime NULL,
        PRIMARY KEY (id),
        KEY user_id (user_id),
        KEY task_type (task_type),
        KEY status (status)
    ) $charset_collate;";
    
    require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
    dbDelta($sql);
}

// Create shipping analytics table
function vmo_create_shipping_analytics_table() {
    global $wpdb;
    
    $table_name = $wpdb->prefix . 'vmo_shipping_analytics';
    
    $charset_collate = $wpdb->get_charset_collate();
    
    $sql = "CREATE TABLE $table_name (
        id mediumint(9) NOT NULL AUTO_INCREMENT,
        user_id bigint(20) NOT NULL,
        order_id varchar(100) NOT NULL,
        carrier varchar(50) NOT NULL,
        service_type varchar(100) NOT NULL,
        shipping_cost decimal(10,2) NOT NULL,
        fulfillment_hours decimal(5,2) NOT NULL,
        on_time_delivery tinyint(1) DEFAULT 0,
        ai_savings decimal(10,2) DEFAULT 0,
        tracking_number varchar(255) NULL,
        shipped_date datetime NOT NULL,
        delivered_date datetime NULL,
        created_at datetime DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY (id),
        KEY user_id (user_id),
        KEY carrier (carrier),
        KEY shipped_date (shipped_date)
    ) $charset_collate;";
    
    require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
    dbDelta($sql);
}

// Generate shipping recommendations based on user profile and patterns
function vmo_generate_ai_shipping_recommendations($user_id, $consultation_profile) {
    return vmo_build_shipping_recommendations($user_id);
}

// Apply shipping optimization
function vmo_apply_shipping_optimization($user_id, $recommendation_id) {
    return [
        'success' => false,
        'error' => 'Shipping optimizations are disabled until real carrier/fulfillment integrations are implemented.',
        'savings' => 0,
    ];
}

// ==========================================
// EMAIL TOKEN SYSTEM
// ==========================================

/**
 * Send email with token deduction
 * 
 * @param string $to Recipient email
 * @param string $subject Email subject
 * @param string $message Email body
 * @param array|string $headers Optional headers
 * @param array $attachments Optional attachments
 * @return bool True if sent, false otherwise
 */
function vmo_send_email_with_tokens($to, $subject, $message, $headers = '', $attachments = array()) {
    $user_id = get_current_user_id();
    if (!$user_id) {
        // Fallback for system emails or non-logged in users (optional: charge admin or skip)
        return wp_mail($to, $subject, $message, $headers, $attachments);
    }

    // Check token balance
    $balance = vmo_get_token_balance($user_id);
    $cost = defined('VMO_TOKENS_EMAIL') ? VMO_TOKENS_EMAIL : 50;

    if ($balance < $cost) {
        if (defined('WP_DEBUG') && WP_DEBUG) error_log("VMO Email Error: Insufficient tokens for user $user_id. Required: $cost, Available: $balance");
        return false;
    }

    // Deduct tokens
    if (vmo_deduct_tokens($user_id, $cost, 'email_sent', "Email to $to: $subject")) {
        return wp_mail($to, $subject, $message, $headers, $attachments);
    }

    return false;
}

// ==========================================
// SHIPPING AUTOMATION
// ==========================================

/**
 * Auto-configure shipping based on business type
 * 
 * @param array $business_data
 * @return array Configuration result
 */
function vmo_auto_configure_shipping($business_data) {
    $user_id = get_current_user_id();
    if (!$user_id) return ['success' => false, 'message' => 'No user logged in'];

    $type = strtolower($business_data['business_type'] ?? '');
    $industry = strtolower($business_data['industry'] ?? '');
    
    // Determine needs
    $needs_shipping = false;
    $carriers = [];
    
    if (strpos($type, 'retail') !== false || strpos($type, 'ecommerce') !== false || strpos($type, 'product') !== false || strpos($type, 'store') !== false) {
        $needs_shipping = true;
        $carriers = ['usps' => true, 'ups' => true]; // Standard for retail
    } elseif (strpos($type, 'service') !== false && strpos($industry, 'legal') !== false) {
        $needs_shipping = true;
        $carriers = ['fedex' => true]; // Documents often go FedEx
    }

    if ($needs_shipping) {
        $settings = [
            'carriers' => [],
            'fulfillment' => [],
            'updated_at' => current_time('mysql'),
            'auto_configured' => true
        ];

        $all_carriers = ['fedex' => 'FedEx', 'ups' => 'UPS', 'usps' => 'USPS', 'dhl' => 'DHL'];
        foreach ($all_carriers as $id => $name) {
            $settings['carriers'][] = [
                'id' => $id,
                'name' => $name,
                'enabled' => isset($carriers[$id])
            ];
        }
        
        update_user_meta($user_id, 'vmo_shipping_settings', $settings);
        update_user_meta($user_id, 'vmo_shipping_integration_active', false);
        
        return [
            'success' => true, 
            'message' => 'Shipping profile configured based on the business profile. Live carrier integration stays inactive until provider credentials and endpoints are added.',
            'carriers' => array_keys($carriers),
            'integration_active' => false,
        ];
    }

    return ['success' => true, 'message' => 'Shipping configuration not required for this business type.'];
}

// === Global Shipping Configuration ===

add_action('wp_ajax_vmo_save_global_shipping', 'vmo_save_global_shipping');
function vmo_save_global_shipping() {
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if (!vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Invalid nonce', 'status' => 'invalid_nonce'], 403);
    }

    $user_id = get_current_user_id();
    $settings_json = isset($_POST['settings']) ? stripslashes((string) $_POST['settings']) : '';
    $settings = json_decode($settings_json, true);
    
    if (!$settings) {
        wp_send_json_error(['message' => 'Invalid settings data']);
    }

    // Support lightweight updates from region toggles: {carrier_id, enabled}
    if (isset($settings['carrier_id'])) {
        $carrier_id = sanitize_text_field((string) $settings['carrier_id']);
        $enabled = !empty($settings['enabled']);
        $existing = get_user_meta($user_id, 'vmo_global_shipping_settings', true);
        if (!is_array($existing)) {
            $existing = ['carriers' => [], 'trade_settings' => []];
        }
        if (!isset($existing['carriers']) || !is_array($existing['carriers'])) {
            $existing['carriers'] = [];
        }
        $existing['carriers'][$carrier_id] = $enabled;
        update_user_meta($user_id, 'vmo_global_shipping_settings', $existing);
        wp_send_json_success(['message' => 'Global settings saved', 'settings' => $existing]);
    }

    update_user_meta($user_id, 'vmo_global_shipping_settings', $settings);
    wp_send_json_success(['message' => 'Global settings saved', 'settings' => $settings]);
}

add_action('wp_ajax_vmo_load_global_shipping', 'vmo_load_global_shipping');
function vmo_load_global_shipping() {
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if (!vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Invalid nonce', 'status' => 'invalid_nonce'], 403);
    }
    $user_id = get_current_user_id();
    $settings = get_user_meta($user_id, 'vmo_global_shipping_settings', true);
    if (!is_array($settings)) $settings = [];
    wp_send_json_success($settings);
}

add_action('wp_ajax_vmo_test_global_carriers', 'vmo_test_global_carriers');
function vmo_test_global_carriers() {
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }
    if (!vmo_verify_admin_security()) {
        wp_send_json_error(['message' => 'Invalid nonce', 'status' => 'invalid_nonce'], 403);
    }
    $user_id = get_current_user_id();

    $settings = get_user_meta($user_id, 'vmo_global_shipping_settings', true);
    $carriers = $settings['carriers'] ?? [];

    $results = [];
    $regions = [
        'asia-sf-express' => 'Asia Pacific (SF Express)',
        'asia-japan-post' => 'Japan (Japan Post)',
        'asia-australia-post' => 'Oceania (Australia Post)',
        'africa-aramex' => 'Middle East (Aramex)',
        'africa-fastway' => 'Africa (Fastway)',
        'sa-correios' => 'Brazil (Correios)',
        'sa-mercado' => 'South America (MercadoEnvíos)'
    ];

    foreach ($regions as $key => $label) {
        if (!empty($carriers[$key])) {
            $results[$label] = [
                'status' => 'configured',
                'connected' => false,
                'latency' => '',
                'coverage' => 'Configured',
                'message' => 'Configured in global routing. Live carrier API testing is not available for this regional carrier.'
            ];
        } else {
            $results[$label] = [
                'status' => 'inactive',
                'connected' => false,
                'message' => 'Not configured'
            ];
        }
    }
    
    wp_send_json_success($results);
}

// Import Leads from CSV
add_action('wp_ajax_vmo_import_leads_csv', 'vmo_import_leads_csv');
function vmo_import_leads_csv() {
    vmo_require_crm_ajax_security();
    if (!function_exists('vmo_crm_upsert_contact')) {
        wp_send_json_error(['message' => 'CRM helper unavailable']);
    }
    
    $leads = map_deep(wp_unslash($_POST['leads'] ?? []), 'sanitize_text_field');
    if (empty($leads) || !is_array($leads)) {
        wp_send_json_error(['message' => 'No leads data provided']);
    }
    
    $user_id = get_current_user_id();
    $imported = 0;
    $updated = 0;
    $created = 0;
    
    foreach ($leads as $lead) {
        if (!is_array($lead)) {
            continue;
        }

        $email = sanitize_email($lead['email'] ?? '');
        if (!$email) {
            continue;
        }

        $full_name = sanitize_text_field($lead['name'] ?? '');
        $company = sanitize_text_field($lead['company'] ?? '');
        $phone = sanitize_text_field($lead['phone'] ?? '');

        if ($full_name === '' && $company !== '') {
            $full_name = $company;
        }

        $existing = vmo_crm_find_contact($user_id, ['email' => $email, 'phone' => $phone]);
        $contact = vmo_crm_upsert_contact($user_id, [
            'email' => $email,
            'phone' => $phone,
            'full_name' => $full_name,
            'company' => $company,
        ], [
            'source' => 'manual_entry',
            'primary_category' => 'lead',
            'lifecycle_stage' => 'new',
            'lead_score' => 0,
            'engagement_level' => 'medium',
            'value_potential' => 50,
            'acquisition_details' => ['source' => 'legacy_leads_csv_import'],
        ]);
        if (!empty($contact['contact_id'])) {
            if ($existing) {
                $updated++;
            } else {
                $created++;
            }
            $imported++;
        }
    }
    
    wp_send_json_success(['message' => "Imported $imported leads", 'count' => $imported, 'created' => $created, 'updated' => $updated]);
}

// Retrieve User Business Phone for Calling
add_action('wp_ajax_vmo_get_user_business_phone', 'vmo_get_user_business_phone_callback');
function vmo_get_user_business_phone_callback() {
    // Basic security check
    if (!is_user_logged_in()) {
        wp_send_json_error(['message' => 'Unauthorized']);
    }
    
    $user_id = get_current_user_id();
    global $wpdb;
    $phone = '';

    if (class_exists('VMO_Twilio_Manager')) {
        try {
            $phone = (string) (VMO_Twilio_Manager::get_instance()->get_user_phone($user_id) ?: '');
        } catch (Throwable $e) {
            $phone = '';
        }
    }

    // 1. Try dedicated tables if the manager lookup did not return a value
    if (empty($phone)) {
        foreach (array(
            $wpdb->prefix . 'vmo_phone_numbers',
            $wpdb->prefix . 'vmo_user_phone_numbers',
        ) as $table) {
            // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
            if ($wpdb->get_var("SHOW TABLES LIKE '{$table}'") !== $table) {
                continue;
            }

            $phone = (string) $wpdb->get_var($wpdb->prepare(
                "SELECT phone_number FROM {$table} WHERE user_id = %d ORDER BY id DESC LIMIT 1",
                $user_id
            ));
            if (!empty($phone)) {
                break;
            }
        }
    }

    // 2. Try User Meta
    if (empty($phone)) {
        $phone = get_user_meta($user_id, 'vmo_phone_number', true);
    }
    if (empty($phone)) {
        $phone = get_user_meta($user_id, 'vmo_twilio_phone_number', true);
    }
    if (empty($phone)) {
        $phone = get_user_meta($user_id, 'vmo_twilio_number', true);
    }
    if (empty($phone)) {
        $phone = get_user_meta($user_id, 'vmo_business_phone', true);
    }

    if (!empty($phone)) {
        wp_send_json_success(['phone_number' => $phone]);
    } else {
        wp_send_json_error(['message' => 'No number found']);
    }
}

// ── Site Import: Fetch & extract content from an existing website ──
// ── Guardrail G4: Requires Starter plan or above (not Tester/free) ──
add_action('wp_ajax_vmo_import_site', 'vmo_import_site');
function vmo_import_site() {
    check_ajax_referer('vmo_nonce', 'nonce');
    if (!current_user_can('read')) {
        wp_send_json_error(['message' => 'Unauthorized'], 403);
    }

    // ── G4: Plan gating — Tester and free users cannot import/migrate sites ──
    $user_id = get_current_user_id();
    $plan = strtolower((string) get_user_meta($user_id, 'vmo_subscription_status', true));
    $allowed_plans = ['starter', 'pro', 'elite', 'enterprise', 'premium', 'business'];
    $is_admin = user_can($user_id, 'manage_options');

    if (!$is_admin && !in_array($plan, $allowed_plans, true)) {
        wp_send_json_error([
            'message' => 'Site import requires a Starter plan or above. Upgrade your plan to use this feature.',
            'upgrade_required' => true,
        ]);
    }

    $url = esc_url_raw($_POST['url'] ?? '');
    if (empty($url) || !filter_var($url, FILTER_VALIDATE_URL)) {
        wp_send_json_error(['message' => 'Invalid URL provided']);
    }

    // Fetch the page
    $response = wp_remote_get($url, [
        'timeout' => 15,
        'user-agent' => 'VMO-SiteImporter/1.0 (WordPress; +https://getvmo.com)',
        'sslverify' => true,
    ]);

    if (is_wp_error($response)) {
        wp_send_json_error(['message' => 'Could not reach the website: ' . $response->get_error_message()]);
    }

    $status = wp_remote_retrieve_response_code($response);
    if ($status < 200 || $status >= 400) {
        wp_send_json_error(['message' => "Website returned HTTP {$status}"]);
    }

    $html = wp_remote_retrieve_body($response);
    if (empty($html)) {
        wp_send_json_error(['message' => 'Website returned empty content']);
    }

    // Suppress libxml errors
    libxml_use_internal_errors(true);
    $dom = new \DOMDocument();
    $dom->loadHTML('<?xml encoding="UTF-8">' . $html, LIBXML_NOWARNING | LIBXML_NOERROR);
    libxml_clear_errors();

    $xpath = new \DOMXPath($dom);

    // Extract title
    $title_nodes = $dom->getElementsByTagName('title');
    $title = $title_nodes->length > 0 ? trim($title_nodes->item(0)->textContent) : '';

    // Extract meta description
    $meta_desc = '';
    $meta_nodes = $xpath->query('//meta[@name="description"]');
    if ($meta_nodes->length > 0) {
        $meta_desc = $meta_nodes->item(0)->getAttribute('content');
    }

    // Extract OG data
    $og_image = '';
    $og_nodes = $xpath->query('//meta[@property="og:image"]');
    if ($og_nodes->length > 0) {
        $og_image = $og_nodes->item(0)->getAttribute('content');
    }

    // Extract headings (h1-h3)
    $headings = [];
    foreach (['h1', 'h2', 'h3'] as $tag) {
        $nodes = $dom->getElementsByTagName($tag);
        for ($i = 0; $i < min($nodes->length, 10); $i++) {
            $text = trim($nodes->item($i)->textContent);
            if (!empty($text)) {
                $headings[] = ['level' => $tag, 'text' => mb_substr($text, 0, 200)];
            }
        }
    }

    // Extract main text content (strip scripts/styles, get body text)
    $body_nodes = $dom->getElementsByTagName('body');
    $body_text = '';
    if ($body_nodes->length > 0) {
        // Remove script and style elements
        $scripts = $xpath->query('//script | //style | //noscript');
        foreach ($scripts as $script) {
            $script->parentNode->removeChild($script);
        }
        $body_text = trim($body_nodes->item(0)->textContent);
        // Collapse whitespace
        $body_text = preg_replace('/\s+/', ' ', $body_text);
        // Limit to 3000 chars for context
        $body_text = mb_substr($body_text, 0, 3000);
    }

    // Extract images (src + alt)
    $images = [];
    $img_nodes = $dom->getElementsByTagName('img');
    for ($i = 0; $i < min($img_nodes->length, 20); $i++) {
        $src = $img_nodes->item($i)->getAttribute('src');
        $alt = $img_nodes->item($i)->getAttribute('alt');
        if (!empty($src)) {
            // Make relative URLs absolute
            if (strpos($src, '//') === false && strpos($src, 'http') !== 0) {
                $parsed = parse_url($url);
                $base = $parsed['scheme'] . '://' . $parsed['host'];
                $src = $base . '/' . ltrim($src, '/');
            }
            $images[] = ['src' => $src, 'alt' => $alt];
        }
    }

    // Extract navigation links
    $nav_links = [];
    $nav_nodes = $xpath->query('//nav//a | //header//a');
    for ($i = 0; $i < min($nav_nodes->length, 15); $i++) {
        $link_text = trim($nav_nodes->item($i)->textContent);
        $link_href = $nav_nodes->item($i)->getAttribute('href');
        if (!empty($link_text) && !empty($link_href)) {
            $nav_links[] = ['text' => mb_substr($link_text, 0, 100), 'href' => $link_href];
        }
    }

    // Detect technologies / CMS hints
    $technologies = [];
    if (stripos($html, 'wp-content') !== false) $technologies[] = 'WordPress';
    if (stripos($html, 'shopify') !== false) $technologies[] = 'Shopify';
    if (stripos($html, 'wix.com') !== false) $technologies[] = 'Wix';
    if (stripos($html, 'squarespace') !== false) $technologies[] = 'Squarespace';
    if (stripos($html, 'bootstrap') !== false) $technologies[] = 'Bootstrap';
    if (stripos($html, 'jquery') !== false) $technologies[] = 'jQuery';

    // Store extracted data in user meta for later use
    $user_id = get_current_user_id();
    $import_data = [
        'url' => $url,
        'title' => $title,
        'meta_description' => $meta_desc,
        'og_image' => $og_image,
        'headings' => $headings,
        'body_text' => $body_text,
        'images' => array_slice($images, 0, 10),
        'nav_links' => $nav_links,
        'technologies' => $technologies,
        'imported_at' => current_time('mysql'),
    ];
    update_user_meta($user_id, 'vmo_imported_site_data', $import_data);

    wp_send_json_success([
        'title' => $title,
        'meta_description' => $meta_desc,
        'headings_count' => count($headings),
        'headings' => array_slice($headings, 0, 5),
        'images_count' => count($images),
        'nav_links' => $nav_links,
        'technologies' => $technologies,
        'content_length' => strlen($body_text),
        'message' => "Successfully imported content from {$url}",
        // ── G4: Disclosure notice ──
        'disclosure' => 'This import crawls publicly visible content (text, headings, images, navigation) from the specified URL. No login credentials, databases, or private data are accessed. The imported content is used as a reference to help generate your new site — it is not copied verbatim.',
    ]);
}


// ╔══════════════════════════════════════════════════════════════════════════╗
// ║                     SHORTCODES                                         ║
// ║  [vmo_consultation]  – Embeds the AI consultation wizard (frontend)    ║
// ║  [vmo_business_dashboard] – Embeds the strategic dashboard             ║
// ║  [vmo_pricing_table] – Renders the 5-tier pricing table                ║
// ╚══════════════════════════════════════════════════════════════════════════╝

// ─── [vmo_consultation] ───────────────────────────────────────────────────
// Renders the full AI Business Consultation wizard UI on any page/post.
// Requires a logged-in user with an active plan.
add_shortcode('vmo_consultation', function($atts) {
    if (!is_user_logged_in()) {
        return '<div class="vmo-login-required" style="text-align:center;padding:40px 20px;background:#0d1117;color:#c9d1d9;border-radius:12px;margin:20px 0;">'
             . '<h3 style="color:#58a6ff;">Sign In Required</h3>'
             . '<p>Please <a href="' . esc_url(wp_login_url(get_permalink())) . '" style="color:#58a6ff;">log in</a> to access the AI Business Consultation.</p>'
             . '</div>';
    }

    $user_id = get_current_user_id();
    $tokens  = (int) get_user_meta($user_id, 'vmo_token_balance', true);
    $consultation_unmetered = function_exists('vmo_realtime_is_unmetered_master')
        ? vmo_realtime_is_unmetered_master($user_id)
        : ((defined('VMO_MASTER_VERSION') && VMO_MASTER_VERSION === true) && !(defined('VMO_CLIENT_MODE') && VMO_CLIENT_MODE === true));
    if ($tokens <= 0 && !$consultation_unmetered) {
        return '<div class="vmo-no-tokens" style="text-align:center;padding:40px 20px;background:#0d1117;color:#c9d1d9;border-radius:12px;margin:20px 0;">'
             . '<h3 style="color:#f0883e;">Active Plan Required</h3>'
             . '<p>You need an active VMO plan to start a consultation. <a href="' . esc_url(home_url('/pricing/')) . '" style="color:#58a6ff;">View Plans</a></p>'
             . '</div>';
    }

    // Enqueue consultation assets. Keep the audio stack in dependency order so
    // Jerreme can use Premium Live Voice where available and stable TTS fallback
    // everywhere else.
    $base = VMO_AI_BUILDER_URL . 'assets/';
    $dir  = VMO_AI_BUILDER_DIR . 'assets/';
    $builder_nonce = wp_create_nonce('vmo_builder_nonce');
    $rest_nonce = wp_create_nonce('wp_rest');
    $is_client_mode = ((defined('VMO_CLIENT_MODE') && VMO_CLIENT_MODE === true) || get_option('vmo_quicklaunch_hosted_client') === '1');
    $current_user = wp_get_current_user();

    wp_enqueue_style('vmo-chat-formatting',  $base . 'vmo-chat-formatting.css', [], filemtime($dir . 'vmo-chat-formatting.css'));
    wp_enqueue_style('vmo-media-integration', $base . 'vmo-media-integration.css', [], filemtime($dir . 'vmo-media-integration.css'));

    wp_enqueue_script('vmo-audio-coordinator', $base . 'vmo-audio-coordinator.js', [], filemtime($dir . 'vmo-audio-coordinator.js'), true);
    wp_enqueue_script('vmo-tts-hd', $base . 'vmo-tts-hd.js', ['vmo-audio-coordinator'], filemtime($dir . 'vmo-tts-hd.js'), true);
    wp_localize_script('vmo-tts-hd', 'wpApiSettings', [
        'root' => esc_url_raw(rest_url()),
        'nonce' => $rest_nonce,
    ]);
    wp_localize_script('vmo-tts-hd', 'vmoClientProxy', vmo_client_direct_master_voice_proxy_config());

    wp_enqueue_script('vmo-realtime', $base . 'vmo-realtime.js', ['vmo-audio-coordinator'], filemtime($dir . 'vmo-realtime.js'), true);
    wp_localize_script('vmo-realtime', 'vmo_ajax', [
        'ajaxurl' => admin_url('admin-ajax.php'),
        'nonce'   => $builder_nonce,
        'security'=> $builder_nonce,
        'is_client_mode' => $is_client_mode,
        'user_display_name' => $current_user && $current_user->exists() ? (function_exists('vmo_get_greeting_display_name') ? vmo_get_greeting_display_name() : $current_user->display_name) : '',
    ]);
    wp_localize_script('vmo-realtime', 'vmoClientProxy', vmo_client_direct_master_voice_proxy_config());

    wp_enqueue_script('vmo-conversational',   $base . 'vmo-conversational.js',   [], filemtime($dir . 'vmo-conversational.js'), true);
    wp_enqueue_script(
        'vmo-intelligent-consultation',
        $base . 'vmo-intelligent-consultation.js',
        ['jquery', 'vmo-conversational', 'vmo-audio-coordinator', 'vmo-tts-hd', 'vmo-realtime'],
        filemtime($dir . 'vmo-intelligent-consultation.js'),
        true
    );
    wp_enqueue_script('vmo-media-integration', $base . 'vmo-media-integration.js', ['jquery', 'vmo-intelligent-consultation'], filemtime($dir . 'vmo-media-integration.js'), true);

    $ajax_payload = [
        'ajaxurl' => admin_url('admin-ajax.php'),
        'ajax_url' => admin_url('admin-ajax.php'),
        'nonce' => $builder_nonce,
        'security' => $builder_nonce,
        'pluginUrl' => VMO_AI_BUILDER_URL,
        'is_client_mode' => $is_client_mode,
        'user_display_name' => $current_user && $current_user->exists() ? (function_exists('vmo_get_greeting_display_name') ? vmo_get_greeting_display_name() : $current_user->display_name) : '',
    ];
    wp_localize_script('vmo-intelligent-consultation', 'vmoAjax', $ajax_payload);
    wp_localize_script('vmo-intelligent-consultation', 'vmo_ajax', $ajax_payload);

    wp_localize_script('vmo-intelligent-consultation', 'vmo_consultation_data', [
        'ajaxurl'      => admin_url('admin-ajax.php'),
        'nonce'        => wp_create_nonce('vmo_nonce'),
        'security'     => $builder_nonce,
        'user_id'      => $user_id,
        'token_balance' => $tokens,
        'rest_url'     => rest_url('vmo/v1/'),
        'is_frontend'  => true,
        'is_client_mode' => $is_client_mode,
    ]);

    ob_start();
    ?>
    <div id="vmo-consultation-root" class="vmo-consultation-embed" data-user-id="<?php echo esc_attr($user_id); ?>">
        <div id="vmo-chat-container" class="vmo-consultation-chat"
             style="min-height:600px;background:#0d1117;border-radius:16px;overflow:hidden;border:1px solid #30363d;">
            <div id="vmo-chat-messages" class="vmo-messages-area" style="padding:24px;min-height:500px;overflow-y:auto;"></div>
            <div id="vmo-chat-input-area" class="vmo-input-area"
                 style="padding:16px 24px;border-top:1px solid #30363d;display:flex;gap:12px;align-items:center;">
                <input type="text" id="vmo-chat-input" placeholder="Describe your business vision..."
                       style="flex:1;padding:12px 16px;border-radius:12px;border:1px solid #30363d;background:#161b22;color:#c9d1d9;font-size:15px;"
                       autocomplete="off" />
                <button id="vmo-chat-send" class="vmo-send-btn"
                        style="padding:12px 24px;border-radius:12px;background:linear-gradient(135deg,#7c3aed,#2563eb);color:#fff;border:none;cursor:pointer;font-weight:600;">
                    Send
                </button>
            </div>
        </div>
    </div>
    <?php
    return ob_get_clean();
});

// ─── [vmo_business_dashboard] ─────────────────────────────────────────────
// Renders the strategic business dashboard for logged-in users.
add_shortcode('vmo_business_dashboard', function($atts) {
    if (!is_user_logged_in()) {
        return '<div class="vmo-login-required" style="text-align:center;padding:40px 20px;background:#0d1117;color:#c9d1d9;border-radius:12px;margin:20px 0;">'
             . '<h3 style="color:#58a6ff;">Sign In Required</h3>'
             . '<p>Please <a href="' . esc_url(wp_login_url(get_permalink())) . '" style="color:#58a6ff;">log in</a> to view your dashboard.</p>'
             . '</div>';
    }

    $user_id = get_current_user_id();

    // Enqueue dashboard script
    $js_file = VMO_AI_BUILDER_DIR . 'assets/vmo-business-dashboard.js';
    if (file_exists($js_file)) {
        wp_enqueue_script(
            'vmo-business-dashboard',
            VMO_AI_BUILDER_URL . 'assets/vmo-business-dashboard.js',
            ['jquery'],
            filemtime($js_file),
            true
        );
        wp_localize_script('vmo-business-dashboard', 'vmo_dashboard_data', [
            'ajaxurl'  => admin_url('admin-ajax.php'),
            'nonce'    => wp_create_nonce('vmo_nonce'),
            'user_id'  => $user_id,
            'rest_url' => rest_url('vmo/v1/'),
        ]);
    }

    // Enqueue dashboard styles
    $css_file = VMO_AI_BUILDER_DIR . 'assets/vmo-professional-summary.css';
    if (file_exists($css_file)) {
        wp_enqueue_style('vmo-professional-summary', VMO_AI_BUILDER_URL . 'assets/vmo-professional-summary.css', [], filemtime($css_file));
    }

    ob_start();
    // Try to load the strategic dashboard template
    $template = VMO_AI_BUILDER_DIR . 'templates/strategic-dashboard.php';
    if (file_exists($template)) {
        include $template;
    } else {
        ?>
        <div id="vmo-business-dashboard-root" class="vmo-dashboard-embed"
             data-user-id="<?php echo esc_attr($user_id); ?>"
             style="min-height:400px;background:#0d1117;border-radius:16px;padding:32px;border:1px solid #30363d;">
            <div class="vmo-dashboard-loading" style="text-align:center;color:#8b949e;padding:60px 0;">
                <div class="vmo-spinner" style="display:inline-block;width:40px;height:40px;border:3px solid #30363d;border-top-color:#58a6ff;border-radius:50%;animation:spin 1s linear infinite;"></div>
                <p style="margin-top:16px;">Loading your business dashboard...</p>
            </div>
        </div>
        <style>@keyframes spin { to { transform: rotate(360deg); } }</style>
        <?php
    }
    return ob_get_clean();
});

// ─── [vmo_pricing_table] ─────────────────────────────────────────────────
// Renders the 5-tier pricing grid with monthly/annual toggle.
// Supports attributes: highlight="pro" annual_discount="20"
add_shortcode('vmo_pricing_table', function($atts) {
    $a = shortcode_atts([
        'highlight'       => 'pro',
        'annual_discount' => 20,
    ], $atts, 'vmo_pricing_table');

    $discount = max(0, min(50, intval($a['annual_discount'])));
    $highlight_plan = strtolower($a['highlight']);

    $plans = [
        [
            'key'       => 'tester',
            'name'      => 'Tester',
            'price'     => 49.99,
            'tokens'    => 36034,
            'type'      => 'one-time',
            'features'  => ['Full AI consultation', 'Website generation', '108 min phone OR 127 pages', 'Email support', 'Basic analytics'],
            'cta'       => 'Get Started',
        ],
        [
            'key'       => 'starter',
            'name'      => 'Starter',
            'price'     => 99.99,
            'tokens'    => 72326,
            'type'      => 'monthly',
            'features'  => ['Everything in Tester', '217 min phone OR 255 pages', 'Custom domain', 'SEO optimization', 'Email marketing tools'],
            'cta'       => 'Start Building',
        ],
        [
            'key'       => 'pro',
            'name'      => 'Professional',
            'price'     => 199.99,
            'tokens'    => 144909,
            'type'      => 'monthly',
            'features'  => ['Everything in Starter', '435 min phone OR 512 pages', 'Deep research (29 chunks)', 'AI visual page editor', 'Priority support', 'Video generation'],
            'cta'       => 'Go Professional',
        ],
        [
            'key'       => 'elite',
            'name'      => 'Elite',
            'price'     => 499.99,
            'tokens'    => 362659,
            'type'      => 'monthly',
            'features'  => ['Everything in Pro', '1,089 min phone OR 1,281 pages', 'White-glove onboarding', 'Multi-site management', 'Legal documents', 'Advanced analytics'],
            'cta'       => 'Go Elite',
        ],
        [
            'key'       => 'enterprise',
            'name'      => 'Enterprise',
            'price'     => 999.99,
            'tokens'    => 500000,
            'type'      => 'monthly',
            'features'  => ['Everything in Elite', '500K tokens/month', 'Dedicated account manager', 'Custom AI fine-tuning', 'SLA guarantee', 'API access'],
            'cta'       => 'Contact Sales',
        ],
    ];

    // Get Stripe price IDs from settings
    $stripe_prices = [];
    foreach (['tester', 'starter', 'pro', 'elite', 'enterprise'] as $plan_key) {
        $stripe_prices[$plan_key] = [
            'monthly' => get_option("vmo_price_{$plan_key}", ''),
            'annual'  => get_option("vmo_price_{$plan_key}_annual", ''),
        ];
    }

    // Enqueue payments JS for Stripe checkout
    $payments_js = VMO_AI_BUILDER_DIR . 'assets/vmo-payments.js';
    if (file_exists($payments_js)) {
        wp_enqueue_script('vmo-payments', VMO_AI_BUILDER_URL . 'assets/vmo-payments.js', ['jquery'], filemtime($payments_js), true);
        wp_localize_script('vmo-payments', 'vmo_payments_data', [
            'ajaxurl' => admin_url('admin-ajax.php'),
            'nonce'   => wp_create_nonce('vmo_nonce'),
        ]);
    }

    ob_start();
    ?>
    <div class="vmo-pricing-table-wrap" style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;max-width:1400px;margin:0 auto;padding:40px 20px;">

        <!-- Billing Toggle -->
        <div class="vmo-billing-toggle" style="text-align:center;margin-bottom:40px;">
            <span class="vmo-billing-label vmo-monthly-label" style="color:#c9d1d9;font-weight:600;font-size:16px;">Monthly</span>
            <label class="vmo-toggle-switch" style="position:relative;display:inline-block;width:60px;height:30px;margin:0 16px;vertical-align:middle;">
                <input type="checkbox" id="vmo-annual-toggle" style="opacity:0;width:0;height:0;" />
                <span style="position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background:#30363d;border-radius:30px;transition:.3s;">
                    <span class="vmo-toggle-knob" style="position:absolute;content:'';height:24px;width:24px;left:3px;bottom:3px;background:#58a6ff;border-radius:50%;transition:.3s;"></span>
                </span>
            </label>
            <span class="vmo-billing-label vmo-annual-label" style="color:#8b949e;font-weight:600;font-size:16px;">
                Annual <span style="background:linear-gradient(135deg,#7c3aed,#2563eb);color:#fff;padding:2px 8px;border-radius:20px;font-size:12px;margin-left:4px;">Save <?php echo esc_html($discount); ?>%</span>
            </span>
        </div>

        <!-- Plans Grid -->
        <div class="vmo-plans-grid" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:24px;">
        <?php foreach ($plans as $plan):
            $is_highlighted = ($plan['key'] === $highlight_plan);
            $monthly = $plan['price'];
            $annual_monthly = round($monthly * (1 - $discount / 100), 2);
            $border_color = $is_highlighted ? '#7c3aed' : '#30363d';
            $bg = $is_highlighted ? 'background:linear-gradient(180deg,#161b22 0%,#1a1040 100%)' : 'background:#0d1117';
            $stripe_month = esc_attr($stripe_prices[$plan['key']]['monthly'] ?? '');
            $stripe_annual = esc_attr($stripe_prices[$plan['key']]['annual'] ?? '');
        ?>
            <div class="vmo-plan-card" data-plan="<?php echo esc_attr($plan['key']); ?>"
                 data-stripe-monthly="<?php echo $stripe_month; ?>"
                 data-stripe-annual="<?php echo $stripe_annual; ?>"
                 style="<?php echo $bg; ?>;border:2px solid <?php echo $border_color; ?>;border-radius:16px;padding:32px 24px;position:relative;transition:transform .2s,box-shadow .2s;">
                <?php if ($is_highlighted): ?>
                    <div style="position:absolute;top:-14px;left:50%;transform:translateX(-50%);background:linear-gradient(135deg,#7c3aed,#2563eb);color:#fff;padding:4px 16px;border-radius:20px;font-size:13px;font-weight:600;">Most Popular</div>
                <?php endif; ?>
                <h3 style="color:#f0f6fc;font-size:22px;margin:0 0 8px;"><?php echo esc_html($plan['name']); ?></h3>
                <div class="vmo-plan-price" style="margin-bottom:8px;">
                    <span class="vmo-price-monthly" style="color:#f0f6fc;font-size:36px;font-weight:700;">$<?php echo number_format($monthly, 2); ?></span>
                    <span class="vmo-price-annual" style="color:#f0f6fc;font-size:36px;font-weight:700;display:none;">$<?php echo number_format($annual_monthly, 2); ?></span>
                    <?php if ($plan['type'] === 'monthly'): ?>
                        <span style="color:#8b949e;font-size:14px;">/month</span>
                    <?php else: ?>
                        <span style="color:#8b949e;font-size:14px;">one-time</span>
                    <?php endif; ?>
                </div>
                <p style="color:#8b949e;font-size:14px;margin:0 0 20px;"><?php echo number_format($plan['tokens']); ?> AI tokens</p>
                <ul style="list-style:none;padding:0;margin:0 0 24px;">
                    <?php foreach ($plan['features'] as $feat): ?>
                        <li style="color:#c9d1d9;font-size:14px;padding:6px 0;display:flex;align-items:center;gap:8px;">
                            <svg width="16" height="16" viewBox="0 0 16 16" fill="none" style="flex-shrink:0;"><circle cx="8" cy="8" r="8" fill="#238636"/><path d="M5 8l2 2 4-4" stroke="#fff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
                            <?php echo esc_html($feat); ?>
                        </li>
                    <?php endforeach; ?>
                </ul>
                <button class="vmo-plan-select-btn" data-plan="<?php echo esc_attr($plan['key']); ?>"
                        style="width:100%;padding:14px;border:none;border-radius:12px;font-size:15px;font-weight:600;cursor:pointer;transition:opacity .2s;
                        <?php echo $is_highlighted ? 'background:linear-gradient(135deg,#7c3aed,#2563eb);color:#fff;' : 'background:#21262d;color:#c9d1d9;border:1px solid #30363d;'; ?>">
                    <?php echo esc_html($plan['cta']); ?>
                </button>
            </div>
        <?php endforeach; ?>
        </div>
    </div>

    <script>
    (function(){
        var toggle = document.getElementById('vmo-annual-toggle');
        if (!toggle) return;
        toggle.addEventListener('change', function(){
            var annual = this.checked;
            document.querySelectorAll('.vmo-price-monthly').forEach(function(el){ el.style.display = annual ? 'none' : ''; });
            document.querySelectorAll('.vmo-price-annual').forEach(function(el){ el.style.display = annual ? '' : 'none'; });
            var knob = document.querySelector('.vmo-toggle-knob');
            if (knob) knob.style.transform = annual ? 'translateX(30px)' : 'translateX(0)';
            var labels = document.querySelectorAll('.vmo-billing-label');
            if (labels.length >= 2) {
                labels[0].style.color = annual ? '#8b949e' : '#c9d1d9';
                labels[1].style.color = annual ? '#c9d1d9' : '#8b949e';
            }
        });
        document.querySelectorAll('.vmo-plan-select-btn').forEach(function(btn){
            btn.addEventListener('click', function(){
                var card = this.closest('.vmo-plan-card');
                var annual = document.getElementById('vmo-annual-toggle').checked;
                var priceId = annual ? card.getAttribute('data-stripe-annual') : card.getAttribute('data-stripe-monthly');
                var planKey = card.getAttribute('data-plan');
                if (priceId && typeof jQuery !== 'undefined') {
                    jQuery.post(vmo_payments_data.ajaxurl, {
                        action: 'vmo_create_checkout_session',
                        nonce: vmo_payments_data.nonce,
                        price_id: priceId,
                        plan: planKey,
                        billing: annual ? 'annual' : 'monthly'
                    }, function(resp) {
                        if (resp.success && resp.data.url) {
                            window.location.href = resp.data.url;
                        } else {
                            alert(resp.data && resp.data.message ? resp.data.message : 'Unable to start checkout. Please try again.');
                        }
                    });
                } else if (!priceId) {
                    alert('This plan is not yet configured. Please contact support.');
                }
            });
        });
    })();
    </script>
    <?php
    return ob_get_clean();
});


// ╔══════════════════════════════════════════════════════════════════════════╗
// ║                     CRON JOB HANDLERS                                  ║
// ╚══════════════════════════════════════════════════════════════════════════╝

// ─── Session Cleanup ──────────────────────────────────────────────────────
// Removes consultation sessions older than 30 days and orphaned temp data.
add_action('vmo_cleanup_expired_sessions', function() {
    global $wpdb;

    // Clean up consultation sessions older than 30 days
    $tables_to_clean = [
        $wpdb->prefix . 'vmo_consultation_sessions',
        $wpdb->prefix . 'vmo_conversation_history',
    ];
    foreach ($tables_to_clean as $table) {
        if ($wpdb->get_var("SHOW TABLES LIKE '{$table}'") === $table) {
            $wpdb->query($wpdb->prepare(
                "DELETE FROM {$table} WHERE created_at < %s",
                gmdate('Y-m-d H:i:s', strtotime('-30 days'))
            ));
        }
    }

    // Clean up orphaned transients related to VMO
    $wpdb->query(
        "DELETE FROM {$wpdb->options}
         WHERE option_name LIKE '_transient_vmo_%'
         AND option_name LIKE '_transient_timeout_vmo_%'"
    );

    // Clean up expired research chunks
    $research_table = $wpdb->prefix . 'vmo_deep_research';
    if ($wpdb->get_var("SHOW TABLES LIKE '{$research_table}'") === $research_table) {
        $wpdb->query($wpdb->prepare(
            "DELETE FROM {$research_table} WHERE created_at < %s AND status = 'completed'",
            gmdate('Y-m-d H:i:s', strtotime('-60 days'))
        ));
    }

    if (defined('WP_DEBUG') && WP_DEBUG) {
        error_log('[VMO Cron] Expired sessions cleanup completed at ' . current_time('mysql'));
    }
});

// ─── Email Queue Processor ─────────────────────────────────────────────────
function vmo_ensure_email_queue_table_schema() {
    global $wpdb;
    $table = $wpdb->prefix . 'vmo_email_queue';
    require_once ABSPATH . 'wp-admin/includes/upgrade.php';
    $charset_collate = $wpdb->get_charset_collate();
    $sql = "CREATE TABLE {$table} (
        id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
        user_id BIGINT UNSIGNED NOT NULL,
        mailbox_id BIGINT UNSIGNED NULL,
        type VARCHAR(30) NOT NULL DEFAULT 'transactional',
        to_email VARCHAR(255) NOT NULL,
        from_email VARCHAR(255) NULL,
        cc_email TEXT NULL,
        bcc_email TEXT NULL,
        subject VARCHAR(255) NOT NULL,
        body LONGTEXT NOT NULL,
        provider VARCHAR(30) NULL,
        headers LONGTEXT NULL,
        attachments LONGTEXT NULL,
        last_error TEXT NULL,
        status VARCHAR(40) NOT NULL DEFAULT 'pending',
        priority INT NOT NULL DEFAULT 5,
        scheduled_at DATETIME NOT NULL,
        sent_at DATETIME NULL,
        attempts INT NOT NULL DEFAULT 0,
        created_at DATETIME NOT NULL,
        updated_at DATETIME NULL,
        PRIMARY KEY (id),
        KEY status_schedule (status, scheduled_at),
        KEY user_status (user_id, status),
        KEY mailbox_status (mailbox_id, status)
    ) {$charset_collate};";
    dbDelta($sql);
}

// Processes pending outbound emails through the same router/storage path as instant sends.
function vmo_process_scheduled_email_queue() {
    global $wpdb;
    $table = $wpdb->prefix . 'vmo_email_queue';

    vmo_ensure_email_queue_table_schema();

    if ($wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table)) !== $table) {
        return;
    }

    // Grab up to 50 pending emails per run
    $pending = $wpdb->get_results($wpdb->prepare(
        "SELECT * FROM {$table} WHERE status = 'pending' AND scheduled_at <= %s ORDER BY priority DESC, created_at ASC LIMIT 50",
        current_time('mysql')
    ));

    if (empty($pending)) return;

    if (!class_exists('VMO_Email_Router')) {
        $router_file = defined('VMO_AI_BUILDER_DIR') ? VMO_AI_BUILDER_DIR . 'includes/class-vmo-email-router.php' : __DIR__ . '/includes/class-vmo-email-router.php';
        if (file_exists($router_file) && is_readable($router_file)) {
            require_once $router_file;
        }
    }

    if (!class_exists('VMO_Email_Router')) {
        foreach ($pending as $email) {
            $wpdb->update($table, [
                'status' => 'failed',
                'last_error' => 'VMO email router is unavailable.',
                'updated_at' => current_time('mysql'),
            ], ['id' => $email->id]);
        }
        return;
    }

    foreach ($pending as $email) {
        // Mark as processing
        $attempts = intval($email->attempts) + 1;
        $wpdb->update($table, [
            'status' => 'processing',
            'attempts' => $attempts,
            'updated_at' => current_time('mysql'),
        ], ['id' => $email->id]);

        $success = false;
        $error_message = '';
        $args = [
            'skip_storage' => true,
            'user_id' => intval($email->user_id),
            'headers' => ['Content-Type: text/html; charset=UTF-8'],
        ];
        $from_email = !empty($email->from_email) ? sanitize_email((string) $email->from_email) : '';
        $mailbox_id = isset($email->mailbox_id) ? intval($email->mailbox_id) : 0;
        if ($mailbox_id > 0) {
            $args['mailbox_id'] = $mailbox_id;
        }
        if ($from_email) {
            $args['from_email'] = $from_email;
        }

        foreach (['Cc' => $email->cc_email ?? '', 'Bcc' => $email->bcc_email ?? ''] as $header_name => $raw_recipients) {
            $emails = array_filter(array_map('sanitize_email', preg_split('/[,;\s]+/', (string) $raw_recipients)));
            if (!empty($emails)) {
                $args['headers'][] = $header_name . ': ' . implode(', ', $emails);
            }
        }

        $attachment_metadata = [];
        if (!empty($email->attachments)) {
            $decoded_attachments = json_decode((string) $email->attachments, true);
            if (is_array($decoded_attachments)) {
                $args['attachments'] = array_values(array_filter(array_map('sanitize_text_field', $decoded_attachments)));
                foreach ($args['attachments'] as $attachment_path) {
                    $attachment_path = (string) $attachment_path;
                    $attachment_metadata[] = [
                        'name' => basename($attachment_path),
                        'size' => file_exists($attachment_path) ? filesize($attachment_path) : 0,
                        'delivery' => 'attachment',
                    ];
                }
            }
        }

        $accounts_table = $wpdb->prefix . 'vmo_email_accounts';
        if (function_exists('vmo_ensure_email_accounts_table')) {
            vmo_ensure_email_accounts_table();
        }
        if (($mailbox_id > 0 || $from_email) && $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $accounts_table)) === $accounts_table) {
            if ($mailbox_id > 0) {
                $account = $wpdb->get_row($wpdb->prepare(
                    "SELECT id, email, display_name, smtp_host, smtp_port, encrypted_password FROM {$accounts_table} WHERE user_id = %d AND id = %d LIMIT 1",
                    intval($email->user_id),
                    $mailbox_id
                ));
            } else {
                $account = $wpdb->get_row($wpdb->prepare(
                    "SELECT id, email, display_name, smtp_host, smtp_port, encrypted_password FROM {$accounts_table} WHERE user_id = %d AND (email = %s OR email_address = %s) LIMIT 1",
                    intval($email->user_id),
                    $from_email,
                    $from_email
                ));
            }

            if (!empty($account->email)) {
                $mailbox_id = intval($account->id);
                $from_email = sanitize_email((string) $account->email);
                $args['mailbox_id'] = $mailbox_id;
                $args['from_email'] = $from_email;
                if (!empty($account->display_name)) {
                    $args['from_name'] = sanitize_text_field((string) $account->display_name);
                }
                if (!empty($account->smtp_host) && !empty($account->smtp_port) && !empty($account->encrypted_password) && function_exists('vmo_decrypt_credential')) {
                    $password = vmo_decrypt_credential((string) $account->encrypted_password);
                    if ($password) {
                        $port = intval($account->smtp_port);
                        $args['smtp'] = [
                            'host' => (string) $account->smtp_host,
                            'port' => $port,
                            'username' => (string) $account->email,
                            'password' => $password,
                            'secure' => $port === 465 ? 'ssl' : ($port === 587 ? 'tls' : ''),
                        ];
                    }
                }
            }
        }

        try {
            $router = VMO_Email_Router::get_instance();
            $result = $router->send_email(
                (string) $email->to_email,
                (string) $email->subject,
                (string) $email->body,
                !empty($email->type) ? sanitize_key((string) $email->type) : 'transactional',
                $args
            );
            $success = is_array($result) ? !empty($result['success']) : (bool) $result;
            if (!$success && is_array($result)) {
                $error_message = sanitize_text_field((string) ($result['message'] ?? 'Email router could not send the message.'));
            }
        } catch (Throwable $e) {
            $error_message = $e->getMessage();
            if (defined('WP_DEBUG') && WP_DEBUG) {
                error_log('[VMO Email Queue] Error sending email #' . $email->id . ': ' . $e->getMessage());
            }
        }

        if ($success) {
            $wpdb->update($table, [
                'status'  => 'sent',
                'sent_at' => current_time('mysql'),
                'last_error' => null,
                'updated_at' => current_time('mysql'),
            ], ['id' => $email->id]);

            if (!class_exists('VMO_Email_Storage')) {
                $storage_file = defined('VMO_AI_BUILDER_DIR') ? VMO_AI_BUILDER_DIR . 'includes/class-vmo-email-storage.php' : __DIR__ . '/includes/class-vmo-email-storage.php';
                if (file_exists($storage_file) && is_readable($storage_file)) {
                    require_once $storage_file;
                }
            }
            if (class_exists('VMO_Email_Storage')) {
                $storage = new VMO_Email_Storage();
                $storage->save_email([
                    'user_id' => intval($email->user_id),
                    'mailbox_id' => $mailbox_id > 0 ? $mailbox_id : null,
                    'folder' => 'sent',
                    'type' => !empty($email->type) ? sanitize_key((string) $email->type) : 'transactional',
                    'provider' => !empty($args['smtp']) ? 'smtp' : 'local_host',
                    'from_email' => $from_email ?: sanitize_email(get_option('admin_email')),
                    'to_email' => (string) $email->to_email,
                    'cc_email' => (string) ($email->cc_email ?? ''),
                    'bcc_email' => (string) ($email->bcc_email ?? ''),
                    'subject' => (string) $email->subject,
                    'message' => (string) $email->body,
                    'attachments' => !empty($attachment_metadata) ? $attachment_metadata : null,
                    'status' => 'sent',
                    'is_read' => 1,
                    'is_ai_generated' => 0,
                    'created_at' => current_time('mysql'),
                    'updated_at' => current_time('mysql'),
                    'sent_at' => current_time('mysql'),
                ]);
            }
        } else {
            // Retry up to 3 times, then mark failed
            $new_status = ($attempts >= 3) ? 'failed' : 'pending';
            $wpdb->update($table, [
                'status' => $new_status,
                'last_error' => $error_message ?: 'Email router could not send the message.',
                'updated_at' => current_time('mysql'),
            ], ['id' => $email->id]);
        }
    }

    if (defined('WP_DEBUG') && WP_DEBUG) {
        error_log('[VMO Cron] Email queue processed: ' . count($pending) . ' emails at ' . current_time('mysql'));
    }
}
add_action('vmo_process_email_queue', 'vmo_process_scheduled_email_queue');
add_action('vmo_process_scheduled_emails', 'vmo_process_scheduled_email_queue');

// ─── Stripe Subscription Sync ──────────────────────────────────────────────
// Fallback sync for Stripe subscription status changes that may have been
// missed by webhooks (network issues, webhook failures, etc.).
add_action('vmo_sync_stripe_subscriptions', function() {
    $stripe_secret = get_option('vmo_stripe_secret_key', '');
    if (empty($stripe_secret)) return;

    global $wpdb;

    // Get all users with active Stripe subscriptions
    $users_with_subs = $wpdb->get_results(
        "SELECT user_id, meta_value as subscription_id
         FROM {$wpdb->usermeta}
         WHERE meta_key = 'vmo_stripe_subscription_id'
         AND meta_value != ''
         LIMIT 100"
    );

    if (empty($users_with_subs)) return;

    foreach ($users_with_subs as $user_sub) {
        try {
            $response = wp_remote_get(
                "https://api.stripe.com/v1/subscriptions/{$user_sub->subscription_id}",
                [
                    'headers' => [
                        'Authorization' => 'Bearer ' . $stripe_secret,
                    ],
                    'timeout' => 15,
                ]
            );

            if (is_wp_error($response)) continue;

            $body = json_decode(wp_remote_retrieve_body($response), true);
            if (empty($body) || !isset($body['status'])) continue;

            $stripe_status = $body['status'];
            $stored_status = get_user_meta($user_sub->user_id, 'vmo_subscription_status', true);
            $recoverable_plan = function_exists('vmo_get_user_recoverable_plan') ? vmo_get_user_recoverable_plan($user_sub->user_id) : (string) $stored_status;

            if ($stripe_status !== $stored_status) {
                update_user_meta($user_sub->user_id, 'vmo_subscription_status', $stripe_status);

                if (in_array($stripe_status, ['active', 'trialing'], true)) {
                    update_user_meta($user_sub->user_id, 'vmo_subscription_payment_status', $stripe_status === 'trialing' ? 'trialing' : 'paid');
                    if ($stripe_status === 'trialing' && !empty($body['trial_end'])) {
                        update_user_meta($user_sub->user_id, 'vmo_free_access_status', 'active');
                        update_user_meta($user_sub->user_id, 'vmo_free_access_expires_at', gmdate('Y-m-d H:i:s', (int) $body['trial_end']));
                        update_user_meta($user_sub->user_id, 'vmo_trial_access_expires_at', gmdate('Y-m-d H:i:s', (int) $body['trial_end']));
                    } elseif ($stripe_status === 'active') {
                        update_user_meta($user_sub->user_id, 'vmo_free_access_status', 'converted_to_paid');
                    }
                    if (function_exists('vmo_unlock_user_services')) {
                        vmo_unlock_user_services($user_sub->user_id, [
                            'reason' => 'stripe_subscription_sync_' . $stripe_status,
                            'payment_status' => $stripe_status === 'trialing' ? 'trialing' : 'paid',
                            'plan' => $recoverable_plan,
                            'restore_pre_lock_tokens' => true,
                        ]);
                    }
                } elseif (in_array($stripe_status, ['canceled', 'unpaid', 'past_due', 'paused', 'incomplete', 'incomplete_expired'], true)) {
                    update_user_meta($user_sub->user_id, 'vmo_subscription_payment_status', $stripe_status);
                    if ($stripe_status === 'canceled') {
                        update_user_meta($user_sub->user_id, 'vmo_plan_expired_at', current_time('mysql'));
                    }
                    if (function_exists('vmo_lock_user_services')) {
                        vmo_lock_user_services($user_sub->user_id, $stripe_status, [
                            'plan' => $recoverable_plan,
                            'stripe_subscription_id' => (string) $user_sub->subscription_id,
                            'source' => 'stripe_subscription_sync',
                            'preserve_site_assets' => true,
                        ]);
                    } else {
                        update_user_meta($user_sub->user_id, 'vmo_token_balance', 0);
                    }
                }

                // Log the status change
                if (defined('WP_DEBUG') && WP_DEBUG) {
                    error_log("[VMO Stripe Sync] User {$user_sub->user_id}: {$stored_status} → {$stripe_status}");
                }
            } elseif (in_array($stripe_status, ['canceled', 'unpaid', 'past_due', 'paused', 'incomplete', 'incomplete_expired'], true) && function_exists('vmo_is_user_service_locked') && !vmo_is_user_service_locked($user_sub->user_id)) {
                if (function_exists('vmo_lock_user_services')) {
                    vmo_lock_user_services($user_sub->user_id, $stripe_status, [
                        'plan' => $recoverable_plan,
                        'stripe_subscription_id' => (string) $user_sub->subscription_id,
                        'source' => 'stripe_subscription_sync_backfill',
                        'preserve_site_assets' => true,
                    ]);
                }
            } elseif (in_array($stripe_status, ['active', 'trialing'], true) && get_user_meta($user_sub->user_id, 'vmo_service_lock_status', true) === 'locked' && function_exists('vmo_unlock_user_services')) {
                if ($stripe_status === 'trialing' && !empty($body['trial_end'])) {
                    update_user_meta($user_sub->user_id, 'vmo_free_access_status', 'active');
                    update_user_meta($user_sub->user_id, 'vmo_free_access_expires_at', gmdate('Y-m-d H:i:s', (int) $body['trial_end']));
                    update_user_meta($user_sub->user_id, 'vmo_trial_access_expires_at', gmdate('Y-m-d H:i:s', (int) $body['trial_end']));
                } elseif ($stripe_status === 'active') {
                    update_user_meta($user_sub->user_id, 'vmo_free_access_status', 'converted_to_paid');
                }
                vmo_unlock_user_services($user_sub->user_id, [
                    'reason' => 'stripe_subscription_sync_backfill_' . $stripe_status,
                    'payment_status' => $stripe_status === 'trialing' ? 'trialing' : 'paid',
                    'plan' => $recoverable_plan,
                    'restore_pre_lock_tokens' => true,
                ]);
            }
        } catch (Throwable $e) {
            if (defined('WP_DEBUG') && WP_DEBUG) {
                error_log('[VMO Stripe Sync] Error: ' . $e->getMessage());
            }
        }
    }
});

// ─── Weekly Analytics Report ────────────────────────────────────────────────
add_action('vmo_weekly_analytics_report', function() {
    if (class_exists('VMO_Analytics_Tracker')) {
        try {
            $tracker = VMO_Analytics_Tracker::get_instance();
            if (method_exists($tracker, 'generate_weekly_report')) {
                $tracker->generate_weekly_report();
            }
        } catch (Throwable $e) {
            if (defined('WP_DEBUG') && WP_DEBUG) {
                error_log('[VMO Cron] Weekly analytics report error: ' . $e->getMessage());
            }
        }
    }
});

// ─── Weekly SEO Report ──────────────────────────────────────────────────────
add_action('vmo_weekly_seo_report', function() {
    if (class_exists('VMO_SEO_Engine')) {
        try {
            $seo = new VMO_SEO_Engine();
            if (method_exists($seo, 'generate_weekly_report')) {
                $seo->generate_weekly_report();
            }
        } catch (Throwable $e) {
            if (defined('WP_DEBUG') && WP_DEBUG) {
                error_log('[VMO Cron] Weekly SEO report error: ' . $e->getMessage());
            }
        }
    }
});

// ─── Daily SEO Check ────────────────────────────────────────────────────────
add_action('vmo_daily_seo_check', function() {
    if (class_exists('VMO_SEO_Engine')) {
        try {
            $seo = new VMO_SEO_Engine();
            if (method_exists($seo, 'run_daily_check')) {
                $seo->run_daily_check();
            }
        } catch (Throwable $e) {
            if (defined('WP_DEBUG') && WP_DEBUG) {
                error_log('[VMO Cron] Daily SEO check error: ' . $e->getMessage());
            }
        }
    }
});

// ─── Daily Competitor Check ─────────────────────────────────────────────────
add_action('vmo_daily_competitor_check', function() {
    if (class_exists('VMO_Competitor_Monitor')) {
        try {
            $monitor = new VMO_Competitor_Monitor();
            if (method_exists($monitor, 'run_daily_check')) {
                $monitor->run_daily_check();
            }
        } catch (Throwable $e) {
            if (defined('WP_DEBUG') && WP_DEBUG) {
                error_log('[VMO Cron] Daily competitor check error: ' . $e->getMessage());
            }
        }
    }
});

// ─── Guardrail G3: Daily Hosting Usage Monitor ──────────────────────────────
add_action('vmo_daily_hosting_usage_check', function() {
    if (class_exists('VMO_QuickLaunch')) {
        try {
            VMO_QuickLaunch::instance()->check_hosting_usage_thresholds();
        } catch (Throwable $e) {
            if (defined('WP_DEBUG') && WP_DEBUG) {
                error_log('[VMO Cron] Hosting usage check error: ' . $e->getMessage());
            }
        }
    }
});


// ─── VPS Teardown on Subscription Cancellation ─────────────────────────────
// Destroys the Vultr VPS instance 7 days after subscription cancellation.
// Scheduled by VMO_QuickLaunch::handle_subscription_deleted().
add_action('vmo_ql_teardown_vps', function($user_id, $domain) {
    // Double-check the subscription is still canceled (user may have reactivated)
    $status = get_user_meta($user_id, 'vmo_subscription_status', true);
    if ($status === 'active' || empty(get_user_meta($user_id, 'vmo_vps_teardown_scheduled', true))) {
        error_log("[VMO VPS Teardown] Skipped for user {$user_id} ({$domain}) — subscription is active or teardown was canceled.");
        return;
    }

    $vultr_key = get_option('vmo_vultr_api_key', '');
    if (empty($vultr_key)) {
        error_log("[VMO VPS Teardown] Cannot destroy VPS for {$domain} — Vultr API key not configured.");
        return;
    }

    // Find the Vultr instance by label matching the domain
    $list_response = wp_remote_get('https://api.vultr.com/v2/instances?tag=vmo-quicklaunch&per_page=100', [
        'headers' => ['Authorization' => 'Bearer ' . $vultr_key],
        'timeout' => 30,
    ]);

    if (is_wp_error($list_response)) {
        error_log("[VMO VPS Teardown] Vultr API error: " . $list_response->get_error_message());
        return;
    }

    $body = json_decode(wp_remote_retrieve_body($list_response), true);
    $instances = $body['instances'] ?? [];

    $instance_id = null;
    foreach ($instances as $inst) {
        $label = $inst['label'] ?? '';
        if (stripos($label, $domain) !== false) {
            $instance_id = $inst['id'];
            break;
        }
    }

    if (!$instance_id) {
        error_log("[VMO VPS Teardown] No Vultr instance found for domain {$domain}.");
        delete_user_meta($user_id, 'vmo_vps_teardown_scheduled');
        return;
    }

    // Destroy the instance
    $destroy_response = wp_remote_request("https://api.vultr.com/v2/instances/{$instance_id}", [
        'method'  => 'DELETE',
        'headers' => ['Authorization' => 'Bearer ' . $vultr_key],
        'timeout' => 30,
    ]);

    if (is_wp_error($destroy_response)) {
        error_log("[VMO VPS Teardown] Failed to destroy instance {$instance_id}: " . $destroy_response->get_error_message());
        return;
    }

    $status_code = wp_remote_retrieve_response_code($destroy_response);
    if ($status_code === 204 || $status_code === 200) {
        error_log("[VMO VPS Teardown] Successfully destroyed VPS {$instance_id} for user {$user_id} ({$domain}).");
        delete_user_meta($user_id, 'vmo_vps_teardown_scheduled');
        update_user_meta($user_id, 'vmo_vps_destroyed_at', current_time('mysql'));

        // Notify user
        $user = get_user_by('ID', $user_id);
        if ($user) {
            wp_mail(
                $user->user_email,
                'VMO Website Removed',
                "Hello,\n\nYour VMO website at {$domain} has been permanently removed due to subscription cancellation.\n\n"
                . "If you'd like to start fresh, you can sign up again at " . home_url('/get-started/') . "\n\n"
                . "— The VMO Team"
            );
        }
    } else {
        error_log("[VMO VPS Teardown] Unexpected response code {$status_code} when destroying instance {$instance_id}.");
    }
}, 10, 2);


// ╔══════════════════════════════════════════════════════════════════════════╗
// ║              AJAX ACTION ALIASES (Spec → Actual Mapping)               ║
// ║  Maps spec-documented action names to their actual handler methods     ║
// ╚══════════════════════════════════════════════════════════════════════════╝

// Research aliases
add_action('wp_ajax_vmo_deep_research_start', function() {
    // Alias → actual deep research trigger
    if (class_exists('VMO_Deep_Research')) {
        $dr = VMO_Deep_Research::get_instance();
        if (method_exists($dr, 'ajax_trigger_research')) {
            $dr->ajax_trigger_research();
            return;
        }
    }
    wp_send_json_error(['message' => 'Deep research handler not available'], 500);
});

add_action('wp_ajax_vmo_deep_research_poll', function() {
    if (class_exists('VMO_Deep_Research')) {
        $dr = VMO_Deep_Research::get_instance();
        if (method_exists($dr, 'ajax_poll_chunk')) {
            $dr->ajax_poll_chunk();
            return;
        }
    }
    wp_send_json_error(['message' => 'Deep research poll handler not available'], 500);
});

// Consultation message alias
add_action('wp_ajax_vmo_send_consultation_message', function() {
    if (class_exists('VMO_Enhanced_Business_Wizard')) {
        $wizard = VMO_Enhanced_Business_Wizard::get_instance();
        if (method_exists($wizard, 'ajax_process_consultation_message')) {
            $wizard->ajax_process_consultation_message();
            return;
        }
    }
    wp_send_json_error(['message' => 'Consultation handler not available'], 500);
});

// Legacy completion alias: completion now means manifest/readiness is present, not old website-builder finalization.
add_action('wp_ajax_vmo_complete_consultation', function() {
    $nonce_value = '';
    foreach (array('security', '_ajax_nonce', 'nonce') as $nonce_field) {
        if (isset($_POST[$nonce_field])) {
            $nonce_value = sanitize_text_field(wp_unslash($_POST[$nonce_field]));
            break;
        }
    }
    if (!$nonce_value || !(wp_verify_nonce($nonce_value, 'vmo_builder_nonce') || wp_verify_nonce($nonce_value, 'vmo_admin_nonce'))) {
        wp_send_json_error(array('message' => 'Security validation failed.'), 403);
    }

    $user_id = get_current_user_id();
    $session_id = sanitize_text_field((string) ($_POST['consultation_id'] ?? ($_POST['session_id'] ?? '')));
    $manifest = $session_id !== '' ? get_user_meta($user_id, 'vmo_business_manifest_' . $session_id, true) : array();
    if (!is_array($manifest) || empty($manifest)) {
        $manifest = get_user_meta($user_id, 'vmo_business_manifest', true);
    }
    $operating_state = get_user_meta($user_id, $session_id !== '' ? 'vmo_business_operating_state_' . $session_id : 'vmo_business_operating_state', true);
    if (!is_array($operating_state)) {
        $operating_state = get_user_meta($user_id, 'vmo_business_operating_state', true);
    }

    wp_send_json_success(array(
        'status' => !empty($manifest) ? 'manifest_ready' : 'manifest_pending',
        'source_of_truth' => 'VMO_Enhanced_Business_Wizard',
        'summary_message' => !empty($manifest)
            ? 'Your consultation is now stored as the VMO business operating manifest. Jarvis can use it to run the dashboard, website, email, phone, payments, documents, KPIs, and automations.'
            : 'The enhanced consultation is still preparing your operating manifest. Continue through the guided consultation flow.',
        'business_manifest' => is_array($manifest) ? $manifest : array(),
        'operating_state' => is_array($operating_state) ? $operating_state : array(),
    ));
});

// Promotion Command Center helpers and AJAX handlers
if (!function_exists('vmo_promotion_platforms')) {
    function vmo_promotion_platforms() {
        return array(
            'Instagram', 'Facebook', 'LinkedIn', 'TikTok', 'X', 'YouTube', 'Pinterest', 'Threads',
            'Snapchat', 'Reddit', 'Tumblr', 'Mastodon', 'Google Business Profile', 'WhatsApp Channels',
            'Telegram', 'Discord Community',
        );
    }
}

if (!function_exists('vmo_promotion_default_settings')) {
    function vmo_promotion_default_settings() {
        return array(
            'goal' => 'Help me create and distribute strategic social posts that attract the right followers, increase engagement, and promote my products effectively.',
            'social_mode' => 'ai',
            'selected_platforms' => array('Instagram', 'Facebook', 'LinkedIn', 'TikTok', 'YouTube'),
            'auto_post' => false,
            'reply_mode' => true,
            'promo_budget' => 6800,
            'cadence' => '5 posts per week',
            'cta_destination' => 'ai_recommend',
            'manual_notes' => '',
            'manual_assets' => array(),
            'latest_strategy' => array(),
            'latest_posts' => array(),
            'latest_engagement' => array(),
        );
    }
}

if (!function_exists('vmo_promotion_default_queue')) {
    function vmo_promotion_default_queue() {
        return array(
            array('id' => 'promo_seed_1', 'title' => 'Weekend promo reel', 'status' => 'Awaiting Approval', 'time' => 'Today · 2:00 PM'),
            array('id' => 'promo_seed_2', 'title' => 'Product carousel post', 'status' => 'Scheduled', 'time' => 'Tomorrow · 10:30 AM'),
            array('id' => 'promo_seed_3', 'title' => 'Follower engagement question', 'status' => 'Draft', 'time' => 'Fri · 6:00 PM'),
        );
    }
}

if (!function_exists('vmo_promotion_feature_enabled')) {
    function vmo_promotion_feature_enabled() {
        $enabled = defined('VMO_PROMOTION_FEATURE_ENABLED') ? (bool) VMO_PROMOTION_FEATURE_ENABLED : false;
        return (bool) apply_filters('vmo_promotion_feature_enabled', $enabled);
    }
}

if (!function_exists('vmo_promotion_is_preview_only')) {
    function vmo_promotion_is_preview_only() {
        return !vmo_promotion_feature_enabled();
    }
}

if (!function_exists('vmo_promotion_preview_message')) {
    function vmo_promotion_preview_message() {
        return 'The Promotion Command Center is coming soon in this release. Everything remains visible for preview, but social posting, provider connections, replies, and advertising controls stay locked until the next update.';
    }
}

if (!function_exists('vmo_promotion_require_access')) {
    function vmo_promotion_require_access($capability = 'read') {
        if (!current_user_can($capability)) {
            wp_send_json_error(array('message' => 'Unauthorized'), 403);
        }
        if (!function_exists('vmo_verify_admin_security') || !vmo_verify_admin_security()) {
            wp_send_json_error(array('message' => 'Invalid nonce', 'status' => 'invalid_nonce'), 403);
        }
    }
}

if (!function_exists('vmo_promotion_require_live')) {
    function vmo_promotion_require_live($capability = 'read') {
        vmo_promotion_require_access($capability);
        if (vmo_promotion_is_preview_only()) {
            wp_send_json_error(array(
                'message' => vmo_promotion_preview_message(),
                'status' => 'promotion_preview_only',
                'preview_only' => true,
            ), 423);
        }
    }
}

if (!function_exists('vmo_promotion_decode_json_request')) {
    function vmo_promotion_decode_json_request($key, $default = array()) {
        $raw = wp_unslash($_POST[$key] ?? '');
        if (is_array($raw)) {
            return $raw;
        }
        if (!is_string($raw) || trim($raw) === '') {
            return $default;
        }
        $decoded = json_decode($raw, true);
        return is_array($decoded) ? $decoded : $default;
    }
}

if (!function_exists('vmo_promotion_normalize_result_items')) {
    function vmo_promotion_normalize_result_items($items, $type = 'generic') {
        $items = is_array($items) ? array_values($items) : array();
        $normalized = array();
        foreach ($items as $item) {
            if (!is_array($item)) {
                $item = array('title' => '', 'text' => (string) $item);
            }
            $entry = array(
                'title' => sanitize_text_field($item['title'] ?? ''),
                'text' => sanitize_textarea_field($item['text'] ?? ($item['summary'] ?? '')),
                'summary' => sanitize_textarea_field($item['summary'] ?? ($item['text'] ?? '')),
                'caption' => sanitize_textarea_field($item['caption'] ?? ($item['text'] ?? '')),
                'cta' => sanitize_text_field($item['cta'] ?? ''),
                'asset_hint' => sanitize_text_field($item['asset_hint'] ?? ''),
            );
            if ($type === 'strategy') {
                $normalized[] = array(
                    'title' => $entry['title'] !== '' ? $entry['title'] : 'Strategy',
                    'text' => $entry['text'] !== '' ? $entry['text'] : $entry['summary'],
                );
            } elseif ($type === 'posts') {
                $normalized[] = array(
                    'title' => $entry['title'] !== '' ? $entry['title'] : 'Draft Post',
                    'caption' => $entry['caption'],
                    'cta' => $entry['cta'],
                    'asset_hint' => $entry['asset_hint'],
                );
            } else {
                $normalized[] = array(
                    'title' => $entry['title'] !== '' ? $entry['title'] : 'Suggestion',
                    'text' => $entry['text'] !== '' ? $entry['text'] : $entry['summary'],
                );
            }
            if (count($normalized) >= 10) {
                break;
            }
        }
        return $normalized;
    }
}

if (!function_exists('vmo_promotion_normalize_settings')) {
    function vmo_promotion_normalize_settings($settings) {
        $defaults = vmo_promotion_default_settings();
        $settings = is_array($settings) ? array_merge($defaults, $settings) : $defaults;
        $allowed_platforms = vmo_promotion_platforms();
        $selected = array();
        foreach ((array) ($settings['selected_platforms'] ?? array()) as $platform) {
            $platform = sanitize_text_field($platform);
            if (in_array($platform, $allowed_platforms, true) && !in_array($platform, $selected, true)) {
                $selected[] = $platform;
            }
        }
        if (empty($selected)) {
            $selected = $defaults['selected_platforms'];
        }
        $cta = sanitize_key((string) ($settings['cta_destination'] ?? 'ai_recommend'));
        if (!in_array($cta, array('product_page', 'store_page', 'direct_checkout', 'ai_recommend'), true)) {
            $cta = 'ai_recommend';
        }
        $manual_assets = array();
        foreach ((array) ($settings['manual_assets'] ?? array()) as $asset) {
            if (!is_array($asset)) {
                continue;
            }
            $manual_assets[] = array(
                'id' => sanitize_text_field($asset['id'] ?? wp_generate_uuid4()),
                'name' => sanitize_file_name($asset['name'] ?? ($asset['file_name'] ?? 'asset')),
                'url' => esc_url_raw($asset['url'] ?? ''),
                'kind' => sanitize_key($asset['kind'] ?? 'asset'),
                'type' => sanitize_text_field($asset['type'] ?? ''),
            );
            if (count($manual_assets) >= 20) {
                break;
            }
        }

        return array(
            'goal' => sanitize_textarea_field($settings['goal'] ?? $defaults['goal']),
            'social_mode' => ($settings['social_mode'] ?? 'ai') === 'manual' ? 'manual' : 'ai',
            'selected_platforms' => $selected,
            'auto_post' => !empty($settings['auto_post']),
            'reply_mode' => !empty($settings['reply_mode']),
            'promo_budget' => max(0, (int) round((float) ($settings['promo_budget'] ?? $defaults['promo_budget']))),
            'cadence' => sanitize_text_field($settings['cadence'] ?? $defaults['cadence']),
            'cta_destination' => $cta,
            'manual_notes' => sanitize_textarea_field($settings['manual_notes'] ?? ''),
            'manual_assets' => $manual_assets,
            'latest_strategy' => vmo_promotion_normalize_result_items($settings['latest_strategy'] ?? array(), 'strategy'),
            'latest_posts' => vmo_promotion_normalize_result_items($settings['latest_posts'] ?? array(), 'posts'),
            'latest_engagement' => vmo_promotion_normalize_result_items($settings['latest_engagement'] ?? array(), 'generic'),
        );
    }
}

if (!function_exists('vmo_promotion_normalize_queue')) {
    function vmo_promotion_normalize_queue($queue) {
        $queue = is_array($queue) ? array_values($queue) : array();
        $normalized = array();
        foreach ($queue as $item) {
            if (!is_array($item)) {
                continue;
            }
            $title = sanitize_text_field($item['title'] ?? '');
            if ($title === '') {
                continue;
            }
            $normalized[] = array(
                'id' => sanitize_text_field($item['id'] ?? wp_generate_uuid4()),
                'title' => $title,
                'status' => sanitize_text_field($item['status'] ?? 'Draft'),
                'time' => sanitize_text_field($item['time'] ?? 'Pending schedule'),
            );
            if (count($normalized) >= 20) {
                break;
            }
        }
        return $normalized;
    }
}

if (!function_exists('vmo_promotion_get_state')) {
    function vmo_promotion_get_state($user_id) {
        if (class_exists('VMO_Promotion_Orchestrator')) {
            return VMO_Promotion_Orchestrator::get_instance()->get_state($user_id);
        }

        $settings = get_user_meta($user_id, 'vmo_promotion_settings', true);
        $queue = get_user_meta($user_id, 'vmo_promotion_queue', true);
        return array(
            'settings' => vmo_promotion_normalize_settings($settings),
            'queue' => ($queue === '' || $queue === array()) ? vmo_promotion_default_queue() : vmo_promotion_normalize_queue($queue),
        );
    }
}

if (!function_exists('vmo_promotion_save_state')) {
    function vmo_promotion_save_state($user_id, $settings, $queue = null) {
        if (class_exists('VMO_Promotion_Orchestrator')) {
            $orchestrator = VMO_Promotion_Orchestrator::get_instance();
            if ($queue === null) {
                $existing = $orchestrator->get_state($user_id);
                $queue = $existing['queue'] ?? array();
            }
            return $orchestrator->save_state($user_id, is_array($settings) ? $settings : array(), is_array($queue) ? $queue : array());
        }

        $normalized_settings = vmo_promotion_normalize_settings($settings);
        update_user_meta($user_id, 'vmo_promotion_settings', $normalized_settings);
        if ($queue !== null) {
            $normalized_queue = vmo_promotion_normalize_queue($queue);
            update_user_meta($user_id, 'vmo_promotion_queue', $normalized_queue);
        } else {
            $normalized_queue = vmo_promotion_get_state($user_id)['queue'];
        }
        return array(
            'settings' => $normalized_settings,
            'queue' => $normalized_queue,
        );
    }
}

if (!function_exists('vmo_promotion_build_context')) {
    function vmo_promotion_build_context($user_id, array $settings) {
        if (class_exists('VMO_Promotion_Orchestrator')) {
            return VMO_Promotion_Orchestrator::get_instance()->build_context($user_id, $settings);
        }

        $profile = function_exists('vmo_get_consultation_profile') ? vmo_get_consultation_profile($user_id) : array();
        $insights = function_exists('vmo_get_cached_industry_insights') ? vmo_get_cached_industry_insights($user_id) : array();
        $business_name = sanitize_text_field($profile['business_name'] ?? (get_user_meta($user_id, 'business_name', true) ?: wp_get_current_user()->display_name ?: 'The business'));
        $industry = sanitize_text_field($profile['industry'] ?? (get_user_meta($user_id, 'business_type', true) ?: 'General'));
        $audience = sanitize_textarea_field($profile['target_audience'] ?? ($profile['ideal_customer'] ?? ''));
        $offers = $profile['core_offerings'] ?? ($profile['services'] ?? ($profile['products'] ?? array()));
        if (is_array($offers)) {
            $offers = implode(', ', array_map('sanitize_text_field', array_slice($offers, 0, 8)));
        }
        $location = sanitize_text_field($profile['location'] ?? ($profile['business_location'] ?? ''));
        $has_published_site = function_exists('vmo_user_has_published_site') ? vmo_user_has_published_site($user_id) : false;
        $has_payments = !empty(get_user_meta($user_id, 'vmo_pay_account_id', true));
        $insight_lines = array();
        if (is_array($insights)) {
            foreach (array('opportunities', 'threats', 'competitors') as $bucket) {
                foreach ((array) ($insights[$bucket] ?? array()) as $item) {
                    if (is_array($item)) {
                        $item = $item['title'] ?? $item['name'] ?? $item['summary'] ?? '';
                    }
                    $item = sanitize_text_field((string) $item);
                    if ($item !== '') {
                        $insight_lines[] = $item;
                    }
                    if (count($insight_lines) >= 6) {
                        break 2;
                    }
                }
            }
        }

        return array(
            'business_name' => $business_name !== '' ? $business_name : 'The business',
            'industry' => $industry,
            'audience' => $audience,
            'offers' => sanitize_textarea_field((string) $offers),
            'location' => $location,
            'has_published_site' => $has_published_site ? 'yes' : 'no',
            'has_payments' => $has_payments ? 'yes' : 'no',
            'insights' => implode('; ', $insight_lines),
            'platforms' => implode(', ', (array) ($settings['selected_platforms'] ?? array())),
            'goal' => sanitize_textarea_field($settings['goal'] ?? ''),
            'cta_destination' => sanitize_text_field($settings['cta_destination'] ?? ''),
            'cadence' => sanitize_text_field($settings['cadence'] ?? ''),
        );
    }
}

if (!function_exists('vmo_promotion_parse_ai_json')) {
    function vmo_promotion_parse_ai_json($content) {
        $content = trim((string) $content);
        if ($content === '') {
            return array();
        }
        $content = preg_replace('/^```(?:json)?\s*/i', '', $content);
        $content = preg_replace('/\s*```$/', '', $content);
        $decoded = json_decode($content, true);
        if (is_array($decoded)) {
            return $decoded;
        }
        if (preg_match('/\{.*\}/s', $content, $match)) {
            $decoded = json_decode($match[0], true);
            if (is_array($decoded)) {
                return $decoded;
            }
        }
        return array();
    }
}

if (!function_exists('vmo_promotion_charge_tokens')) {
    function vmo_promotion_charge_tokens($user_id, $operation, $fallback_cost, $description) {
        if (function_exists('vmo_deduct_tokens_for_operation')) {
            if (vmo_deduct_tokens_for_operation($user_id, $operation)) {
                return true;
            }
            return new WP_Error('insufficient_tokens', 'Insufficient tokens for this promotion request.', array(
                'tokens_required' => function_exists('vmo_get_token_cost') ? vmo_get_token_cost($operation) : $fallback_cost,
                'tokens_available' => function_exists('vmo_get_token_balance') ? vmo_get_token_balance($user_id) : 0,
            ));
        }
        if (function_exists('vmo_deduct_tokens')) {
            if (vmo_deduct_tokens($user_id, $fallback_cost, $operation, $description)) {
                return true;
            }
            return new WP_Error('insufficient_tokens', 'Insufficient tokens for this promotion request.', array(
                'tokens_required' => $fallback_cost,
                'tokens_available' => function_exists('vmo_get_token_balance') ? vmo_get_token_balance($user_id) : 0,
            ));
        }
        return true;
    }
}

if (!function_exists('vmo_promotion_ai_json_request')) {
    function vmo_promotion_ai_json_request($messages, $route = 'fast', $temperature = 0.5, $max_tokens = 1400) {
        if (!function_exists('vmo_openai_chat')) {
            return new WP_Error('ai_unavailable', 'AI request helper unavailable.');
        }
        $model = function_exists('vmo_get_model') ? vmo_get_model($route) : '';
        if ($model === '') {
            $model = ($route === 'strategic') ? 'gpt-5.5' : 'gpt-5.5-mini';
        }
        $response = vmo_openai_chat($messages, $model, array(
            'temperature' => $temperature,
            'max_tokens' => $max_tokens,
            'timeout' => 90,
        ));
        if (is_wp_error($response)) {
            return $response;
        }
        $payload = json_decode(wp_remote_retrieve_body($response), true);
        $content = trim((string) ($payload['choices'][0]['message']['content'] ?? ''));
        $decoded = vmo_promotion_parse_ai_json($content);
        if (!is_array($decoded) || empty($decoded)) {
            return new WP_Error('ai_invalid_json', 'AI returned an invalid structured response.');
        }
        return $decoded;
    }
}

if (!function_exists('vmo_promotion_success_payload')) {
    function vmo_promotion_success_payload($user_id, array $state) {
        $payload = array(
            'settings' => $state['settings'],
            'queue' => $state['queue'],
            'token_balance' => function_exists('vmo_get_token_balance') ? (int) vmo_get_token_balance($user_id) : (int) get_user_meta($user_id, 'vmo_token_balance', true),
            'feature_enabled' => vmo_promotion_feature_enabled(),
            'preview_only' => vmo_promotion_is_preview_only(),
            'preview_message' => vmo_promotion_preview_message(),
        );

        if (class_exists('VMO_Promotion_Orchestrator')) {
            $orchestrator = VMO_Promotion_Orchestrator::get_instance();
            $payload['provider_matrix'] = $orchestrator->get_provider_matrix($user_id);
            $payload['connected_accounts'] = $orchestrator->get_connected_accounts($user_id);
            $payload['campaign'] = $orchestrator->get_campaign_summary($user_id);
        }

        return $payload;
    }
}

if (!function_exists('vmo_promotion_get_provider_objects')) {
    function vmo_promotion_get_provider_objects($platform) {
        $registry = class_exists('VMO_Promotion_Provider_Registry') ? VMO_Promotion_Provider_Registry::get_instance() : null;
        $provider = $registry ? $registry->get_provider($platform) : array();
        $connector = $registry ? $registry->get_connector($platform) : null;
        $config = $registry ? $registry->get_provider_config($platform) : array();

        return array($registry, $provider, $connector, $config);
    }
}

function vmo_get_promotion_provider_capabilities() {
    vmo_promotion_require_access('read');
    $platform = sanitize_key($_POST['platform'] ?? '');
    list($registry, $provider, $connector, $config) = vmo_promotion_get_provider_objects($platform);

    if (!$registry || empty($provider) || !$connector) {
        wp_send_json_error(array('message' => 'Unknown promotion provider.'), 404);
    }

    wp_send_json_success(array(
        'platform_key' => sanitize_key($provider['platform_key'] ?? $platform),
        'label' => sanitize_text_field($provider['label'] ?? $platform),
        'category' => sanitize_text_field($provider['category'] ?? ''),
        'rollout_wave' => (int) ($provider['rollout_wave'] ?? 0),
        'capabilities' => $connector->get_capabilities(),
        'config' => $config,
    ));
}

function vmo_start_promotion_provider_connection() {
    vmo_promotion_require_live('read');
    $user_id = get_current_user_id();
    $platform = sanitize_key($_POST['platform'] ?? '');
    list($registry, $provider, $connector) = vmo_promotion_get_provider_objects($platform);

    if (!$registry || empty($provider) || !$connector) {
        wp_send_json_error(array('message' => 'Unknown promotion provider.'), 404);
    }

    $result = $connector->connect_account(array(
        'user_id' => $user_id,
        'platform_key' => $platform,
    ));

    if (is_wp_error($result)) {
        wp_send_json_error(array(
            'message' => $result->get_error_message(),
            'details' => $result->get_error_data(),
        ), 400);
    }

    wp_send_json_success($result);
}

function vmo_refresh_promotion_provider_auth() {
    vmo_promotion_require_live('read');
    $user_id = get_current_user_id();
    $platform = sanitize_key($_POST['platform'] ?? '');
    list($registry, $provider, $connector) = vmo_promotion_get_provider_objects($platform);

    if (!$registry || empty($provider) || !$connector) {
        wp_send_json_error(array('message' => 'Unknown promotion provider.'), 404);
    }

    $result = $connector->refresh_auth(array(
        'user_id' => $user_id,
        'platform_key' => $platform,
    ));

    if (is_wp_error($result)) {
        wp_send_json_error(array(
            'message' => $result->get_error_message(),
            'details' => $result->get_error_data(),
        ), 400);
    }

    wp_send_json_success($result);
}

function vmo_get_promotion_provider_accounts() {
    vmo_promotion_require_live('read');
    $user_id = get_current_user_id();
    $platform = sanitize_key($_POST['platform'] ?? '');
    list($registry, $provider, $connector) = vmo_promotion_get_provider_objects($platform);

    if (!$registry || empty($provider) || !$connector) {
        wp_send_json_error(array('message' => 'Unknown promotion provider.'), 404);
    }

    $result = $connector->list_accounts(array(
        'user_id' => $user_id,
        'platform_key' => $platform,
    ));

    if (is_wp_error($result)) {
        wp_send_json_error(array(
            'message' => $result->get_error_message(),
            'details' => $result->get_error_data(),
        ), 400);
    }

    wp_send_json_success($result);
}

function vmo_get_promotion_provider_ad_accounts() {
    vmo_promotion_require_live('read');
    $user_id = get_current_user_id();
    $platform = sanitize_key($_POST['platform'] ?? '');
    list($registry, $provider, $connector) = vmo_promotion_get_provider_objects($platform);

    if (!$registry || empty($provider) || !$connector) {
        wp_send_json_error(array('message' => 'Unknown promotion provider.'), 404);
    }

    $result = $connector->list_ad_accounts(array(
        'user_id' => $user_id,
        'platform_key' => $platform,
    ));

    if (is_wp_error($result)) {
        wp_send_json_error(array(
            'message' => $result->get_error_message(),
            'details' => $result->get_error_data(),
        ), 400);
    }

    wp_send_json_success($result);
}

function vmo_create_promotion_ad_draft() {
    vmo_promotion_require_live('read');
    $user_id = get_current_user_id();
    $platform = sanitize_key($_POST['platform'] ?? '');
    $draft = vmo_promotion_decode_json_request('draft', array());
    list($registry, $provider, $connector) = vmo_promotion_get_provider_objects($platform);

    if (!$registry || empty($provider) || !$connector) {
        wp_send_json_error(array('message' => 'Unknown promotion provider.'), 404);
    }

    $result = $connector->create_ad_draft(array(
        'user_id' => $user_id,
        'platform_key' => $platform,
        'draft' => $draft,
    ));

    if (is_wp_error($result)) {
        wp_send_json_error(array(
            'message' => $result->get_error_message(),
            'details' => $result->get_error_data(),
        ), 400);
    }

    $ad_job = array();
    if (class_exists('VMO_Promotion_Data_Store')) {
        $store = VMO_Promotion_Data_Store::get_instance();
        $ad_job = $store->queue_ad_job($user_id, array(
            'platform_key' => sanitize_key($provider['platform_key'] ?? $platform),
            'platform_label' => sanitize_text_field($provider['label'] ?? $platform),
            'ad_product' => sanitize_key($draft['ad_product'] ?? 'search_ads'),
            'objective' => sanitize_text_field($draft['objective'] ?? ''),
            'ad_account_id' => sanitize_text_field($draft['ad_account_id'] ?? ($result['ad_account_id'] ?? '')),
            'budget_amount' => (float) ($draft['budget_amount'] ?? 0),
            'currency' => sanitize_text_field($draft['currency'] ?? 'USD'),
            'schedule_start' => sanitize_text_field($draft['schedule_start'] ?? ''),
            'schedule_end' => sanitize_text_field($draft['schedule_end'] ?? ''),
            'job_status' => 'planned',
            'provider_campaign_id' => sanitize_text_field($result['provider_campaign_id'] ?? ''),
            'creative_payload' => (array) ($draft['creative_payload'] ?? array()),
            'job_payload' => $draft,
            'provider_response' => (array) ($result['response'] ?? array()),
        ));

        $store->log_event(
            $user_id,
            'promotion_ad_draft_created',
            sprintf('Ad draft created for %s.', sanitize_text_field($provider['label'] ?? $platform)),
            array(
                'platform_key' => sanitize_key($provider['platform_key'] ?? $platform),
                'provider_campaign_id' => sanitize_text_field($result['provider_campaign_id'] ?? ''),
                'ad_job_id' => (int) ($ad_job['id'] ?? 0),
            )
        );
    }

    wp_send_json_success(array(
        'platform_key' => sanitize_key($provider['platform_key'] ?? $platform),
        'provider_result' => $result,
        'ad_job' => $ad_job,
    ));
}

function vmo_run_promotion_jobs() {
    vmo_promotion_require_live('read');
    if (!class_exists('VMO_Promotion_Execution_Engine')) {
        wp_send_json_error(array('message' => 'Promotion execution engine unavailable.'), 500);
    }

    $limit = max(1, min(50, (int) ($_POST['limit'] ?? 10)));
    $result = VMO_Promotion_Execution_Engine::get_instance()->run_due_jobs($limit);
    wp_send_json_success($result);
}

function vmo_run_promotion_ad_jobs() {
    vmo_promotion_require_live('read');
    if (!class_exists('VMO_Promotion_Ad_Execution_Engine')) {
        wp_send_json_error(array('message' => 'Promotion ad execution engine unavailable.'), 500);
    }

    $limit = max(1, min(50, (int) ($_POST['limit'] ?? 10)));
    $result = VMO_Promotion_Ad_Execution_Engine::get_instance()->run_due_jobs($limit);
    wp_send_json_success($result);
}

function vmo_get_promotion_settings() {
    vmo_promotion_require_access('read');
    $user_id = get_current_user_id();
    wp_send_json_success(vmo_promotion_success_payload($user_id, vmo_promotion_get_state($user_id)));
}

function vmo_save_promotion_settings() {
    vmo_promotion_require_live('read');
    $user_id = get_current_user_id();
    $state = vmo_promotion_save_state(
        $user_id,
        vmo_promotion_decode_json_request('settings', array()),
        vmo_promotion_decode_json_request('queue', array())
    );
    wp_send_json_success(vmo_promotion_success_payload($user_id, $state));
}

function vmo_upload_promotion_asset() {
    vmo_promotion_require_live('upload_files');
    if (empty($_FILES['file'])) {
        wp_send_json_error(array('message' => 'No file uploaded'), 400);
    }

    require_once ABSPATH . 'wp-admin/includes/file.php';
    require_once ABSPATH . 'wp-admin/includes/media.php';
    require_once ABSPATH . 'wp-admin/includes/image.php';

    $uploaded = wp_handle_upload($_FILES['file'], array('test_form' => false));
    if (!empty($uploaded['error'])) {
        wp_send_json_error(array('message' => (string) $uploaded['error']), 400);
    }

    $attachment = array(
        'post_mime_type' => sanitize_text_field($uploaded['type'] ?? ''),
        'post_title' => sanitize_file_name($_FILES['file']['name'] ?? 'promotion-asset'),
        'post_content' => '',
        'post_status' => 'inherit',
    );
    $attachment_id = wp_insert_attachment($attachment, $uploaded['file']);
    if (is_wp_error($attachment_id) || !$attachment_id) {
        wp_send_json_error(array('message' => 'Unable to save uploaded asset.'), 500);
    }

    $attach_data = wp_generate_attachment_metadata($attachment_id, $uploaded['file']);
    wp_update_attachment_metadata($attachment_id, $attach_data);

    $user_id = get_current_user_id();
    $state = vmo_promotion_get_state($user_id);
    $settings = $state['settings'];
    $settings['manual_assets'][] = array(
        'id' => 'promo_asset_' . $attachment_id,
        'name' => sanitize_file_name($_FILES['file']['name'] ?? basename($uploaded['file'])),
        'url' => esc_url_raw($uploaded['url'] ?? wp_get_attachment_url($attachment_id)),
        'kind' => sanitize_key($_POST['asset_kind'] ?? 'asset'),
        'type' => sanitize_text_field($uploaded['type'] ?? ''),
    );
    $state = vmo_promotion_save_state($user_id, $settings, $state['queue']);
    wp_send_json_success(vmo_promotion_success_payload($user_id, $state));
}

function vmo_generate_promotion_strategy() {
    vmo_promotion_require_live('read');
    $user_id = get_current_user_id();
    $token_check = vmo_promotion_charge_tokens($user_id, 'chat', 250, 'Promotion strategy generation');
    if (is_wp_error($token_check)) {
        wp_send_json_error(array(
            'message' => $token_check->get_error_message(),
            'tokens_required' => $token_check->get_error_data()['tokens_required'] ?? 0,
            'tokens_available' => $token_check->get_error_data()['tokens_available'] ?? 0,
        ), 402);
    }

    $incoming_settings = vmo_promotion_decode_json_request('settings', array());
    $incoming_queue = vmo_promotion_decode_json_request('queue', array());
    $state = vmo_promotion_save_state($user_id, $incoming_settings, $incoming_queue);
    $settings = $state['settings'];
    $context = vmo_promotion_build_context($user_id, $settings);

    $messages = array(
        array(
            'role' => 'system',
            'content' => 'You are VMO\'s social media promotion strategist. Return JSON only with keys: cards (array of 4-6 objects with title and text), recommended_platforms (array of platform strings), cadence (string), budget_note (string), queue_suggestions (array of up to 3 objects with title, status, time). Keep the strategy practical, AI-first, and execution-oriented.',
        ),
        array(
            'role' => 'user',
            'content' => "Business name: {$context['business_name']}\nIndustry: {$context['industry']}\nAudience: {$context['audience']}\nOffers: {$context['offers']}\nLocation: {$context['location']}\nGoal: {$context['goal']}\nSelected platforms: {$context['platforms']}\nPreferred cadence: {$context['cadence']}\nCTA destination: {$context['cta_destination']}\nPublished website available: {$context['has_published_site']}\nPayments active: {$context['has_payments']}\nMarket insights: {$context['insights']}\n\nBuild a focused social posting strategy for the live VMO Promotion Command Center.",
        ),
    );

    $result = vmo_promotion_ai_json_request($messages, 'strategic', 0.45, 1500);
    if (is_wp_error($result)) {
        $fallback_cards = array(
            array('title' => 'Campaign focus', 'text' => 'Lead with your highest-value offer and turn social posts into a repeatable visibility engine instead of one-off announcements.'),
            array('title' => 'Platform priority', 'text' => 'Use the currently selected platforms first, then expand only after post quality and posting rhythm are stable.'),
            array('title' => 'Engagement layer', 'text' => 'Alternate direct promotional posts with questions, social proof, and educational content so the feed does not become one-dimensional.'),
            array('title' => 'Conversion path', 'text' => 'Match each post to the chosen CTA path so product-driven content consistently points people toward the website, store, or checkout route you want to grow.'),
        );
        $settings['latest_strategy'] = $fallback_cards;
        $state = vmo_promotion_save_state($user_id, $settings, $state['queue']);
        wp_send_json_success(vmo_promotion_success_payload($user_id, $state));
    }

    $settings['latest_strategy'] = vmo_promotion_normalize_result_items($result['cards'] ?? array(), 'strategy');
    if (!empty($result['cadence'])) {
        $settings['cadence'] = sanitize_text_field($result['cadence']);
    }
    $recommended = array_values(array_intersect((array) ($result['recommended_platforms'] ?? array()), vmo_promotion_platforms()));
    if (!empty($recommended) && empty($settings['selected_platforms'])) {
        $settings['selected_platforms'] = array_slice($recommended, 0, 6);
    }
    if (empty($state['queue']) && !empty($result['queue_suggestions'])) {
        $state['queue'] = vmo_promotion_normalize_queue($result['queue_suggestions']);
    }
    $state = vmo_promotion_save_state($user_id, $settings, $state['queue']);
    wp_send_json_success(vmo_promotion_success_payload($user_id, $state));
}

function vmo_generate_promotion_posts() {
    vmo_promotion_require_live('read');
    $user_id = get_current_user_id();
    $token_check = vmo_promotion_charge_tokens($user_id, 'content', 350, 'Promotion post generation');
    if (is_wp_error($token_check)) {
        wp_send_json_error(array(
            'message' => $token_check->get_error_message(),
            'tokens_required' => $token_check->get_error_data()['tokens_required'] ?? 0,
            'tokens_available' => $token_check->get_error_data()['tokens_available'] ?? 0,
        ), 402);
    }

    $post_type = sanitize_key($_POST['post_type'] ?? 'general');
    $incoming_settings = vmo_promotion_decode_json_request('settings', array());
    $incoming_queue = vmo_promotion_decode_json_request('queue', array());
    $state = vmo_promotion_save_state($user_id, $incoming_settings, $incoming_queue);
    $settings = $state['settings'];
    $context = vmo_promotion_build_context($user_id, $settings);
    $count = ($post_type === 'weekly_plan') ? 7 : 3;

    $messages = array(
        array(
            'role' => 'system',
            'content' => 'You create social media draft packages for VMO. Return JSON only with key items. Each item must contain title, caption, cta, and asset_hint. Keep captions platform-aware, concise, and ready for approval.',
        ),
        array(
            'role' => 'user',
            'content' => "Business name: {$context['business_name']}\nIndustry: {$context['industry']}\nAudience: {$context['audience']}\nOffers: {$context['offers']}\nGoal: {$context['goal']}\nSelected platforms: {$context['platforms']}\nCTA destination: {$context['cta_destination']}\nPosting cadence: {$context['cadence']}\nPost request type: {$post_type}\nDraft count needed: {$count}\n\nGenerate the requested social media drafts now.",
        ),
    );

    $result = vmo_promotion_ai_json_request($messages, 'fast', 0.6, 1600);
    if (is_wp_error($result)) {
        $fallback_posts = array();
        for ($i = 1; $i <= $count; $i++) {
            $fallback_posts[] = array(
                'title' => ucfirst(str_replace('_', ' ', $post_type)) . ' Draft ' . $i,
                'caption' => 'Highlight a business win, explain the value clearly, and end with a direct next step that matches your chosen CTA path.',
                'cta' => 'Invite the audience to learn more through the preferred conversion path.',
                'asset_hint' => 'Use a clean product, service, or behind-the-scenes visual.',
            );
        }
        $settings['latest_posts'] = vmo_promotion_normalize_result_items($fallback_posts, 'posts');
        $state = vmo_promotion_save_state($user_id, $settings, $state['queue']);
        wp_send_json_success(vmo_promotion_success_payload($user_id, $state));
    }

    $settings['latest_posts'] = vmo_promotion_normalize_result_items($result['items'] ?? array(), 'posts');
    $state = vmo_promotion_save_state($user_id, $settings, $state['queue']);
    wp_send_json_success(vmo_promotion_success_payload($user_id, $state));
}

function vmo_refine_promotion_posts() {
    vmo_promotion_require_live('read');
    $user_id = get_current_user_id();
    $token_check = vmo_promotion_charge_tokens($user_id, 'refinement', 300, 'Promotion post refinement');
    if (is_wp_error($token_check)) {
        wp_send_json_error(array(
            'message' => $token_check->get_error_message(),
            'tokens_required' => $token_check->get_error_data()['tokens_required'] ?? 0,
            'tokens_available' => $token_check->get_error_data()['tokens_available'] ?? 0,
        ), 402);
    }

    $instruction = sanitize_textarea_field($_POST['instruction'] ?? '');
    $incoming_settings = vmo_promotion_decode_json_request('settings', array());
    $incoming_queue = vmo_promotion_decode_json_request('queue', array());
    $state = vmo_promotion_save_state($user_id, $incoming_settings, $incoming_queue);
    $settings = $state['settings'];
    $current_posts = $settings['latest_posts'] ?? array();
    if (empty($current_posts)) {
        wp_send_json_error(array('message' => 'Generate post drafts first before refining them.'), 400);
    }

    $context = vmo_promotion_build_context($user_id, $settings);
    $messages = array(
        array(
            'role' => 'system',
            'content' => 'You refine social media draft packages for VMO. Return JSON only with key items. Each item must contain title, caption, cta, and asset_hint.',
        ),
        array(
            'role' => 'user',
            'content' => "Business name: {$context['business_name']}\nIndustry: {$context['industry']}\nSelected platforms: {$context['platforms']}\nInstruction: {$instruction}\nCurrent drafts:\n" . wp_json_encode($current_posts),
        ),
    );

    $result = vmo_promotion_ai_json_request($messages, 'fast', 0.55, 1600);
    if (is_wp_error($result)) {
        wp_send_json_error(array('message' => 'Unable to refine the current post drafts right now.'), 500);
    }

    $settings['latest_posts'] = vmo_promotion_normalize_result_items($result['items'] ?? array(), 'posts');
    $state = vmo_promotion_save_state($user_id, $settings, $state['queue']);
    wp_send_json_success(vmo_promotion_success_payload($user_id, $state));
}

function vmo_generate_promotion_engagement() {
    vmo_promotion_require_live('read');
    $user_id = get_current_user_id();
    $token_check = vmo_promotion_charge_tokens($user_id, 'chat', 220, 'Promotion engagement guidance');
    if (is_wp_error($token_check)) {
        wp_send_json_error(array(
            'message' => $token_check->get_error_message(),
            'tokens_required' => $token_check->get_error_data()['tokens_required'] ?? 0,
            'tokens_available' => $token_check->get_error_data()['tokens_available'] ?? 0,
        ), 402);
    }

    $engagement_type = sanitize_key($_POST['engagement_type'] ?? 'questions');
    $incoming_settings = vmo_promotion_decode_json_request('settings', array());
    $incoming_queue = vmo_promotion_decode_json_request('queue', array());
    $state = vmo_promotion_save_state($user_id, $incoming_settings, $incoming_queue);
    $settings = $state['settings'];
    $context = vmo_promotion_build_context($user_id, $settings);

    $messages = array(
        array(
            'role' => 'system',
            'content' => 'You create engagement ideas for VMO social posting. Return JSON only with key items. Each item must contain title and text.',
        ),
        array(
            'role' => 'user',
            'content' => "Business name: {$context['business_name']}\nIndustry: {$context['industry']}\nAudience: {$context['audience']}\nOffers: {$context['offers']}\nSelected platforms: {$context['platforms']}\nEngagement request type: {$engagement_type}\nGoal: {$context['goal']}\n\nGenerate concise, usable engagement guidance.",
        ),
    );

    $result = vmo_promotion_ai_json_request($messages, 'fast', 0.6, 1200);
    if (is_wp_error($result)) {
        $fallback = array(
            array('title' => 'Engagement prompt', 'text' => 'Ask a practical question that invites your audience to describe their biggest challenge in this area.'),
            array('title' => 'Growth move', 'text' => 'Alternate promotional posts with useful, discussion-oriented content so the audience has a reason to respond instead of just scroll.'),
            array('title' => 'Reply guidance', 'text' => 'Keep replies warm, direct, and business-aware so they move the conversation toward the next relevant step.'),
        );
        $settings['latest_engagement'] = vmo_promotion_normalize_result_items($fallback, 'generic');
        $state = vmo_promotion_save_state($user_id, $settings, $state['queue']);
        wp_send_json_success(vmo_promotion_success_payload($user_id, $state));
    }

    $settings['latest_engagement'] = vmo_promotion_normalize_result_items($result['items'] ?? array(), 'generic');
    $state = vmo_promotion_save_state($user_id, $settings, $state['queue']);
    wp_send_json_success(vmo_promotion_success_payload($user_id, $state));
}

// Website generation alias
add_action('wp_ajax_vmo_generate_website', function() {
    if (class_exists('VMO_Enhanced_Business_Wizard')) {
        $wizard = VMO_Enhanced_Business_Wizard::get_instance();
        if (method_exists($wizard, 'ajax_generate_business_website_enhanced')) {
            $wizard->ajax_generate_business_website_enhanced();
            return;
        }
    }
    wp_send_json_error(['message' => 'Website generation handler not available'], 500);
});

// Operation cost preview — powers the rerun-warning modal. Returns the token
// cost the user will actually be charged for an operation (honoring the
// one-time initial-setup discount) plus their balance, so the UI can warn
// before re-running a previously-discounted setup operation at full price.
add_action('wp_ajax_vmo_get_operation_cost', function() {
    if (!is_user_logged_in()) {
        wp_send_json_error(['message' => 'Not authenticated'], 403);
    }
    check_ajax_referer('vmo_builder_nonce', 'security');

    $operation = isset($_POST['operation']) ? sanitize_key(wp_unslash($_POST['operation'])) : '';
    if ($operation === '') {
        wp_send_json_error(['message' => 'Missing operation key'], 400);
    }

    $user_id = get_current_user_id();

    if (function_exists('vmo_get_operation_charge_breakdown')) {
        wp_send_json_success(vmo_get_operation_charge_breakdown($user_id, $operation));
    }

    // Fallback if the setup policy is unavailable.
    $full = function_exists('vmo_get_token_cost') ? (int) vmo_get_token_cost($operation) : 0;
    $balance = function_exists('vmo_get_token_balance') ? (int) vmo_get_token_balance($user_id) : 0;
    wp_send_json_success([
        'operation'      => $operation,
        'full_tokens'    => $full,
        'charged_tokens' => $full,
        'discount_tokens'=> 0,
        'balance_tokens' => $balance,
        'sufficient'     => $balance >= $full,
    ]);
});

// Dashboard stats alias
add_action('wp_ajax_vmo_get_dashboard_stats', function() {
    if (class_exists('VMO_Enhanced_Business_Wizard')) {
        $wizard = VMO_Enhanced_Business_Wizard::get_instance();
        if (method_exists($wizard, 'ajax_get_dashboard_stats')) {
            $wizard->ajax_get_dashboard_stats();
            return;
        }
    }
    wp_send_json_error(['message' => 'Dashboard stats handler not available'], 500);
});

// SEO analysis alias
add_action('wp_ajax_vmo_run_seo_analysis', function() {
    if (class_exists('VMO_SEO_Engine')) {
        try {
            $seo = new VMO_SEO_Engine();
            if (method_exists($seo, 'ajax_run_analysis')) {
                $seo->ajax_run_analysis();
                return;
            }
        } catch (Throwable $e) {
            wp_send_json_error(['message' => $e->getMessage()], 500);
        }
    }
    wp_send_json_error(['message' => 'SEO engine not available'], 500);
});

/* ═══════════════════════════════════════════════════════════════
 *  STRATEGIC DASHBOARD, REPORTS, ACTIONS, CFO EXPANSION HANDLERS
 * ═══════════════════════════════════════════════════════════════ */

// ─── Strategic Dashboard data ───
add_action('wp_ajax_vmo_get_strategic_data', function() {
    if (!current_user_can('read')) wp_send_json_error(['message' => 'Unauthorized'], 403);
    $user_id = get_current_user_id();
    global $wpdb;

    // Vision progress from roadmap
    $roadmap = get_user_meta($user_id, 'vmo_business_roadmap', true);
    $vision_label = '';
    $vision_progress = 0;
    if (!empty($roadmap)) {
        if (is_string($roadmap)) $roadmap = json_decode($roadmap, true);
        $vision_label = isset($roadmap['vision']) ? $roadmap['vision'] : '';
        $total = 0; $done = 0;
        if (!empty($roadmap['milestones'])) {
            foreach ($roadmap['milestones'] as $m) {
                $total++;
                if (!empty($m['completed'])) $done++;
            }
        }
        if ($total > 0) $vision_progress = round(($done / $total) * 100);
    }

    // KPIs from KPI dashboard
    $kpis = [];
    if (class_exists('VMO_KPI_Dashboard')) {
        $kpi_dash = new VMO_KPI_Dashboard();
        $all_kpis = method_exists($kpi_dash, 'get_dashboard_kpis') ? $kpi_dash->get_dashboard_kpis($user_id) : [];
        if (!empty($all_kpis)) {
            foreach ($all_kpis as $k) {
                $cat = isset($k['category']) ? $k['category'] : 'general';
                if (!isset($kpis[$cat])) $kpis[$cat] = [];
                $kpis[$cat][] = ['label' => $k['label'], 'value' => $k['value']];
            }
        }
    }

    // Business intelligence for strategic position
    $bi = get_user_meta($user_id, 'vmo_business_intelligence', true);
    if (is_string($bi) && !empty($bi)) $bi = json_decode($bi, true);
    $market_position = !empty($bi['market_position']) ? $bi['market_position'] : 'Establishing';
    $growth_rate = !empty($bi['growth_rate']) ? $bi['growth_rate'] : '—';

    // Competitor data
    $competitor_movement = [];
    $competitors = get_user_meta($user_id, 'vmo_competitors_data', true);
    if (!empty($competitors) && is_array($competitors)) {
        foreach (array_slice($competitors, 0, 5) as $c) {
            $competitor_movement[] = isset($c['name']) ? $c['name'] . ': ' . (isset($c['change']) ? $c['change'] : 'stable') : '';
        }
    }

    // Tasks-based health
    $table = $wpdb->prefix . 'vmo_follow_ups';
    $total_tasks = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$table} WHERE user_id = %d", $user_id));
    $completed_tasks = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$table} WHERE user_id = %d AND status = 'completed'", $user_id));
    $health_score = $total_tasks > 0 ? round(($completed_tasks / $total_tasks) * 100) . '%' : '—';

    // Recent important changes (last 7 days of activity)
    $recent_changes = [];
    $recent = $wpdb->get_results($wpdb->prepare(
        "SELECT action_type, description, created_at FROM {$wpdb->prefix}vmo_activity_log WHERE user_id = %d AND created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY) ORDER BY created_at DESC LIMIT 10",
        $user_id
    ));
    if ($recent) {
        foreach ($recent as $r) {
            $recent_changes[] = $r->description . ' (' . human_time_diff(strtotime($r->created_at)) . ' ago)';
        }
    }

    // Approval queue
    $approvals = [];
    $pending = $wpdb->get_results($wpdb->prepare(
        "SELECT id, title, created_at FROM {$wpdb->prefix}vmo_approvals WHERE user_id = %d AND status = 'pending' ORDER BY created_at DESC LIMIT 10",
        $user_id
    ));
    if ($pending) {
        foreach ($pending as $p) {
            $approvals[] = $p->title;
        }
    }

    // Build strategic interpretation narrative
    $interpretation = '';
    if ($vision_progress > 0) {
        if ($vision_progress >= 75) $interpretation .= "Strong progress toward vision — {$vision_progress}% of milestones completed. ";
        elseif ($vision_progress >= 40) $interpretation .= "Moderate progress — {$vision_progress}% of milestones completed; maintain momentum. ";
        else $interpretation .= "Early stage — {$vision_progress}% of milestones completed; accelerate execution to stay on track. ";
    }
    if ($total_tasks > 0) {
        $task_pct = round(($completed_tasks / $total_tasks) * 100);
        if ($task_pct >= 80) $interpretation .= "Task completion rate ({$task_pct}%) is excellent. ";
        elseif ($task_pct >= 50) $interpretation .= "Task completion rate ({$task_pct}%) is satisfactory but has room for improvement. ";
        else $interpretation .= "Task completion rate ({$task_pct}%) needs attention — consider prioritizing overdue items. ";
    }
    if (!empty($competitor_movement)) $interpretation .= count($competitor_movement) . " competitor(s) tracked. ";
    if (!empty($approvals)) $interpretation .= count($approvals) . " item(s) awaiting your approval. ";
    if (empty($interpretation)) $interpretation = 'Build your business data to unlock strategic insights.';

    wp_send_json_success([
        'vision_progress'       => $vision_progress,
        'vision_label'          => $vision_label,
        'market_position'       => $market_position,
        'growth_rate'           => $growth_rate,
        'health_score'          => $health_score,
        'competitive_index'     => !empty($bi['competitive_index']) ? $bi['competitive_index'] : '—',
        'readiness_score'       => !empty($bi['readiness_score']) ? $bi['readiness_score'] : '—',
        'opportunities'         => !empty($bi['opportunities']) ? (array) $bi['opportunities'] : [],
        'risks'                 => !empty($bi['risks']) ? (array) $bi['risks'] : [],
        'competitor_movement'   => $competitor_movement,
        'kpis'                  => $kpis,
        'strategic_actions'     => !empty($bi['strategic_actions']) ? (array) $bi['strategic_actions'] : [],
        'approval_queue'        => $approvals,
        'recent_changes'        => $recent_changes,
        'forecast'              => !empty($bi['forecast']) ? $bi['forecast'] : 'Build your business data to unlock AI forecasting.',
        'interpretation'        => $interpretation,
    ]);
});

// ─── Reports data ───
add_action('wp_ajax_vmo_get_reports_data', function() {
    if (!current_user_can('read')) wp_send_json_error(['message' => 'Unauthorized'], 403);
    $user_id = get_current_user_id();
    $type = isset($_POST['report_type']) ? sanitize_text_field($_POST['report_type']) : 'action-items';
    global $wpdb;

    $html = '';
    switch ($type) {
        case 'action-items':
            $items = $wpdb->get_results($wpdb->prepare(
                "SELECT * FROM {$wpdb->prefix}vmo_follow_ups WHERE user_id = %d AND status != 'completed' ORDER BY priority DESC, due_date ASC LIMIT 20",
                $user_id
            ));
            if ($items) {
                $html .= '<table style="width:100%;border-collapse:collapse;font-size:12px;"><thead><tr style="border-bottom:1px solid rgba(255,255,255,.1);"><th style="padding:6px 8px;text-align:left;">Task</th><th style="padding:6px 8px;">Priority</th><th style="padding:6px 8px;">Due</th><th style="padding:6px 8px;">Status</th></tr></thead><tbody>';
                foreach ($items as $item) {
                    $html .= '<tr style="border-bottom:1px solid rgba(255,255,255,.04);">';
                    $html .= '<td style="padding:6px 8px;">' . esc_html($item->description ?? $item->title ?? '—') . '</td>';
                    $html .= '<td style="padding:6px 8px;text-align:center;">' . esc_html($item->priority ?? '—') . '</td>';
                    $html .= '<td style="padding:6px 8px;text-align:center;">' . esc_html($item->due_date ?? '—') . '</td>';
                    $html .= '<td style="padding:6px 8px;text-align:center;">' . esc_html($item->status ?? 'pending') . '</td>';
                    $html .= '</tr>';
                }
                $html .= '</tbody></table>';
            }
            break;

        case 'calls':
            $calls = $wpdb->get_results($wpdb->prepare(
                "SELECT * FROM {$wpdb->prefix}vmo_call_logs WHERE user_id = %d ORDER BY created_at DESC LIMIT 30",
                $user_id
            ));
            if ($calls) {
                $html .= '<table style="width:100%;border-collapse:collapse;font-size:12px;"><thead><tr style="border-bottom:1px solid rgba(255,255,255,.1);"><th style="padding:6px 8px;text-align:left;">Date</th><th style="padding:6px 8px;">Direction</th><th style="padding:6px 8px;">Number</th><th style="padding:6px 8px;">Duration</th><th style="padding:6px 8px;">Status</th></tr></thead><tbody>';
                foreach ($calls as $c) {
                    $html .= '<tr style="border-bottom:1px solid rgba(255,255,255,.04);">';
                    $html .= '<td style="padding:6px 8px;">' . esc_html($c->created_at ?? '—') . '</td>';
                    $html .= '<td style="padding:6px 8px;text-align:center;">' . esc_html($c->direction ?? '—') . '</td>';
                    $html .= '<td style="padding:6px 8px;">' . esc_html($c->phone_number ?? '—') . '</td>';
                    $html .= '<td style="padding:6px 8px;text-align:center;">' . esc_html($c->duration ?? '0') . 's</td>';
                    $html .= '<td style="padding:6px 8px;text-align:center;">' . esc_html($c->status ?? '—') . '</td>';
                    $html .= '</tr>';
                }
                $html .= '</tbody></table>';
            }
            break;

        case 'email':
            $emails = $wpdb->get_results($wpdb->prepare(
                "SELECT * FROM {$wpdb->prefix}vmo_emails WHERE user_id = %d ORDER BY sent_at DESC LIMIT 30",
                $user_id
            ));
            if ($emails) {
                $html .= '<table style="width:100%;border-collapse:collapse;font-size:12px;"><thead><tr style="border-bottom:1px solid rgba(255,255,255,.1);"><th style="padding:6px 8px;text-align:left;">Date</th><th style="padding:6px 8px;">To</th><th style="padding:6px 8px;">Subject</th><th style="padding:6px 8px;">Status</th></tr></thead><tbody>';
                foreach ($emails as $e) {
                    $html .= '<tr style="border-bottom:1px solid rgba(255,255,255,.04);">';
                    $html .= '<td style="padding:6px 8px;">' . esc_html($e->sent_at ?? '—') . '</td>';
                    $html .= '<td style="padding:6px 8px;">' . esc_html($e->recipient ?? '—') . '</td>';
                    $html .= '<td style="padding:6px 8px;">' . esc_html($e->subject ?? '—') . '</td>';
                    $html .= '<td style="padding:6px 8px;text-align:center;">' . esc_html($e->status ?? '—') . '</td>';
                    $html .= '</tr>';
                }
                $html .= '</tbody></table>';
            }
            break;

        case 'sms':
            $sms = $wpdb->get_results($wpdb->prepare(
                "SELECT * FROM {$wpdb->prefix}vmo_sms_logs WHERE user_id = %d ORDER BY created_at DESC LIMIT 30",
                $user_id
            ));
            if ($sms) {
                $html .= '<table style="width:100%;border-collapse:collapse;font-size:12px;"><thead><tr style="border-bottom:1px solid rgba(255,255,255,.1);"><th style="padding:6px 8px;text-align:left;">Date</th><th style="padding:6px 8px;">Direction</th><th style="padding:6px 8px;">Number</th><th style="padding:6px 8px;">Message</th></tr></thead><tbody>';
                foreach ($sms as $s) {
                    $html .= '<tr style="border-bottom:1px solid rgba(255,255,255,.04);">';
                    $html .= '<td style="padding:6px 8px;">' . esc_html($s->created_at ?? '—') . '</td>';
                    $html .= '<td style="padding:6px 8px;text-align:center;">' . esc_html($s->direction ?? '—') . '</td>';
                    $html .= '<td style="padding:6px 8px;">' . esc_html($s->phone_number ?? '—') . '</td>';
                    $html .= '<td style="padding:6px 8px;">' . esc_html(wp_trim_words($s->body ?? '', 15)) . '</td>';
                    $html .= '</tr>';
                }
                $html .= '</tbody></table>';
            }
            break;

        case 'daily':
            $reports = $wpdb->get_results($wpdb->prepare(
                "SELECT * FROM {$wpdb->prefix}vmo_daily_reports WHERE user_id = %d ORDER BY report_date DESC LIMIT 14",
                $user_id
            ));
            if ($reports) {
                $html .= '<div style="display:grid;gap:10px;">';
                foreach ($reports as $rpt) {
                    $html .= '<div style="padding:10px 12px;background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.06);border-radius:8px;">';
                    $html .= '<strong>' . esc_html($rpt->report_date ?? '—') . '</strong>';
                    $html .= '<div style="margin-top:4px;font-size:12px;color:var(--muted);">' . esc_html($rpt->summary ?? $rpt->content ?? 'No summary') . '</div>';
                    if (!empty($rpt->completion_rate)) {
                        $html .= '<div style="margin-top:4px;font-size:11px;">Completion: <strong>' . esc_html($rpt->completion_rate) . '%</strong></div>';
                    }
                    $html .= '</div>';
                }
                $html .= '</div>';
            }
            break;

        case 'strategic':
            $bi = get_user_meta($user_id, 'vmo_business_intelligence', true);
            if (is_string($bi)) $bi = json_decode($bi, true);
            if (!empty($bi)) {
                $html .= '<div style="padding:12px;font-size:13px;line-height:1.6;">';
                if (!empty($bi['strategic_summary'])) {
                    $html .= '<h4 style="margin-bottom:8px;">Strategic Summary</h4>';
                    $html .= '<p>' . esc_html($bi['strategic_summary']) . '</p>';
                }
                if (!empty($bi['market_analysis'])) {
                    $html .= '<h4 style="margin:12px 0 8px;">Market Analysis</h4>';
                    $html .= '<p>' . esc_html($bi['market_analysis']) . '</p>';
                }
                $html .= '</div>';
            }
            break;

        case 'milestones':
            $roadmap = get_user_meta($user_id, 'vmo_business_roadmap', true);
            if (is_string($roadmap)) $roadmap = json_decode($roadmap, true);
            if (!empty($roadmap['milestones'])) {
                $html .= '<div style="display:grid;gap:8px;">';
                foreach ($roadmap['milestones'] as $idx => $m) {
                    $status = !empty($m['completed']) ? '✅' : '⬜';
                    $html .= '<div style="display:flex;align-items:center;gap:8px;padding:8px 10px;background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.06);border-radius:8px;">';
                    $html .= '<span>' . $status . '</span>';
                    $html .= '<span style="flex:1;font-size:13px;">' . esc_html($m['title'] ?? $m['name'] ?? 'Milestone ' . ($idx + 1)) . '</span>';
                    if (!empty($m['target_date'])) {
                        $html .= '<span style="font-size:11px;color:var(--muted);">' . esc_html($m['target_date']) . '</span>';
                    }
                    $html .= '</div>';
                }
                $html .= '</div>';
            }
            break;
    }

    if (empty($html)) {
        $html = '<p style="color:var(--muted);padding:12px;">No ' . esc_html(str_replace('-', ' ', $type)) . ' reports available yet. As your business grows, reports will appear here automatically.</p>';
    }

    wp_send_json_success(['html' => $html]);
});

// ─── Actions data (ad hoc from phone/email/SMS/CRM) ───
add_action('wp_ajax_vmo_get_actions_data', function() {
    if (!current_user_can('read')) wp_send_json_error(['message' => 'Unauthorized'], 403);
    $user_id = get_current_user_id();
    global $wpdb;

    $actions = [];
    $table = $wpdb->prefix . 'vmo_actions';
    $table_exists = $wpdb->get_var("SHOW TABLES LIKE '{$table}'") === $table;

    if ($table_exists) {
        $rows = $wpdb->get_results($wpdb->prepare(
            "SELECT id, title, source, status, created_at FROM {$table} WHERE user_id = %d AND status = 'pending' ORDER BY created_at DESC LIMIT 30",
            $user_id
        ));
        if ($rows) {
            foreach ($rows as $row) {
                $actions[] = ['id' => $row->id, 'title' => $row->title, 'source' => $row->source ?? 'manual'];
            }
        }
    }

    wp_send_json_success(['actions' => $actions]);
});

// ─── Add manual action ───
add_action('wp_ajax_vmo_add_manual_action', function() {
    if (!current_user_can('read')) wp_send_json_error(['message' => 'Unauthorized'], 403);
    $user_id = get_current_user_id();
    $title = isset($_POST['title']) ? sanitize_text_field($_POST['title']) : '';
    if (empty($title)) wp_send_json_error(['message' => 'Title required']);

    global $wpdb;
    $table = $wpdb->prefix . 'vmo_actions';

    // Ensure table exists
    $charset = $wpdb->get_charset_collate();
    $wpdb->query("CREATE TABLE IF NOT EXISTS {$table} (
        id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
        user_id BIGINT UNSIGNED NOT NULL,
        title VARCHAR(500) NOT NULL,
        source VARCHAR(50) DEFAULT 'manual',
        status VARCHAR(20) DEFAULT 'pending',
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        completed_at DATETIME NULL,
        INDEX idx_user_status (user_id, status)
    ) {$charset}");

    $wpdb->insert($table, [
        'user_id'    => $user_id,
        'title'      => $title,
        'source'     => 'manual',
        'status'     => 'pending',
        'created_at' => current_time('mysql'),
    ]);

    wp_send_json_success(['message' => 'Action added', 'id' => $wpdb->insert_id]);
});

// ─── Complete action ───
add_action('wp_ajax_vmo_complete_action', function() {
    if (!current_user_can('read')) wp_send_json_error(['message' => 'Unauthorized'], 403);
    $user_id = get_current_user_id();
    $action_id = isset($_POST['action_id']) ? (int) $_POST['action_id'] : 0;
    if ($action_id <= 0) wp_send_json_error(['message' => 'Invalid action ID']);

    global $wpdb;
    $table = $wpdb->prefix . 'vmo_actions';
    $wpdb->update($table, [
        'status'       => 'completed',
        'completed_at' => current_time('mysql'),
    ], ['id' => $action_id, 'user_id' => $user_id]);

    wp_send_json_success(['message' => 'Action completed']);
});

// ─── Complete a daily-activity task (mark done) ───
add_action('wp_ajax_vmo_complete_today_task', function() {
    if (!current_user_can('read')) wp_send_json_error(['message' => 'Unauthorized'], 403);
    global $wpdb;
    $user_id  = get_current_user_id();
    $task_id  = sanitize_text_field(wp_unslash($_POST['task_id'] ?? ''));
    if (empty($task_id)) wp_send_json_error(['message' => 'Missing task_id']);

    $table = $wpdb->prefix . 'vmo_daily_activities';
    $updated = $wpdb->update($table, [
        'status'            => 'completed',
        'actual_completed_at' => current_time('mysql'),
        'updated_at'        => current_time('mysql'),
    ], ['activity_id' => $task_id, 'user_id' => $user_id]);

    if ($updated === false) wp_send_json_error(['message' => 'Update failed']);
    wp_send_json_success(['message' => 'Task completed']);
});

// ─── Unmark (reopen) a daily-activity task ───
add_action('wp_ajax_vmo_unmark_today_task', function() {
    if (!current_user_can('read')) wp_send_json_error(['message' => 'Unauthorized'], 403);
    global $wpdb;
    $user_id = get_current_user_id();
    $task_id = sanitize_text_field(wp_unslash($_POST['task_id'] ?? ''));
    if (empty($task_id)) wp_send_json_error(['message' => 'Missing task_id']);

    $table = $wpdb->prefix . 'vmo_daily_activities';
    $wpdb->update($table, [
        'status'              => 'pending',
        'actual_completed_at' => null,
        'updated_at'          => current_time('mysql'),
    ], ['activity_id' => $task_id, 'user_id' => $user_id]);

    wp_send_json_success(['message' => 'Task unmarked']);
});

// ─── Defer a daily-activity task to the next day ───
add_action('wp_ajax_vmo_defer_today_task', function() {
    if (!current_user_can('read')) wp_send_json_error(['message' => 'Unauthorized'], 403);
    global $wpdb;
    $user_id = get_current_user_id();
    $task_id = sanitize_text_field(wp_unslash($_POST['task_id'] ?? ''));
    if (empty($task_id)) wp_send_json_error(['message' => 'Missing task_id']);

    $table   = $wpdb->prefix . 'vmo_daily_activities';
    $current = $wpdb->get_row($wpdb->prepare(
        "SELECT activity_date FROM {$table} WHERE activity_id = %s AND user_id = %d LIMIT 1",
        $task_id, $user_id
    ));
    if (!$current) wp_send_json_error(['message' => 'Task not found']);

    $new_date = date('Y-m-d', strtotime($current->activity_date . ' +1 day'));
    $wpdb->update($table, [
        'activity_date' => $new_date,
        'status'        => 'pending',
        'updated_at'    => current_time('mysql'),
    ], ['activity_id' => $task_id, 'user_id' => $user_id]);

    wp_send_json_success(['message' => 'Task deferred to ' . $new_date]);
});

// ─── Reschedule a daily-activity task to a specific date ───
add_action('wp_ajax_vmo_reschedule_today_task', function() {
    if (!current_user_can('read')) wp_send_json_error(['message' => 'Unauthorized'], 403);
    global $wpdb;
    $user_id  = get_current_user_id();
    $task_id  = sanitize_text_field(wp_unslash($_POST['task_id'] ?? ''));
    $new_date = sanitize_text_field(wp_unslash($_POST['new_date'] ?? ''));
    if (empty($task_id) || empty($new_date)) wp_send_json_error(['message' => 'Missing task_id or new_date']);
    if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $new_date)) wp_send_json_error(['message' => 'Invalid date format']);

    $table = $wpdb->prefix . 'vmo_daily_activities';
    $wpdb->update($table, [
        'activity_date' => $new_date,
        'status'        => 'pending',
        'updated_at'    => current_time('mysql'),
    ], ['activity_id' => $task_id, 'user_id' => $user_id]);

    wp_send_json_success(['message' => 'Task rescheduled to ' . $new_date]);
});

// ─── Done items (completed tasks + actions today) ───
add_action('wp_ajax_vmo_get_done_items', function() {
    if (!current_user_can('read')) wp_send_json_error(['message' => 'Unauthorized'], 403);
    $user_id = get_current_user_id();
    global $wpdb;
    $today = current_time('Y-m-d');
    $items = [];

    // Completed follow-up tasks today
    $tasks = $wpdb->get_results($wpdb->prepare(
        "SELECT description as title, updated_at as completed_at FROM {$wpdb->prefix}vmo_follow_ups WHERE user_id = %d AND status = 'completed' AND DATE(updated_at) = %s ORDER BY updated_at DESC LIMIT 30",
        $user_id, $today
    ));
    if ($tasks) {
        foreach ($tasks as $t) {
            $items[] = ['title' => $t->title, 'completed_at' => wp_date('g:i A', strtotime($t->completed_at))];
        }
    }

    // Completed actions today
    $actions_table = $wpdb->prefix . 'vmo_actions';
    if ($wpdb->get_var("SHOW TABLES LIKE '{$actions_table}'") === $actions_table) {
        $actions = $wpdb->get_results($wpdb->prepare(
            "SELECT title, completed_at FROM {$actions_table} WHERE user_id = %d AND status = 'completed' AND DATE(completed_at) = %s ORDER BY completed_at DESC LIMIT 20",
            $user_id, $today
        ));
        if ($actions) {
            foreach ($actions as $a) {
                $items[] = ['title' => $a->title, 'completed_at' => wp_date('g:i A', strtotime($a->completed_at))];
            }
        }
    }

    wp_send_json_success(['items' => $items]);
});

if (!function_exists('vmo_cfo_token_rate_usd')) {
    function vmo_cfo_token_rate_usd() {
        return defined('VMO_TOKEN_RATE') ? (float) VMO_TOKEN_RATE : 0.000667;
    }
}

if (!function_exists('vmo_cfo_table_columns')) {
    function vmo_cfo_table_columns($table) {
        static $cache = [];
        if (isset($cache[$table])) {
            return $cache[$table];
        }
        global $wpdb;
        if (!function_exists('vmo_table_exists') || !vmo_table_exists($table)) {
            $cache[$table] = [];
            return [];
        }
        $columns = $wpdb->get_col("SHOW COLUMNS FROM {$table}", 0);
        $cache[$table] = is_array($columns) ? $columns : [];
        return $cache[$table];
    }
}

if (!function_exists('vmo_cfo_ensure_tables')) {
    function vmo_cfo_ensure_tables() {
        static $done = false;
        if ($done) {
            return;
        }
        $done = true;

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

        global $wpdb;
        require_once ABSPATH . 'wp-admin/includes/upgrade.php';
        $charset_collate = $wpdb->get_charset_collate();

        $token_usage_table = $wpdb->prefix . 'vmo_token_usage';
        dbDelta("CREATE TABLE IF NOT EXISTS {$token_usage_table} (
            id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
            user_id BIGINT UNSIGNED NOT NULL,
            feature_key VARCHAR(120) DEFAULT NULL,
            system_key VARCHAR(80) DEFAULT NULL,
            action VARCHAR(160) DEFAULT NULL,
            usage_type VARCHAR(120) DEFAULT NULL,
            feature VARCHAR(120) DEFAULT NULL,
            tokens_used INT UNSIGNED DEFAULT 0,
            actual_cost_usd DECIMAL(12,4) DEFAULT 0.0000,
            user_facing_value_usd DECIMAL(12,4) DEFAULT 0.0000,
            markup_multiplier DECIMAL(8,2) DEFAULT 0.00,
            description TEXT DEFAULT NULL,
            metadata LONGTEXT DEFAULT NULL,
            created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
            KEY idx_user_id (user_id),
            KEY idx_system_key (system_key),
            KEY idx_feature_key (feature_key),
            KEY idx_created_at (created_at)
        ) {$charset_collate};");

        $usage_tracking_table = $wpdb->prefix . 'vmo_usage_tracking';
        dbDelta("CREATE TABLE IF NOT EXISTS {$usage_tracking_table} (
            id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
            user_id BIGINT UNSIGNED NOT NULL,
            feature_type VARCHAR(120) NOT NULL,
            action VARCHAR(160) DEFAULT NULL,
            tokens_used INT UNSIGNED DEFAULT 0,
            metadata LONGTEXT DEFAULT NULL,
            created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
            KEY idx_user_id (user_id),
            KEY idx_feature_type (feature_type),
            KEY idx_created_at (created_at)
        ) {$charset_collate};");

        $events_table = $wpdb->prefix . 'vmo_financial_events';
        dbDelta("CREATE TABLE IF NOT EXISTS {$events_table} (
            id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
            user_id BIGINT UNSIGNED NOT NULL,
            event_type VARCHAR(80) NOT NULL DEFAULT 'transaction',
            source VARCHAR(80) NOT NULL DEFAULT 'manual_entry',
            source_reference VARCHAR(191) DEFAULT NULL,
            direction VARCHAR(30) DEFAULT 'income',
            amount DECIMAL(14,2) DEFAULT 0.00,
            currency VARCHAR(10) DEFAULT 'USD',
            payment_status VARCHAR(60) DEFAULT 'pending',
            event_status VARCHAR(60) DEFAULT 'needs_review',
            confidence DECIMAL(5,2) DEFAULT 0.00,
            customer_name VARCHAR(255) DEFAULT NULL,
            customer_email VARCHAR(255) DEFAULT NULL,
            customer_phone VARCHAR(80) DEFAULT NULL,
            vendor_name VARCHAR(255) DEFAULT NULL,
            payment_method VARCHAR(80) DEFAULT NULL,
            category VARCHAR(120) DEFAULT NULL,
            product_service VARCHAR(255) DEFAULT NULL,
            transaction_date DATETIME DEFAULT NULL,
            tax_amount DECIMAL(14,2) DEFAULT 0.00,
            fees_amount DECIMAL(14,2) DEFAULT 0.00,
            contact_id VARCHAR(191) DEFAULT NULL,
            crm_contact_db_id BIGINT UNSIGNED DEFAULT NULL,
            invoice_id BIGINT UNSIGNED DEFAULT NULL,
            purchase_id BIGINT UNSIGNED DEFAULT NULL,
            recommended_action VARCHAR(120) DEFAULT NULL,
            missing_fields TEXT DEFAULT NULL,
            notes TEXT DEFAULT NULL,
            metadata LONGTEXT DEFAULT NULL,
            decided_at DATETIME DEFAULT NULL,
            posted_at DATETIME DEFAULT NULL,
            created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
            updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
            KEY idx_user_id (user_id),
            KEY idx_status (event_status),
            KEY idx_source (source),
            KEY idx_reference (source_reference),
            KEY idx_created_at (created_at)
        ) {$charset_collate};");

        $ledger_table = $wpdb->prefix . 'vmo_financial_ledger';
        dbDelta("CREATE TABLE IF NOT EXISTS {$ledger_table} (
            id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
            user_id BIGINT UNSIGNED NOT NULL,
            event_id BIGINT UNSIGNED DEFAULT NULL,
            source VARCHAR(80) DEFAULT 'manual_entry',
            source_reference VARCHAR(191) DEFAULT NULL,
            transaction_type VARCHAR(80) DEFAULT 'income',
            direction VARCHAR(30) DEFAULT 'income',
            amount DECIMAL(14,2) DEFAULT 0.00,
            tax_amount DECIMAL(14,2) DEFAULT 0.00,
            fees_amount DECIMAL(14,2) DEFAULT 0.00,
            net_amount DECIMAL(14,2) DEFAULT 0.00,
            currency VARCHAR(10) DEFAULT 'USD',
            status VARCHAR(60) DEFAULT 'posted',
            category VARCHAR(120) DEFAULT NULL,
            customer_vendor VARCHAR(255) DEFAULT NULL,
            contact_id VARCHAR(191) DEFAULT NULL,
            crm_contact_db_id BIGINT UNSIGNED DEFAULT NULL,
            invoice_id BIGINT UNSIGNED DEFAULT NULL,
            purchase_id BIGINT UNSIGNED DEFAULT NULL,
            transaction_id VARCHAR(191) DEFAULT NULL,
            product_service VARCHAR(255) DEFAULT NULL,
            payment_method VARCHAR(80) DEFAULT NULL,
            transaction_date DATETIME DEFAULT NULL,
            metadata LONGTEXT DEFAULT NULL,
            created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
            updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
            UNIQUE KEY uniq_event (event_id),
            KEY idx_user_id (user_id),
            KEY idx_direction (direction),
            KEY idx_status (status),
            KEY idx_transaction_date (transaction_date),
            KEY idx_source_reference (source_reference)
        ) {$charset_collate};");
    }
}

if (!function_exists('vmo_cfo_system_key_for_feature')) {
    function vmo_cfo_system_key_for_feature($feature_key, $action = '') {
        $key = strtolower(trim((string) $feature_key . ' ' . (string) $action));
        if (preg_match('/phone|call|voice|twilio|recording|transcription|tts/', $key)) return 'phone';
        if (preg_match('/sms|text|a2p/', $key)) return 'sms';
        if (preg_match('/email|mail|campaign/', $key)) return 'email';
        if (preg_match('/website|page|build|publish|section|content|refinement/', $key)) return 'website';
        if (preg_match('/media|image|video|sora|dalle|animation|effect/', $key)) return 'media';
        if (preg_match('/seo|scan|audit/', $key)) return 'seo';
        if (preg_match('/doc|pdf|ocr|legal/', $key)) return 'docs';
        if (preg_match('/research|competitor|roadmap/', $key)) return 'research';
        return 'ai-chat';
    }
}

if (!function_exists('vmo_cfo_log_token_usage')) {
    function vmo_cfo_log_token_usage($user_id, $feature_key, $action, $tokens_used, array $details = []) {
        global $wpdb;
        $user_id = absint($user_id);
        $tokens_used = max(0, absint($tokens_used));
        if (!$user_id || !$tokens_used) {
            return false;
        }

        vmo_cfo_ensure_tables();
        $table = $wpdb->prefix . 'vmo_token_usage';
        if (!function_exists('vmo_table_exists') || !vmo_table_exists($table)) {
            return false;
        }

        $feature_key = sanitize_key($feature_key ?: ($details['usage_type'] ?? 'general'));
        $action = sanitize_key($action ?: ($details['action'] ?? $feature_key));
        $system_key = sanitize_key($details['system_key'] ?? vmo_cfo_system_key_for_feature($feature_key, $action));
        $rate = vmo_cfo_token_rate_usd();
        $actual_cost = isset($details['actual_cost_usd']) ? (float) $details['actual_cost_usd'] : 0.0;
        $user_value = isset($details['user_facing_value_usd']) ? (float) $details['user_facing_value_usd'] : round($tokens_used * $rate, 4);
        $markup = $actual_cost > 0 ? round($user_value / $actual_cost, 2) : (float) ($details['markup_multiplier'] ?? 0);
        $metadata = $details['metadata'] ?? [];

        return false !== $wpdb->insert($table, [
            'user_id' => $user_id,
            'feature_key' => $feature_key,
            'system_key' => $system_key,
            'action' => $action,
            'usage_type' => sanitize_key($details['usage_type'] ?? $feature_key),
            'feature' => sanitize_key($details['feature'] ?? $feature_key),
            'tokens_used' => $tokens_used,
            'actual_cost_usd' => $actual_cost,
            'user_facing_value_usd' => $user_value,
            'markup_multiplier' => $markup,
            'description' => sanitize_text_field($details['description'] ?? ''),
            'metadata' => is_string($metadata) ? wp_kses_post($metadata) : wp_json_encode($metadata),
            'created_at' => sanitize_text_field($details['created_at'] ?? current_time('mysql')),
        ]);
    }
}

if (!function_exists('vmo_cfo_get_token_usage_summary')) {
    function vmo_cfo_get_token_usage_summary($user_id, $days = 30) {
        global $wpdb;
        vmo_cfo_ensure_tables();

        $user_id = absint($user_id);
        $days = max(1, absint($days));
        $since = gmdate('Y-m-d H:i:s', strtotime('-' . $days . ' days'));
        $systems = ['phone' => 0, 'sms' => 0, 'email' => 0, 'website' => 0, 'ai-chat' => 0, 'media' => 0, 'seo' => 0, 'docs' => 0, 'research' => 0];
        $details = [];
        $total_tokens = 0;
        $estimated_value = 0.0;

        $usage_table = $wpdb->prefix . 'vmo_token_usage';
        if (function_exists('vmo_table_exists') && vmo_table_exists($usage_table)) {
            $columns = vmo_cfo_table_columns($usage_table);
            $feature_exprs = [];
            foreach (['feature_key', 'feature', 'usage_type', 'action'] as $column) {
                if (in_array($column, $columns, true)) {
                    $feature_exprs[] = "NULLIF({$column}, '')";
                }
            }
            $feature_expr = $feature_exprs ? 'COALESCE(' . implode(', ', $feature_exprs) . ", 'general')" : "'general'";
            $system_expr = in_array('system_key', $columns, true) ? "COALESCE(NULLIF(system_key, ''), '')" : "''";
            $value_expr = in_array('user_facing_value_usd', $columns, true) ? 'SUM(user_facing_value_usd)' : '0';
            $rows = $wpdb->get_results($wpdb->prepare(
                "SELECT {$feature_expr} AS feature_key, {$system_expr} AS system_key, SUM(tokens_used) AS total_tokens, {$value_expr} AS total_value
                 FROM {$usage_table}
                 WHERE user_id = %d AND created_at >= %s
                 GROUP BY feature_key, system_key",
                $user_id,
                $since
            ), ARRAY_A);

            foreach ((array) $rows as $row) {
                $feature = (string) ($row['feature_key'] ?? 'general');
                $system = sanitize_key($row['system_key'] ?: vmo_cfo_system_key_for_feature($feature));
                if (!isset($systems[$system])) {
                    $systems[$system] = 0;
                }
                $tokens = (int) ($row['total_tokens'] ?? 0);
                $systems[$system] += $tokens;
                $total_tokens += $tokens;
                $estimated_value += (float) ($row['total_value'] ?? 0);
                $details[] = ['feature_key' => $feature, 'system_key' => $system, 'tokens' => $tokens];
            }
        }

        if ($total_tokens <= 0) {
            $tracking_table = $wpdb->prefix . 'vmo_usage_tracking';
            if (function_exists('vmo_table_exists') && vmo_table_exists($tracking_table)) {
                $rows = $wpdb->get_results($wpdb->prepare(
                    "SELECT feature_type, action, SUM(tokens_used) AS total_tokens FROM {$tracking_table} WHERE user_id = %d AND created_at >= %s GROUP BY feature_type, action",
                    $user_id,
                    $since
                ), ARRAY_A);
                foreach ((array) $rows as $row) {
                    $feature = (string) ($row['feature_type'] ?? 'general');
                    $system = sanitize_key(vmo_cfo_system_key_for_feature($feature, $row['action'] ?? ''));
                    if (!isset($systems[$system])) {
                        $systems[$system] = 0;
                    }
                    $tokens = (int) ($row['total_tokens'] ?? 0);
                    $systems[$system] += $tokens;
                    $total_tokens += $tokens;
                    $details[] = ['feature_key' => $feature, 'system_key' => $system, 'tokens' => $tokens, 'source' => 'legacy_usage_tracking'];
                }
            }
        }

        if ($total_tokens <= 0) {
            $logs = get_option('vmo_token_usage_logs', []);
            $cutoff = strtotime($since);
            if (is_array($logs)) {
                foreach ($logs as $entry) {
                    if ((int) ($entry['user_id'] ?? 0) !== $user_id) continue;
                    $timestamp = strtotime((string) ($entry['timestamp'] ?? '')) ?: 0;
                    if ($timestamp < $cutoff) continue;
                    $feature = (string) ($entry['operation'] ?? 'general');
                    $system = sanitize_key(vmo_cfo_system_key_for_feature($feature));
                    if (!isset($systems[$system])) {
                        $systems[$system] = 0;
                    }
                    $tokens = (int) ($entry['tokens_used'] ?? 0);
                    $systems[$system] += $tokens;
                    $total_tokens += $tokens;
                    $estimated_value += (float) ($entry['dollar_value'] ?? 0);
                    $details[] = ['feature_key' => $feature, 'system_key' => $system, 'tokens' => $tokens, 'source' => 'legacy_option_log'];
                }
            }
        }

        $daily_avg = $total_tokens > 0 ? round($total_tokens / $days) : 0;
        if ($estimated_value <= 0 && $total_tokens > 0) {
            $estimated_value = round($total_tokens * vmo_cfo_token_rate_usd(), 4);
        }

        return [
            'total_tokens' => $total_tokens,
            'daily_avg' => $daily_avg,
            'cost_by_system' => $systems,
            'details' => $details,
            'estimated_value_usd' => round($estimated_value, 4),
            'source' => $total_tokens > 0 ? 'canonical_or_compatibility_ledger' : 'none',
        ];
    }
}

if (!function_exists('vmo_cfo_get_stripe_connect_status')) {
    function vmo_cfo_get_stripe_connect_status($user_id) {
        global $wpdb;
        $user_id = absint($user_id);
        $table = $wpdb->prefix . 'vmo_connected_accounts';
        $row = [];

        if (function_exists('vmo_table_exists') && vmo_table_exists($table)) {
            $row = $wpdb->get_row($wpdb->prepare(
                "SELECT * FROM {$table} WHERE user_id = %d ORDER BY updated_at DESC, created_at DESC LIMIT 1",
                $user_id
            ), ARRAY_A) ?: [];
        }

        $connected = !empty($row['stripe_account_id']);
        $charges = !empty($row['charges_enabled']);
        $payouts = !empty($row['payouts_enabled']);
        $details = !empty($row['details_submitted']);
        $requirements_due = [];
        if (!empty($row['requirements_due'])) {
            $decoded = json_decode((string) $row['requirements_due'], true);
            $requirements_due = is_array($decoded) ? array_values(array_filter($decoded)) : array_filter(array_map('trim', explode(',', (string) $row['requirements_due'])));
        }

        $blockers = [];
        if (!$connected) {
            $blockers[] = 'Complete VMO Pay setup to accept online payments.';
        } else {
            if (!$details) $blockers[] = 'VMO Pay business details are incomplete.';
            if (!$charges) $blockers[] = 'Charges are not enabled yet.';
            if (!$payouts) $blockers[] = 'Payouts are not enabled yet.';
            foreach ($requirements_due as $requirement) {
                $blockers[] = 'VMO Pay verification requirement due: ' . $requirement;
            }
        }

        $status = 'Not connected';
        if ($connected && $charges && $payouts && $details) {
            $status = 'Active';
        } elseif ($connected && $charges) {
            $status = 'Restricted';
        } elseif ($connected) {
            $status = 'Onboarding incomplete';
        }

        return [
            'connected' => $connected,
            'stripe_account_id' => (string) ($row['stripe_account_id'] ?? ''),
            'status' => $status,
            'details_submitted' => $details,
            'charges_enabled' => $charges,
            'payouts_enabled' => $payouts,
            'requirements_due' => $requirements_due,
            'setup_blockers' => $blockers,
            'last_synced' => (string) ($row['last_synced'] ?? $row['updated_at'] ?? ''),
        ];
    }
}

if (!function_exists('vmo_cfo_get_plan_intelligence')) {
    function vmo_cfo_get_plan_intelligence($user_id, array $token_summary) {
        $usage_data = class_exists('VMO_Usage_Calculator') ? VMO_Usage_Calculator::get_usage_data($user_id) : [];
        $raw_plan = $usage_data['plan'] ?? '';
        if ($raw_plan === '') {
            $raw_plan = get_user_meta($user_id, 'vmo_subscription_status', true);
        }
        if ($raw_plan === '') {
            $raw_plan = get_user_meta($user_id, 'vmo_plan', true);
        }
        $plan_key = sanitize_key($raw_plan ?: 'free');
        $plan_name = $usage_data['plan_name'] ?? ucwords(str_replace('_', ' ', $plan_key));
        $tokens_included = (int) ($usage_data['plan_tokens'] ?? 0);
        $balance = (int) get_user_meta($user_id, 'vmo_token_balance', true);
        $billing_status = sanitize_key(get_user_meta($user_id, 'vmo_subscription_billing_status', true) ?: get_user_meta($user_id, 'vmo_subscription_state', true) ?: ($plan_key === 'free' ? 'free' : 'active'));
        $renewal_date = (string) ($usage_data['renewal_date'] ?? get_user_meta($user_id, 'vmo_subscription_renewal_date', true) ?: get_user_meta($user_id, 'vmo_renewal_date', true) ?: '');
        $billing_cycle = (string) get_user_meta($user_id, 'vmo_billing_cycle', true);
        if ($billing_cycle === '') {
            $billing_cycle = $renewal_date ? 'Monthly' : 'Not set';
        }

        $entitlement_map = [
            'free' => ['basic_dashboard', 'manual_controls'],
            'tester' => ['consultation', 'builder', 'basic_cfo', 'crm'],
            'starter' => ['consultation', 'builder', 'crm', 'cfo', 'reports'],
            'pro' => ['consultation', 'builder', 'crm', 'cfo', 'phone', 'email', 'seo', 'reports'],
            'premium' => ['consultation', 'builder', 'crm', 'cfo', 'phone', 'email', 'seo', 'reports'],
            'elite' => ['consultation', 'builder', 'crm', 'cfo', 'phone', 'email', 'seo', 'media', 'automation', 'reports'],
            'enterprise_pack' => ['all_features'],
        ];
        $entitlements = $entitlement_map[$plan_key] ?? $entitlement_map['free'];
        $restricted = [];
        foreach (['phone', 'email', 'seo', 'media', 'automation'] as $feature) {
            if (!in_array('all_features', $entitlements, true) && !in_array($feature, $entitlements, true)) {
                $restricted[] = $feature;
            }
        }

        $usage_percent = $tokens_included > 0 ? round(($token_summary['total_tokens'] / max(1, $tokens_included)) * 100, 1) : (float) ($usage_data['usage_percent'] ?? 0);
        $recommendation = 'Your current plan appears adequate for the recorded usage pattern.';
        if (in_array($billing_status, ['past_due', 'incomplete', 'failed', 'canceled'], true)) {
            $recommendation = 'Resolve billing status before relying on automated CFO and AI operations.';
        } elseif ($usage_percent >= 85 || ($token_summary['daily_avg'] ?? 0) > 0 && $balance > 0 && round($balance / max(1, (int) $token_summary['daily_avg'])) < 7) {
            $recommendation = 'Usage is trending high; top up or upgrade before automation starts throttling.';
        } elseif ($plan_key === 'free') {
            $recommendation = 'Choose a paid plan to unlock dependable automation and included monthly tokens.';
        }

        return [
            'plan_key' => $plan_key,
            'plan_name' => $plan_name,
            'billing_cycle' => $billing_cycle,
            'renewal_date' => $renewal_date ?: 'N/A',
            'billing_status' => $billing_status,
            'tokens_included' => $tokens_included,
            'tokens_used_cycle' => max(0, (int) ($usage_data['tokens_used'] ?? $token_summary['total_tokens'] ?? 0)),
            'tokens_remaining' => $balance,
            'usage_percent' => $usage_percent,
            'entitlements' => $entitlements,
            'restricted_features' => $restricted,
            'top_up_available' => true,
            'recommendation' => $recommendation,
        ];
    }
}

if (!function_exists('vmo_cfo_required_fields_for_event')) {
    function vmo_cfo_required_fields_for_event(array $event) {
        $required = ['amount', 'direction', 'payment_method', 'transaction_date', 'category'];
        $direction = sanitize_key($event['direction'] ?? $event['transaction_type'] ?? 'income');
        if ($direction === 'expense') {
            $required[] = 'vendor_name';
        } else {
            $required[] = 'customer_name';
            $required[] = 'product_service';
        }
        $missing = [];
        foreach ($required as $field) {
            $value = $event[$field] ?? null;
            if ($field === 'amount') {
                if (floatval($value) <= 0) $missing[] = $field;
            } elseif ($value === null || trim((string) $value) === '') {
                $missing[] = $field;
            }
        }
        return array_values(array_unique($missing));
    }
}

if (!function_exists('vmo_cfo_follow_up_questions_for_missing')) {
    function vmo_cfo_follow_up_questions_for_missing(array $missing_fields, $direction = 'income') {
        $direction = sanitize_key($direction ?: 'income');
        $labels = [
            'amount' => 'How much money was involved?',
            'payment_method' => 'How was it paid or how should it be paid?',
            'transaction_date' => 'What date did this happen?',
            'category' => 'What financial category should VMO use?',
            'customer_name' => 'Who paid or who is expected to pay?',
            'vendor_name' => 'Who was paid or who needs to be paid?',
            'product_service' => $direction === 'expense' ? 'What was this expense for?' : 'What product, service, deposit, or invoice is this for?',
        ];

        $questions = [];
        foreach (array_values(array_unique($missing_fields)) as $field) {
            if (isset($labels[$field])) {
                $questions[] = $labels[$field];
            }
        }
        return $questions;
    }
}

if (!function_exists('vmo_cfo_verify_ajax_nonce')) {
    function vmo_cfo_verify_ajax_nonce() {
        $candidates = [
            sanitize_text_field(wp_unslash($_REQUEST['nonce'] ?? '')),
            sanitize_text_field(wp_unslash($_REQUEST['_ajax_nonce'] ?? '')),
            sanitize_text_field(wp_unslash($_REQUEST['security'] ?? '')),
        ];
        $actions = ['vmo_nonce', 'vmo_admin_nonce', 'vmo_builder_nonce'];
        foreach (array_filter($candidates) as $nonce) {
            foreach ($actions as $action) {
                if (wp_verify_nonce($nonce, $action)) {
                    return true;
                }
            }
        }
        wp_send_json_error(['message' => 'Invalid security token. Refresh the dashboard and try again.'], 403);
    }
}

if (!function_exists('vmo_cfo_parse_financial_instruction_details')) {
    function vmo_cfo_parse_financial_instruction_details($instruction, $default_direction = 'income') {
        $instruction = trim(wp_strip_all_tags((string) $instruction));
        $lower = strtolower($instruction);
        $amount = 0.0;
        if (preg_match('/(?:\$|usd\s*)?([0-9][0-9,]*(?:\.[0-9]{1,2})?)/i', $instruction, $amount_match)) {
            $amount = (float) str_replace(',', '', $amount_match[1]);
        }

        $direction = sanitize_key($default_direction ?: 'income');
        if (preg_match('/\b(spent|paid for|paid out|bought|expense|refund given|refunded)\b/', $lower)) {
            $direction = 'expense';
        } elseif (preg_match('/\b(opportunity|might pay|may pay|quote|estimate|interested|wants to book)\b/', $lower)) {
            $direction = 'opportunity';
        } elseif (!in_array($direction, ['income', 'expense', 'refund', 'opportunity'], true)) {
            $direction = 'income';
        }

        $payment_method = '';
        foreach (['cash', 'zelle', 'venmo', 'cash app', 'paypal', 'bank transfer', 'check', 'vmo pay', 'stripe', 'card', 'invoice', 'payment link'] as $method) {
            if (strpos($lower, $method) !== false) {
                $payment_method = str_replace(' ', '_', $method === 'stripe' ? 'vmo pay' : $method);
                break;
            }
        }

        $product_service = '';
        if (preg_match('/\bfor\s+([^\.\n]+)$/i', $instruction, $service_match)) {
            $product_service = trim($service_match[1]);
        }

        $customer_name = '';
        if ($direction !== 'expense' && preg_match('/\bfrom\s+(.+?)(?:\s+for\b|\s+via\b|\s+by\b|\s+on\b|$)/iu', $instruction, $name_match)) {
            $customer_name = trim($name_match[1]);
        }

        $vendor_name = '';
        if ($direction === 'expense' && preg_match('/\b(?:to|at|from)\s+(.+?)(?:\s+for\b|\s+via\b|\s+by\b|\s+on\b|$)/iu', $instruction, $vendor_match)) {
            $vendor_name = trim($vendor_match[1]);
        }

        $transaction_date = '';
        if (preg_match('/\btoday\b/i', $instruction)) {
            $transaction_date = current_time('mysql');
        } elseif (preg_match('/\byesterday\b/i', $instruction)) {
            $transaction_date = date('Y-m-d H:i:s', strtotime('-1 day', current_time('timestamp')));
        } elseif (preg_match('/\b(?:on\s+)?([0-9]{1,2}\/[0-9]{1,2}(?:\/[0-9]{2,4})?)\b/', $instruction, $date_match)) {
            $parsed_date = strtotime($date_match[1]);
            if ($parsed_date) {
                $transaction_date = date('Y-m-d H:i:s', $parsed_date);
            }
        }

        return [
            'amount' => $amount,
            'direction' => $direction,
            'payment_method' => $payment_method,
            'product_service' => $product_service,
            'customer_name' => $customer_name,
            'vendor_name' => $vendor_name,
            'transaction_date' => $transaction_date,
        ];
    }
}

if (!function_exists('vmo_cfo_get_financial_event')) {
    function vmo_cfo_get_financial_event($user_id, $event_id) {
        global $wpdb;
        vmo_cfo_ensure_tables();
        $events_table = $wpdb->prefix . 'vmo_financial_events';
        return $wpdb->get_row($wpdb->prepare(
            "SELECT * FROM {$events_table} WHERE id = %d AND user_id = %d LIMIT 1",
            absint($event_id),
            absint($user_id)
        ), ARRAY_A);
    }
}

if (!function_exists('vmo_cfo_ensure_vmo_pay_tables')) {
    function vmo_cfo_ensure_vmo_pay_tables() {
        if (class_exists('VMO_Pay')) {
            VMO_Pay::get_instance()->ensure_tables();
        }
    }
}

if (!function_exists('vmo_cfo_update_event_metadata')) {
    function vmo_cfo_update_event_metadata($user_id, $event_id, array $metadata_updates, array $row_updates = []) {
        global $wpdb;
        $event = vmo_cfo_get_financial_event($user_id, $event_id);
        if (!$event) {
            return new WP_Error('financial_event_missing', 'Financial event not found.');
        }
        $metadata = [];
        if (!empty($event['metadata'])) {
            $decoded = json_decode((string) $event['metadata'], true);
            $metadata = is_array($decoded) ? $decoded : [];
        }
        $metadata = array_merge($metadata, $metadata_updates);
        $row_updates['metadata'] = wp_json_encode($metadata);
        $row_updates['updated_at'] = current_time('mysql');
        $events_table = $wpdb->prefix . 'vmo_financial_events';
        $updated = $wpdb->update($events_table, $row_updates, ['id' => absint($event_id), 'user_id' => absint($user_id)]);
        if ($updated === false) {
            return new WP_Error('financial_event_update_failed', 'Unable to update financial event.');
        }
        return ['event_id' => absint($event_id), 'metadata' => $metadata];
    }
}

if (!function_exists('vmo_cfo_create_invoice_for_event')) {
    function vmo_cfo_create_invoice_for_event($user_id, $event_id) {
        global $wpdb;
        vmo_cfo_ensure_vmo_pay_tables();
        $event = vmo_cfo_get_financial_event($user_id, $event_id);
        if (!$event) {
            return new WP_Error('financial_event_missing', 'Financial event not found.');
        }
        if ((float) ($event['amount'] ?? 0) <= 0) {
            return new WP_Error('financial_event_amount_missing', 'Add an amount before creating an invoice.');
        }

        $invoice_table = $wpdb->prefix . 'vmo_invoices';
        if (function_exists('vmo_table_exists') && !vmo_table_exists($invoice_table)) {
            return new WP_Error('invoice_table_missing', 'VMO Pay invoice table is missing.');
        }

        $count = (int) $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$invoice_table} WHERE user_id = %d", $user_id));
        $invoice_number = 'INV-' . gmdate('Ymd') . '-' . str_pad($count + 1, 4, '0', STR_PAD_LEFT);
        $token = wp_generate_password(48, false, false);
        $description = sanitize_text_field($event['product_service'] ?: $event['category'] ?: 'Financial event');
        $amount = round((float) $event['amount'], 2);
        $items = [[
            'description' => $description,
            'qty' => 1,
            'price' => $amount,
            'total' => $amount,
        ]];

        $inserted = $wpdb->insert($invoice_table, [
            'user_id' => absint($user_id),
            'invoice_number' => $invoice_number,
            'client_name' => sanitize_text_field($event['customer_name'] ?: 'Customer'),
            'client_email' => sanitize_email($event['customer_email'] ?? ''),
            'items' => wp_json_encode($items),
            'subtotal' => $amount,
            'tax' => round((float) ($event['tax_amount'] ?? 0), 2),
            'total' => round($amount + (float) ($event['tax_amount'] ?? 0), 2),
            'currency' => strtolower(sanitize_text_field($event['currency'] ?: 'USD')),
            'due_date' => date('Y-m-d', strtotime('+7 days', current_time('timestamp'))),
            'notes' => sanitize_textarea_field($event['notes'] ?: 'Created from the CFO AI Financial Inbox.'),
            'status' => 'draft',
            'pay_token' => $token,
            'created_at' => current_time('mysql'),
            'updated_at' => current_time('mysql'),
        ]);
        if ($inserted === false) {
            return new WP_Error('invoice_create_failed', 'Unable to create invoice from financial event.');
        }
        $invoice_id = (int) $wpdb->insert_id;
        $pay_url = rest_url("vmo/v1/invoice/{$token}");

        $updated = vmo_cfo_update_event_metadata($user_id, $event_id, [
            'invoice_id' => $invoice_id,
            'invoice_number' => $invoice_number,
            'invoice_pay_url' => $pay_url,
        ], [
            'invoice_id' => $invoice_id,
            'event_status' => 'invoice_created',
            'recommended_action' => 'send_invoice',
            'decided_at' => current_time('mysql'),
        ]);
        if (is_wp_error($updated)) {
            return $updated;
        }

        return ['event_id' => absint($event_id), 'invoice_id' => $invoice_id, 'invoice_number' => $invoice_number, 'pay_url' => $pay_url];
    }
}

if (!function_exists('vmo_cfo_get_vmo_pay_account_id')) {
    function vmo_cfo_get_vmo_pay_account_id($user_id) {
        global $wpdb;
        $accounts_table = $wpdb->prefix . 'vmo_connected_accounts';
        if (function_exists('vmo_table_exists') && !vmo_table_exists($accounts_table)) {
            return '';
        }
        return (string) $wpdb->get_var($wpdb->prepare(
            "SELECT stripe_account_id FROM {$accounts_table} WHERE user_id = %d AND charges_enabled = 1 ORDER BY updated_at DESC, created_at DESC LIMIT 1",
            absint($user_id)
        ));
    }
}

if (!function_exists('vmo_cfo_create_payment_link_for_event')) {
    function vmo_cfo_create_payment_link_for_event($user_id, $event_id) {
        global $wpdb;
        vmo_cfo_ensure_vmo_pay_tables();
        $event = vmo_cfo_get_financial_event($user_id, $event_id);
        if (!$event) {
            return new WP_Error('financial_event_missing', 'Financial event not found.');
        }
        $amount = round((float) ($event['amount'] ?? 0), 2);
        if ($amount < 0.50) {
            return new WP_Error('financial_event_amount_missing', 'Add an amount of at least $0.50 before creating a payment link.');
        }
        if (!defined('VMO_AI_BUILDER_DIR') || !file_exists(VMO_AI_BUILDER_DIR . 'stripe-sdk/init.php')) {
            return new WP_Error('vmo_pay_sdk_missing', 'VMO Pay SDK is not available.');
        }
        require_once VMO_AI_BUILDER_DIR . 'stripe-sdk/init.php';
        $secret_key = get_option('vmo_stripe_secret_key');
        if (!$secret_key) {
            return new WP_Error('vmo_pay_not_configured', 'VMO Pay is not configured.');
        }
        \Stripe\Stripe::setApiKey($secret_key);

        $name = sanitize_text_field($event['product_service'] ?: $event['category'] ?: 'CFO Financial Event #' . absint($event_id));
        $description = sanitize_text_field($event['notes'] ?: 'Created from the CFO AI Financial Inbox.');
        $currency = strtolower(sanitize_text_field($event['currency'] ?: 'USD'));
        $amount_cents = (int) round($amount * 100);
        $account_id = vmo_cfo_get_vmo_pay_account_id($user_id);
        $stripe_options = $account_id ? ['stripe_account' => $account_id] : [];

        try {
            $product = \Stripe\Product::create(array_filter(['name' => $name, 'description' => $description]), $stripe_options);
            $price = \Stripe\Price::create([
                'product' => $product->id,
                'currency' => $currency,
                'unit_amount' => $amount_cents,
            ], $stripe_options);
            $link_params = ['line_items' => [['price' => $price->id, 'quantity' => 1]]];
            if ($account_id) {
                $link_params['application_fee_percent'] = 2.9;
            }
            $link = \Stripe\PaymentLink::create($link_params, $stripe_options);
        } catch (Exception $exception) {
            return new WP_Error('vmo_pay_link_failed', 'VMO Pay could not create the payment link: ' . $exception->getMessage());
        }

        $links_table = $wpdb->prefix . 'vmo_payment_links';
        $inserted = $wpdb->insert($links_table, [
            'user_id' => absint($user_id),
            'name' => $name,
            'description' => $description,
            'amount_cents' => $amount_cents,
            'currency' => $currency,
            'allow_custom_amount' => 0,
            'stripe_product_id' => $product->id,
            'stripe_price_id' => $price->id,
            'stripe_payment_link_id' => $link->id,
            'stripe_payment_link_url' => $link->url,
            'status' => 'active',
            'created_at' => current_time('mysql'),
            'updated_at' => current_time('mysql'),
        ]);
        if ($inserted === false) {
            return new WP_Error('payment_link_save_failed', 'VMO Pay created the payment link, but VMO could not save it locally.');
        }
        $payment_link_id = (int) $wpdb->insert_id;

        $updated = vmo_cfo_update_event_metadata($user_id, $event_id, [
            'payment_link_id' => $payment_link_id,
            'payment_link_url' => $link->url,
        ], [
            'event_status' => 'payment_link_created',
            'recommended_action' => 'share_payment_link',
            'decided_at' => current_time('mysql'),
        ]);
        if (is_wp_error($updated)) {
            return $updated;
        }

        return ['event_id' => absint($event_id), 'payment_link_id' => $payment_link_id, 'url' => $link->url, 'name' => $name];
    }
}

if (!function_exists('vmo_cfo_record_financial_event')) {
    function vmo_cfo_record_financial_event($user_id, array $event) {
        global $wpdb;
        vmo_cfo_ensure_tables();
        $user_id = absint($user_id);
        if (!$user_id) {
            return new WP_Error('invalid_user', 'A user is required for financial event capture.');
        }

        $events_table = $wpdb->prefix . 'vmo_financial_events';
        if (!function_exists('vmo_table_exists') || !vmo_table_exists($events_table)) {
            return new WP_Error('missing_financial_events_table', 'Financial events table is missing.');
        }

        $confidence = isset($event['confidence']) ? (float) $event['confidence'] : 0.5;
        if ($confidence > 1) {
            $confidence = $confidence / 100;
        }
        $confidence = max(0, min(1, $confidence));
        $direction = sanitize_key($event['direction'] ?? $event['transaction_type'] ?? 'income');
        if (!in_array($direction, ['income', 'expense', 'refund', 'opportunity'], true)) {
            $direction = 'income';
        }
        $payment_status = sanitize_key($event['payment_status'] ?? $event['status'] ?? 'pending');
        $missing = vmo_cfo_required_fields_for_event(array_merge($event, ['direction' => $direction]));
        $trusted_confirmed_sources = ['stripe', 'vmo_pay', 'online_store', 'invoice', 'crm_purchase', 'manual_approved'];
        $source = sanitize_key($event['source'] ?? 'manual_entry');
        $event_status = sanitize_key($event['event_status'] ?? '');
        if ($event_status === '') {
            if ($missing) {
                $event_status = 'needs_review';
            } elseif (in_array($payment_status, ['completed', 'paid', 'confirmed'], true) && ($confidence >= 0.95 || in_array($source, $trusted_confirmed_sources, true))) {
                $event_status = 'confirmed';
            } elseif (in_array($payment_status, ['invoice_needed', 'payment_link_needed'], true)) {
                $event_status = $payment_status;
            } else {
                $event_status = 'needs_review';
            }
        }

        $transaction_date = sanitize_text_field($event['transaction_date'] ?? $event['date'] ?? current_time('mysql'));
        $source_reference = sanitize_text_field($event['source_reference'] ?? $event['transaction_id'] ?? $event['call_sid'] ?? $event['invoice_id'] ?? '');
        if ($source_reference !== '') {
            $existing_id = (int) $wpdb->get_var($wpdb->prepare(
                "SELECT id FROM {$events_table} WHERE user_id = %d AND source = %s AND source_reference = %s LIMIT 1",
                $user_id,
                $source,
                $source_reference
            ));
            if ($existing_id > 0) {
                return ['event_id' => $existing_id, 'duplicate' => true, 'event_status' => $event_status, 'missing_fields' => $missing];
            }
        }

        $metadata = $event['metadata'] ?? [];
        $row = [
            'user_id' => $user_id,
            'event_type' => sanitize_key($event['event_type'] ?? 'transaction'),
            'source' => $source,
            'source_reference' => $source_reference ?: null,
            'direction' => $direction,
            'amount' => round((float) ($event['amount'] ?? 0), 2),
            'currency' => strtoupper(sanitize_text_field($event['currency'] ?? 'USD')),
            'payment_status' => $payment_status,
            'event_status' => $event_status,
            'confidence' => round($confidence, 2),
            'customer_name' => sanitize_text_field($event['customer_name'] ?? $event['client_name'] ?? $event['caller_name'] ?? ''),
            'customer_email' => sanitize_email($event['customer_email'] ?? $event['client_email'] ?? ''),
            'customer_phone' => sanitize_text_field($event['customer_phone'] ?? $event['phone'] ?? $event['caller_phone'] ?? ''),
            'vendor_name' => sanitize_text_field($event['vendor_name'] ?? ''),
            'payment_method' => sanitize_key($event['payment_method'] ?? ''),
            'category' => sanitize_key($event['category'] ?? ''),
            'product_service' => sanitize_text_field($event['product_service'] ?? $event['service'] ?? $event['product'] ?? ''),
            'transaction_date' => $transaction_date ?: current_time('mysql'),
            'tax_amount' => round((float) ($event['tax_amount'] ?? $event['tax_collected'] ?? 0), 2),
            'fees_amount' => round((float) ($event['fees_amount'] ?? $event['fees_deducted'] ?? 0), 2),
            'contact_id' => sanitize_text_field($event['contact_id'] ?? ''),
            'crm_contact_db_id' => absint($event['crm_contact_db_id'] ?? $event['contact_db_id'] ?? 0) ?: null,
            'invoice_id' => absint($event['invoice_id'] ?? 0) ?: null,
            'purchase_id' => absint($event['purchase_id'] ?? 0) ?: null,
            'recommended_action' => sanitize_key($event['recommended_action'] ?? ($event_status === 'confirmed' ? 'post_to_ledger' : 'review')),
            'missing_fields' => $missing ? implode(',', $missing) : null,
            'notes' => sanitize_textarea_field($event['notes'] ?? $event['summary'] ?? ''),
            'metadata' => is_string($metadata) ? wp_kses_post($metadata) : wp_json_encode($metadata),
            'created_at' => current_time('mysql'),
            'updated_at' => current_time('mysql'),
        ];

        $inserted = $wpdb->insert($events_table, $row);
        if ($inserted === false) {
            return new WP_Error('financial_event_insert_failed', 'Unable to save financial event.');
        }
        $event_id = (int) $wpdb->insert_id;

        if (in_array($event_status, ['confirmed', 'approved'], true) && !empty($event['auto_post'])) {
            vmo_cfo_post_financial_event($user_id, $event_id, 'auto');
        }

        return ['event_id' => $event_id, 'event_status' => $event_status, 'missing_fields' => $missing, 'duplicate' => false];
    }
}

if (!function_exists('vmo_cfo_record_financial_instruction')) {
    function vmo_cfo_record_financial_instruction($user_id, $instruction) {
        $instruction = trim(wp_strip_all_tags((string) $instruction));
        if ($instruction === '') {
            return new WP_Error('empty_financial_instruction', 'Tell VMO what financial event happened.');
        }

        $lower = strtolower($instruction);
        $amount = 0.0;
        if (preg_match('/(?:\$|usd\s*)?([0-9][0-9,]*(?:\.[0-9]{1,2})?)/i', $instruction, $amount_match)) {
            $amount = (float) str_replace(',', '', $amount_match[1]);
        }

        $direction = preg_match('/\b(spent|paid for|paid out|bought|expense|refund given|refunded)\b/', $lower) ? 'expense' : 'income';
        if (preg_match('/\b(opportunity|might pay|may pay|quote|estimate|interested|wants to book)\b/', $lower)) {
            $direction = 'opportunity';
        }

        $payment_method = '';
        foreach (['cash', 'zelle', 'venmo', 'cash app', 'paypal', 'bank transfer', 'check', 'stripe', 'card', 'invoice', 'payment link'] as $method) {
            if (strpos($lower, $method) !== false) {
                $payment_method = str_replace(' ', '_', $method);
                break;
            }
        }

        $product_service = '';
        if (preg_match('/\bfor\s+([^\.\n]+)$/i', $instruction, $service_match)) {
            $product_service = trim($service_match[1]);
        }

        $customer_name = '';
        if ($direction !== 'expense' && preg_match('/\bfrom\s+(.+?)(?:\s+for\b|\s+via\b|\s+by\b|\s+on\b|$)/iu', $instruction, $name_match)) {
            $customer_name = trim($name_match[1]);
        }

        $vendor_name = '';
        if ($direction === 'expense' && preg_match('/\b(?:to|at|from)\s+(.+?)(?:\s+for\b|\s+via\b|\s+by\b|\s+on\b|$)/iu', $instruction, $vendor_match)) {
            $vendor_name = trim($vendor_match[1]);
        }

        $transaction_date = '';
        if (preg_match('/\btoday\b/i', $instruction)) {
            $transaction_date = current_time('mysql');
        } elseif (preg_match('/\byesterday\b/i', $instruction)) {
            $transaction_date = date('Y-m-d H:i:s', strtotime('-1 day', current_time('timestamp')));
        } elseif (preg_match('/\b(?:on\s+)?([0-9]{1,2}\/[0-9]{1,2}(?:\/[0-9]{2,4})?)\b/', $instruction, $date_match)) {
            $parsed_date = strtotime($date_match[1]);
            if ($parsed_date) {
                $transaction_date = date('Y-m-d H:i:s', $parsed_date);
            }
        }

        $confidence = $amount > 0 ? 0.58 : 0.25;
        if ($amount > 0 && $payment_method !== '' && $product_service !== '' && ($customer_name !== '' || $vendor_name !== '')) {
            $confidence = 0.72;
        }

        $event_payload = [
            'event_type' => $direction === 'opportunity' ? 'opportunity' : 'transaction',
            'source' => 'jarvis_instruction',
            'source_reference' => 'jarvis_' . md5($instruction . '|' . current_time('mysql') . '|' . wp_rand()),
            'direction' => $direction,
            'amount' => $amount,
            'currency' => 'USD',
            'payment_status' => $direction === 'opportunity' ? 'pending' : 'needs_confirmation',
            'event_status' => 'needs_review',
            'confidence' => $confidence,
            'customer_name' => $customer_name,
            'vendor_name' => $vendor_name,
            'payment_method' => $payment_method,
            'category' => $direction === 'expense' ? 'operating_expense' : 'sales',
            'product_service' => $product_service,
            'transaction_date' => $transaction_date,
            'recommended_action' => 'ask_follow_up',
            'notes' => $instruction,
            'metadata' => ['raw_instruction' => $instruction, 'capture_mode' => 'conversation', 'date_inferred' => $transaction_date !== ''],
        ];

        $result = vmo_cfo_record_financial_event($user_id, $event_payload);
        if (!is_wp_error($result) && is_array($result)) {
            $missing = $result['missing_fields'] ?? [];
            $questions = vmo_cfo_follow_up_questions_for_missing(is_array($missing) ? $missing : [], $direction);
            if ($amount > 0 && $direction !== 'opportunity') {
                $questions[] = 'Were any taxes collected or processing fees deducted?';
            }
            $result['follow_up_questions'] = array_values(array_unique($questions));
            $result['message'] = !empty($result['follow_up_questions'])
                ? 'VMO saved this in the AI Financial Inbox and needs a few details before it can be cleanly posted.'
                : 'VMO saved this in the AI Financial Inbox for review.';
        }
        return $result;
    }
}

if (!function_exists('vmo_cfo_post_financial_event')) {
    function vmo_cfo_post_financial_event($user_id, $event_id, $decision_source = 'manual') {
        global $wpdb;
        vmo_cfo_ensure_tables();
        $user_id = absint($user_id);
        $event_id = absint($event_id);
        $events_table = $wpdb->prefix . 'vmo_financial_events';
        $ledger_table = $wpdb->prefix . 'vmo_financial_ledger';
        $event = $wpdb->get_row($wpdb->prepare(
            "SELECT * FROM {$events_table} WHERE id = %d AND user_id = %d LIMIT 1",
            $event_id,
            $user_id
        ), ARRAY_A);
        if (!$event) {
            return new WP_Error('financial_event_missing', 'Financial event not found.');
        }

        $existing_ledger_id = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT id FROM {$ledger_table} WHERE event_id = %d AND user_id = %d LIMIT 1",
            $event_id,
            $user_id
        ));
        if ($existing_ledger_id > 0) {
            return ['ledger_id' => $existing_ledger_id, 'duplicate' => true];
        }

        $purchase_id = (int) ($event['purchase_id'] ?? 0);
        $event_metadata = [];
        if (!empty($event['metadata'])) {
            $decoded_metadata = json_decode((string) $event['metadata'], true);
            $event_metadata = is_array($decoded_metadata) ? $decoded_metadata : [];
        }
        $contact = [];
        if (($event['direction'] ?? '') === 'income' && $purchase_id <= 0 && function_exists('vmo_crm_upsert_contact') && empty($event_metadata['skip_crm_sync'])) {
            $contact = vmo_crm_upsert_contact($user_id, [
                'full_name' => $event['customer_name'] ?? '',
                'email' => $event['customer_email'] ?? '',
                'phone' => $event['customer_phone'] ?? '',
            ], [
                'source' => 'manual_entry',
                'primary_category' => 'lead',
                'lifecycle_stage' => 'new',
                'notes' => 'Financial event captured by CFO pipeline.',
            ]);
            if ($contact && function_exists('vmo_crm_record_purchase')) {
                $purchase = vmo_crm_record_purchase($user_id, [
                    'full_name' => $event['customer_name'] ?? '',
                    'email' => $event['customer_email'] ?? '',
                    'phone' => $event['customer_phone'] ?? '',
                    'source' => 'manual_entry',
                ], [
                    'transaction_id' => $event['source_reference'] ?: ('cfo_event_' . $event_id),
                    'product_service' => $event['product_service'] ?: 'Outside transaction',
                    'purchase_type' => 'service',
                    'amount' => (float) $event['amount'],
                    'currency' => $event['currency'] ?: 'USD',
                    'status' => 'completed',
                    'purchase_date' => $event['transaction_date'] ?: current_time('mysql'),
                    'payment_method' => $event['payment_method'] ?: 'manual',
                    'notes' => $event['notes'] ?: 'Posted from CFO financial event.',
                    'cfo_skip_event' => true,
                ]);
                $purchase_id = (int) ($purchase['purchase_id'] ?? 0);
                if (!empty($purchase['contact']['contact_id'])) {
                    $event['contact_id'] = (string) $purchase['contact']['contact_id'];
                    $event['crm_contact_db_id'] = (int) ($purchase['contact']['id'] ?? 0);
                }
            }
        }

        $amount = (float) $event['amount'];
        $fees = (float) $event['fees_amount'];
        $net = ($event['direction'] ?? '') === 'expense' ? -1 * abs($amount + $fees) : max(0, $amount - $fees);
        $inserted = $wpdb->insert($ledger_table, [
            'user_id' => $user_id,
            'event_id' => $event_id,
            'source' => $event['source'],
            'source_reference' => $event['source_reference'],
            'transaction_type' => $event['event_type'],
            'direction' => $event['direction'],
            'amount' => $amount,
            'tax_amount' => (float) $event['tax_amount'],
            'fees_amount' => $fees,
            'net_amount' => $net,
            'currency' => $event['currency'] ?: 'USD',
            'status' => 'posted',
            'category' => $event['category'],
            'customer_vendor' => $event['direction'] === 'expense' ? $event['vendor_name'] : $event['customer_name'],
            'contact_id' => $event['contact_id'],
            'crm_contact_db_id' => (int) ($event['crm_contact_db_id'] ?? 0) ?: null,
            'invoice_id' => (int) ($event['invoice_id'] ?? 0) ?: null,
            'purchase_id' => $purchase_id ?: null,
            'transaction_id' => $event['source_reference'] ?: ('cfo_event_' . $event_id),
            'product_service' => $event['product_service'],
            'payment_method' => $event['payment_method'],
            'transaction_date' => $event['transaction_date'] ?: current_time('mysql'),
            'metadata' => $event['metadata'],
            'created_at' => current_time('mysql'),
        ]);
        if ($inserted === false) {
            return new WP_Error('financial_ledger_insert_failed', 'Unable to post financial event to ledger.');
        }
        $ledger_id = (int) $wpdb->insert_id;

        $wpdb->update($events_table, [
            'event_status' => 'posted',
            'purchase_id' => $purchase_id ?: null,
            'posted_at' => current_time('mysql'),
            'decided_at' => current_time('mysql'),
            'updated_at' => current_time('mysql'),
        ], ['id' => $event_id, 'user_id' => $user_id]);

        return ['ledger_id' => $ledger_id, 'purchase_id' => $purchase_id, 'duplicate' => false, 'decision_source' => $decision_source];
    }
}

if (!function_exists('vmo_cfo_get_financial_inbox')) {
    function vmo_cfo_get_financial_inbox($user_id, $limit = 8) {
        global $wpdb;
        vmo_cfo_ensure_tables();
        $table = $wpdb->prefix . 'vmo_financial_events';
        if (!function_exists('vmo_table_exists') || !vmo_table_exists($table)) {
            return ['items' => [], 'count' => 0, 'needs_review' => 0];
        }
        $statuses = ['needs_review', 'pending', 'invoice_needed', 'payment_link_needed', 'confirmed'];
        $placeholders = implode(',', array_fill(0, count($statuses), '%s'));
        $params = array_merge([absint($user_id)], $statuses, [max(1, absint($limit))]);
        $items = $wpdb->get_results($wpdb->prepare(
            "SELECT id, event_type, source, direction, amount, currency, payment_status, event_status, confidence, customer_name, vendor_name, payment_method, category, product_service, recommended_action, missing_fields, transaction_date, created_at
             FROM {$table}
             WHERE user_id = %d AND event_status IN ({$placeholders})
             ORDER BY FIELD(event_status, 'confirmed', 'needs_review', 'invoice_needed', 'payment_link_needed', 'pending'), created_at DESC
             LIMIT %d",
            $params
        ), ARRAY_A) ?: [];
        $needs_review = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$table} WHERE user_id = %d AND event_status IN ('needs_review','invoice_needed','payment_link_needed','confirmed','pending')",
            $user_id
        ));
        return ['items' => $items, 'count' => count($items), 'needs_review' => $needs_review];
    }
}

if (!function_exists('vmo_cfo_get_financial_forecasts')) {
    function vmo_cfo_get_financial_forecasts($user_id, array $token_summary, array $plan, array $revenue_metrics) {
        $balance = (int) ($plan['tokens_remaining'] ?? get_user_meta($user_id, 'vmo_token_balance', true));
        $daily_avg = (int) ($token_summary['daily_avg'] ?? 0);
        $runway_days = $daily_avg > 0 ? (int) floor($balance / max(1, $daily_avg)) : null;
        $projected_30d_tokens = $daily_avg * 30;
        $projected_30d_value = round($projected_30d_tokens * vmo_cfo_token_rate_usd(), 2);
        $month_revenue = (float) ($revenue_metrics['month'] ?? 0);
        $year_revenue = (float) ($revenue_metrics['year'] ?? 0);
        $monthly_goal = (float) (get_user_meta($user_id, 'vmo_monthly_revenue_goal', true) ?: get_user_meta($user_id, 'vmo_financial_goal_monthly', true) ?: 0);
        $goal_progress = $monthly_goal > 0 ? round(($month_revenue / $monthly_goal) * 100, 1) : null;
        $ops_ratio = $month_revenue > 0 ? round(($projected_30d_value / $month_revenue) * 100, 1) : null;

        $sustainability = 'unknown';
        if ($month_revenue > 0 && $ops_ratio !== null) {
            $sustainability = $ops_ratio <= 8 ? 'healthy' : ($ops_ratio <= 20 ? 'watch' : 'high_cost');
        } elseif ($projected_30d_tokens > 0 && $month_revenue <= 0) {
            $sustainability = 'needs_revenue_signal';
        }

        return [
            'token_runway_days' => $runway_days,
            'projected_30d_tokens' => $projected_30d_tokens,
            'projected_30d_token_value_usd' => $projected_30d_value,
            'monthly_revenue' => $month_revenue,
            'yearly_revenue' => $year_revenue,
            'monthly_goal' => $monthly_goal,
            'monthly_goal_progress_percent' => $goal_progress,
            'operating_usage_to_revenue_percent' => $ops_ratio,
            'sustainability' => $sustainability,
        ];
    }
}

if (!function_exists('vmo_cfo_build_financial_insights')) {
    function vmo_cfo_build_financial_insights(array $token_summary, array $stripe_status, array $plan, array $forecasts, array $inbox) {
        $parts = [];
        $tokens = (int) ($token_summary['total_tokens'] ?? 0);
        if ($tokens > 0) {
            $costs = $token_summary['cost_by_system'] ?? [];
            arsort($costs);
            $top_system = key($costs) ?: 'AI operations';
            $top_tokens = (int) current($costs);
            $parts[] = 'VMO operating usage consumed ' . number_format_i18n($tokens) . ' tokens in the last 30 days; the largest driver is ' . ucwords(str_replace('-', ' ', $top_system)) . ' at ' . number_format_i18n($top_tokens) . ' tokens.';
        } else {
            $parts[] = 'No token usage was found in the canonical or compatibility ledgers for the last 30 days.';
        }
        if (!empty($forecasts['token_runway_days'])) {
            $parts[] = 'At the current pace, the token balance has about ' . number_format_i18n((int) $forecasts['token_runway_days']) . ' days of runway.';
        }
        if (($forecasts['sustainability'] ?? '') === 'high_cost') {
            $parts[] = 'Operating usage is high relative to recorded revenue; review automations before expanding campaigns or media generation.';
        } elseif (($forecasts['sustainability'] ?? '') === 'healthy') {
            $parts[] = 'Operating usage is currently within a healthy share of recorded revenue.';
        }
        if (!empty($inbox['needs_review'])) {
            $parts[] = 'There are ' . number_format_i18n((int) $inbox['needs_review']) . ' detected financial events waiting for review before they affect official numbers.';
        }
        if (empty($stripe_status['connected'])) {
            $parts[] = 'VMO Pay is not connected, so monetization is blocked until Stripe Connect onboarding starts.';
        } elseif (empty($stripe_status['charges_enabled']) || empty($stripe_status['payouts_enabled'])) {
            $parts[] = 'VMO Pay is connected but not fully operational; resolve Stripe setup blockers before relying on checkout revenue.';
        }
        if (!empty($plan['recommendation'])) {
            $parts[] = $plan['recommendation'];
        }
        return implode(' ', $parts);
    }
}

if (!function_exists('vmo_cfo_get_high_cost_approvals')) {
    function vmo_cfo_get_high_cost_approvals($user_id) {
        global $wpdb;
        $table = $wpdb->prefix . 'vmo_approvals';
        if (!function_exists('vmo_table_exists') || !vmo_table_exists($table)) {
            return ['count' => 0, 'items' => []];
        }
        $types = ['campaign_launch', 'system_activation', 'pricing_change', 'content_publish', 'website_change'];
        $placeholders = implode(',', array_fill(0, count($types), '%s'));
        $params = array_merge([absint($user_id)], $types);
        $items = $wpdb->get_results($wpdb->prepare(
            "SELECT id, title, description, approval_type, source_system, created_at FROM {$table} WHERE user_id = %d AND status = 'pending' AND approval_type IN ({$placeholders}) ORDER BY created_at DESC LIMIT 8",
            $params
        ), ARRAY_A) ?: [];
        return ['count' => count($items), 'items' => $items];
    }
}

// ─── CFO Expansion data ───
add_action('wp_ajax_vmo_get_cfo_expansion', function() {
    if (!current_user_can('read')) wp_send_json_error(['message' => 'Unauthorized'], 403);
    vmo_cfo_verify_ajax_nonce();
    $user_id = get_current_user_id();

    $token_summary = vmo_cfo_get_token_usage_summary($user_id, 30);
    $balance = (int) get_user_meta($user_id, 'vmo_token_balance', true);
    $daily_avg = (int) ($token_summary['daily_avg'] ?? 0);
    $tokens_30d = (int) ($token_summary['total_tokens'] ?? 0);
    $capacity = $daily_avg > 0 ? round($balance / $daily_avg) . ' days' : 'N/A';
    $revenue_metrics = function_exists('vmo_calculate_revenue_metrics') ? vmo_calculate_revenue_metrics($user_id) : ['month' => 0, 'year' => 0];
    $stripe = vmo_cfo_get_stripe_connect_status($user_id);
    $plan = vmo_cfo_get_plan_intelligence($user_id, $token_summary);
    $inbox = vmo_cfo_get_financial_inbox($user_id, 8);
    $approvals = vmo_cfo_get_high_cost_approvals($user_id);
    $forecasts = vmo_cfo_get_financial_forecasts($user_id, $token_summary, $plan, $revenue_metrics);

    // Financial Alerts
    $alerts = [];
    if ($balance <= 100 && $balance > 0) {
        $alerts[] = ['severity' => 'warning', 'title' => 'Low Token Balance', 'message' => 'You have ' . number_format_i18n($balance) . ' tokens remaining. Consider topping up.'];
    }
    if ($balance <= 0) {
        $alerts[] = ['severity' => 'critical', 'title' => 'Token Balance Depleted', 'message' => 'Your token balance is zero. AI features will be limited until you top up.'];
    }
    if (!empty($stripe['setup_blockers'])) {
        $alerts[] = ['severity' => empty($stripe['connected']) ? 'critical' : 'warning', 'title' => 'VMO Pay Setup Blocker', 'message' => implode(' ', $stripe['setup_blockers'])];
    }
    if (!empty($inbox['needs_review'])) {
        $alerts[] = ['severity' => 'warning', 'title' => 'Financial Events Need Review', 'message' => number_format_i18n((int) $inbox['needs_review']) . ' detected financial events are waiting in the AI Financial Inbox.'];
    }
    if (!empty($approvals['count'])) {
        $alerts[] = ['severity' => 'info', 'title' => 'High-Cost Actions Awaiting Approval', 'message' => number_format_i18n((int) $approvals['count']) . ' campaign, system, pricing, or publish decisions are waiting for approval.'];
    }

    $insights = vmo_cfo_build_financial_insights($token_summary, $stripe, $plan, $forecasts, $inbox);

    wp_send_json_success([
        'token_balance'      => $balance,
        'token_capacity'     => $capacity,
        'tokens_used_30d'    => $tokens_30d,
        'token_daily_avg'    => $daily_avg,
        'token_value_30d_usd' => (float) ($token_summary['estimated_value_usd'] ?? 0),
        'cost_by_system'     => $token_summary['cost_by_system'],
        'token_usage_details' => $token_summary['details'],
        'plan_name'          => $plan['plan_name'],
        'plan_key'           => $plan['plan_key'],
        'billing_cycle'      => $plan['billing_cycle'],
        'renewal_date'       => $plan['renewal_date'],
        'billing_status'     => $plan['billing_status'],
        'tokens_included'    => $plan['tokens_included'],
        'tokens_remaining'   => $plan['tokens_remaining'],
        'plan_usage_percent' => $plan['usage_percent'],
        'entitlements'       => $plan['entitlements'],
        'restricted_features'=> $plan['restricted_features'],
        'top_up_available'   => $plan['top_up_available'],
        'plan_recommendation'=> $plan['recommendation'],
        'stripe_status'      => $stripe['status'],
        'stripe_account_id'  => $stripe['stripe_account_id'],
        'stripe_connected'   => $stripe['connected'],
        'details_submitted'  => $stripe['details_submitted'],
        'charges_enabled'    => $stripe['charges_enabled'],
        'payouts_enabled'    => $stripe['payouts_enabled'],
        'stripe_blockers'    => $stripe['setup_blockers'],
        'stripe_requirements_due' => $stripe['requirements_due'],
        'stripe_last_synced' => $stripe['last_synced'],
        'financial_inbox'    => $inbox,
        'high_cost_approvals'=> $approvals,
        'forecasts'          => $forecasts,
        'business_revenue'   => $revenue_metrics,
        'alerts'             => $alerts,
        'financial_insights' => $insights,
    ]);
});

add_action('wp_ajax_vmo_cfo_record_financial_event', function() {
    if (!current_user_can('read')) wp_send_json_error(['message' => 'Unauthorized'], 403);
    vmo_cfo_verify_ajax_nonce();
    $payload = [];
    foreach ($_POST as $key => $value) {
        if (in_array($key, ['action', 'nonce', '_ajax_nonce'], true)) continue;
        $payload[$key] = is_array($value) ? array_map('sanitize_text_field', $value) : sanitize_text_field(wp_unslash($value));
    }
    $payload['source'] = $payload['source'] ?? 'manual_entry';
    $payload['event_status'] = $payload['event_status'] ?? 'needs_review';
    $result = vmo_cfo_record_financial_event(get_current_user_id(), $payload);
    if (is_wp_error($result)) {
        wp_send_json_error(['message' => $result->get_error_message()]);
    }
    wp_send_json_success($result);
});

add_action('wp_ajax_vmo_cfo_capture_financial_instruction', function() {
    if (!current_user_can('read')) wp_send_json_error(['message' => 'Unauthorized'], 403);
    vmo_cfo_verify_ajax_nonce();
    $instruction = sanitize_textarea_field(wp_unslash($_POST['instruction'] ?? ''));
    $result = vmo_cfo_record_financial_instruction(get_current_user_id(), $instruction);
    if (is_wp_error($result)) {
        wp_send_json_error(['message' => $result->get_error_message()]);
    }
    wp_send_json_success($result);
});

add_action('wp_ajax_vmo_cfo_update_financial_event_details', function() {
    if (!current_user_can('read')) wp_send_json_error(['message' => 'Unauthorized'], 403);
    vmo_cfo_verify_ajax_nonce();
    global $wpdb;
    $user_id = get_current_user_id();
    $event_id = absint($_POST['event_id'] ?? 0);
    $instruction = sanitize_textarea_field(wp_unslash($_POST['instruction'] ?? ''));
    if (!$event_id || trim($instruction) === '') {
        wp_send_json_error(['message' => 'Choose a financial event and add the missing details.']);
    }
    $event = vmo_cfo_get_financial_event($user_id, $event_id);
    if (!$event) {
        wp_send_json_error(['message' => 'Financial event not found.']);
    }

    $parsed = vmo_cfo_parse_financial_instruction_details($instruction, $event['direction'] ?? 'income');
    $updates = [];
    foreach (['direction', 'payment_method', 'product_service', 'customer_name', 'vendor_name', 'transaction_date'] as $field) {
        if (!empty($parsed[$field])) {
            $updates[$field] = sanitize_text_field($parsed[$field]);
        }
    }
    if (!empty($parsed['amount']) && (float) $parsed['amount'] > 0) {
        $updates['amount'] = round((float) $parsed['amount'], 2);
    }
    if (empty($event['category']) && !empty($updates['direction'])) {
        $updates['category'] = $updates['direction'] === 'expense' ? 'operating_expense' : 'sales';
    }
    if (!$updates) {
        wp_send_json_error(['message' => 'VMO could not identify new financial details in that answer.']);
    }

    $merged_event = array_merge($event, $updates);
    $missing = vmo_cfo_required_fields_for_event($merged_event);
    $metadata = [];
    if (!empty($event['metadata'])) {
        $decoded = json_decode((string) $event['metadata'], true);
        $metadata = is_array($decoded) ? $decoded : [];
    }
    $history = is_array($metadata['slot_fill_history'] ?? null) ? $metadata['slot_fill_history'] : [];
    $history[] = ['answer' => $instruction, 'updates' => $updates, 'at' => current_time('mysql')];
    $updates['metadata'] = wp_json_encode(array_merge($metadata, ['slot_fill_history' => $history]));
    $updates['missing_fields'] = $missing ? implode(',', $missing) : null;
    $updates['recommended_action'] = $missing ? 'ask_follow_up' : 'review';
    $updates['event_status'] = 'needs_review';
    $updates['updated_at'] = current_time('mysql');

    $events_table = $wpdb->prefix . 'vmo_financial_events';
    $updated = $wpdb->update($events_table, $updates, ['id' => $event_id, 'user_id' => $user_id]);
    if ($updated === false) {
        wp_send_json_error(['message' => 'Unable to update financial event details.']);
    }

    $questions = vmo_cfo_follow_up_questions_for_missing($missing, $merged_event['direction'] ?? 'income');
    wp_send_json_success([
        'event_id' => $event_id,
        'updated_fields' => array_keys(array_diff_key($updates, array_flip(['metadata', 'missing_fields', 'recommended_action', 'event_status', 'updated_at']))),
        'missing_fields' => $missing,
        'follow_up_questions' => $questions,
        'message' => $questions ? 'VMO updated the event and still needs a few details.' : 'VMO updated the event. It is ready for review.',
    ]);
});

add_action('wp_ajax_vmo_cfo_review_financial_event', function() {
    if (!current_user_can('read')) wp_send_json_error(['message' => 'Unauthorized'], 403);
    vmo_cfo_verify_ajax_nonce();
    global $wpdb;
    $user_id = get_current_user_id();
    $event_id = absint($_POST['event_id'] ?? 0);
    $decision = sanitize_key($_POST['decision'] ?? '');
    $events_table = $wpdb->prefix . 'vmo_financial_events';
    if (!$event_id || !in_array($decision, ['approve', 'reject', 'needs_invoice', 'payment_link_needed', 'needs_review', 'create_invoice', 'create_payment_link'], true)) {
        wp_send_json_error(['message' => 'Invalid financial event decision.']);
    }

    if ($decision === 'approve') {
        $result = vmo_cfo_post_financial_event($user_id, $event_id, 'review');
        if (is_wp_error($result)) {
            wp_send_json_error(['message' => $result->get_error_message()]);
        }
        wp_send_json_success($result);
    }

    if ($decision === 'create_invoice') {
        $result = vmo_cfo_create_invoice_for_event($user_id, $event_id);
        if (is_wp_error($result)) {
            wp_send_json_error(['message' => $result->get_error_message()]);
        }
        wp_send_json_success($result);
    }

    if ($decision === 'create_payment_link') {
        $result = vmo_cfo_create_payment_link_for_event($user_id, $event_id);
        if (is_wp_error($result)) {
            wp_send_json_error(['message' => $result->get_error_message()]);
        }
        wp_send_json_success($result);
    }

    $status_map = [
        'reject' => 'rejected',
        'needs_invoice' => 'invoice_needed',
        'payment_link_needed' => 'payment_link_needed',
        'needs_review' => 'needs_review',
    ];
    $updated = $wpdb->update($events_table, [
        'event_status' => $status_map[$decision],
        'recommended_action' => $decision,
        'decided_at' => current_time('mysql'),
        'updated_at' => current_time('mysql'),
    ], ['id' => $event_id, 'user_id' => $user_id]);

    if ($updated === false) {
        wp_send_json_error(['message' => 'Unable to update financial event.']);
    }
    wp_send_json_success(['event_id' => $event_id, 'event_status' => $status_map[$decision]]);
});

// ══════════════════════════════════════════════════════════════════
// AUTO-DEDUCTION: Automatically complete matching daily tasks when
// verifiable actions occur (blog published, page created, etc.)
// ══════════════════════════════════════════════════════════════════

/**
 * Helper – Find and auto-complete today's daily-activity task matching keywords.
 */
function vmo_auto_complete_matching_task( int $user_id, array $keywords ): bool {
    global $wpdb;
    $table = $wpdb->prefix . 'vmo_daily_activities';
    if ( ! vmo_table_exists( $table ) ) return false;

    $today = current_time( 'Y-m-d' );
    $tasks = $wpdb->get_results( $wpdb->prepare(
        "SELECT activity_id, activity_title, activity_description FROM {$table}
         WHERE user_id = %d AND activity_date <= %s AND status = 'pending'
         ORDER BY activity_date ASC LIMIT 50",
        $user_id, $today
    ), ARRAY_A );

    foreach ( $tasks as $t ) {
        $haystack = strtolower( ( $t['activity_title'] ?? '' ) . ' ' . ( $t['activity_description'] ?? '' ) );
        foreach ( $keywords as $kw ) {
            if ( strpos( $haystack, strtolower( $kw ) ) !== false ) {
                $wpdb->update( $table, [
                    'status'              => 'completed',
                    'actual_completed_at' => current_time( 'mysql' ),
                    'completion_notes'    => 'Auto-completed: detected matching action (' . $kw . ')',
                    'updated_at'          => current_time( 'mysql' ),
                ], [ 'activity_id' => $t['activity_id'], 'user_id' => $user_id ] );
                return true;
            }
        }
    }
    return false;
}

// When a post/page is published, auto-complete matching tasks
add_action( 'transition_post_status', function( $new_status, $old_status, $post ) {
    if ( $new_status !== 'publish' || $old_status === 'publish' ) return;
    $user_id = (int) $post->post_author;
    if ( $user_id <= 0 ) return;

    $type = $post->post_type;
    $title_lower = strtolower( $post->post_title );
    $keywords = [];

    if ( $type === 'post' ) {
        $keywords = [ 'blog', 'publish post', 'write blog', 'create blog', 'article', 'blog post' ];
    } elseif ( $type === 'page' ) {
        $keywords = [ 'page', 'create page', 'build page', 'landing page', 'publish page' ];
    } elseif ( $type === 'product' ) {
        $keywords = [ 'product', 'add product', 'create product', 'list product' ];
    }

    // Also match by the actual post title
    if ( ! empty( $title_lower ) ) {
        $keywords[] = $title_lower;
    }

    if ( ! empty( $keywords ) ) {
        vmo_auto_complete_matching_task( $user_id, $keywords );
    }
}, 10, 3 );

// When an email is sent through VMO, auto-complete matching tasks
add_action( 'vmo_email_sent', function( $user_id, $to, $subject ) {
    $keywords = [ 'email', 'send email', 'email campaign', 'newsletter', 'follow-up email', 'follow up' ];
    if ( ! empty( $subject ) ) {
        $keywords[] = strtolower( $subject );
    }
    vmo_auto_complete_matching_task( (int) $user_id, $keywords );
}, 10, 3 );

// When SEO recommendations are executed, auto-complete matching tasks
add_action( 'vmo_seo_executed', function( $user_id ) {
    vmo_auto_complete_matching_task( (int) $user_id, [ 'seo', 'optimize seo', 'seo audit', 'keywords', 'meta tags' ] );
}, 10, 1 );

// ── SMS Report → Actions pipeline ──────────────────────────────
// When an inbound SMS is received, create a follow-up action
add_action( 'vmo_sms_received', function( $user_id, $from, $body, $message_sid ) {
    global $wpdb;
    $actions_table = $wpdb->prefix . 'vmo_actions';
    if ( ! vmo_table_exists( $actions_table ) ) return;

    // Only create action for substantive messages (not opt-in/out keywords)
    $body_lower = strtolower( trim( $body ) );
    if ( in_array( $body_lower, [ 'stop', 'start', 'help', 'yes', 'no', 'unsubscribe' ], true ) ) return;
    if ( strlen( $body ) < 5 ) return;

    // Don't duplicate for same message
    $exists = $wpdb->get_var( $wpdb->prepare(
        "SELECT id FROM {$actions_table} WHERE user_id = %d AND source = 'sms_report' AND source_id = %s LIMIT 1",
        $user_id, $message_sid
    ) );
    if ( $exists ) return;

    $preview = mb_substr( $body, 0, 80 );
    $wpdb->insert( $actions_table, [
        'user_id'     => (int) $user_id,
        'title'       => 'Reply to SMS from ' . sanitize_text_field( $from ),
        'description' => 'Inbound SMS: "' . sanitize_text_field( $preview ) . ( strlen( $body ) > 80 ? '…' : '' ) . '"',
        'source'      => 'sms_report',
        'source_id'   => sanitize_text_field( $message_sid ),
        'priority'    => 'normal',
        'category'    => 'follow-up',
        'status'      => 'pending',
        'due_date'    => current_time( 'Y-m-d' ),
        'created_at'  => current_time( 'mysql' ),
        'updated_at'  => current_time( 'mysql' ),
    ] );
}, 10, 4 );

// ── Auto-create CRM contact from inbound SMS ──────────────────
add_action( 'vmo_sms_received', function( $user_id, $from, $body, $message_sid ) {
    if ( function_exists( 'vmo_upsert_contact' ) && $from ) {
        $cid = vmo_upsert_contact( (int) $user_id, [
            'phone'            => $from,
            'source'           => 'phone_call',
            'primary_category' => 'inbound_lead',
            'lifecycle_stage'  => 'new',
            'notes'            => 'Auto-created from inbound SMS',
        ] );
        // Log the interaction for retention tracking
        if ( $cid && function_exists( 'vmo_log_interaction' ) ) {
            vmo_log_interaction( (int) $user_id, $cid, [
                'interaction_type' => 'phone_call',
                'subtype'          => 'sms_inbound',
                'direction'        => 'inbound',
                'subject'          => 'SMS from ' . $from,
                'summary'          => mb_substr( $body, 0, 200 ),
                'outcome'          => 'needs_follow_up',
            ] );
        }
    }
}, 9, 4 );

// ── Auto-log email interactions for CRM retention ──────────────
add_action( 'vmo_email_sent', function( $user_id, $recipients, $subject ) {
    if ( ! function_exists( 'vmo_log_interaction' ) || ! function_exists( 'vmo_upsert_contact' ) ) return;
    $emails = array_slice( explode( ',', $recipients ), 0, 5 );
    foreach ( $emails as $email ) {
        $email = trim( $email );
        if ( ! is_email( $email ) ) continue;
        $cid = vmo_upsert_contact( (int) $user_id, [
            'email'  => $email,
            'source' => 'email_marketing',
            'primary_category' => 'prospect',
            'lifecycle_stage'  => 'engaged',
        ] );
        if ( $cid ) {
            vmo_log_interaction( (int) $user_id, $cid, [
                'interaction_type' => 'email',
                'subtype'          => 'campaign',
                'direction'        => 'outbound',
                'subject'          => $subject,
                'summary'          => 'Marketing email campaign sent',
                'outcome'          => 'pending',
            ] );
        }
    }
}, 10, 3 );

// ── Power Dialer: AI Sales Script Generation ───────────────────
add_action( 'wp_ajax_vmo_generate_sales_script', function() {
    check_ajax_referer( 'vmo_nonce', '_ajax_nonce' );
    if ( ! current_user_can( 'read' ) ) {
        wp_send_json_error( [ 'message' => 'Unauthorized' ], 403 );
    }

    $intent = sanitize_text_field( wp_unslash( $_POST['intent'] ?? '' ) );
    $goal   = sanitize_text_field( wp_unslash( $_POST['goal'] ?? '' ) );
    if ( ! $intent || ! $goal ) {
        wp_send_json_error( [ 'message' => 'Intent and goal are required.' ] );
    }

    $prompt = "You are a world-class sales script writer. Generate a professional cold-call sales script for a phone campaign.\n\n"
            . "User Intent / What they're selling: {$intent}\n"
            . "End Goal / Desired outcome: {$goal}\n\n"
            . "Format the script with clearly labeled sections:\n"
            . "- OPENING (the hook, first 10 seconds)\n"
            . "- BRIDGE (transition to value prop)\n"
            . "- PROBLEM/AGITATE (identify pain points)\n"
            . "- SOLUTION (present the offer)\n"
            . "- CLOSE (call to action)\n"
            . "- OBJECTION HANDLING (3 common objections with responses)\n\n"
            . "Make it natural, conversational, and persuasive. Use [Your Name] and [Your Company] as placeholders.";

    $api_key = get_option( 'vmo_openai_api_key', '' );
    if ( ! $api_key ) {
        wp_send_json_error( [ 'message' => 'OpenAI API key not configured.' ] );
    }

    $model = function_exists('vmo_get_model') ? vmo_get_model('mini') : (defined('VMO_MODEL_MINI') ? VMO_MODEL_MINI : 'gpt-5.5-mini');
    $body = wp_json_encode( array_merge( [
        'model'    => $model,
        'messages' => [
            [ 'role' => 'system', 'content' => 'You are a sales script expert. Output only the script text, no markdown fences.' ],
            [ 'role' => 'user',   'content' => $prompt ],
        ],
        'temperature' => 0.7,
    ], function_exists('vmo_get_token_param') ? vmo_get_token_param($model, 1200) : [ 'max_completion_tokens' => 1200 ] ) );

    $resp = wp_remote_post( 'https://api.openai.com/v1/chat/completions', [
        'timeout' => 30,
        'headers' => [
            'Content-Type'  => 'application/json',
            'Authorization' => 'Bearer ' . $api_key,
        ],
        'body' => $body,
    ] );

    if ( is_wp_error( $resp ) ) {
        wp_send_json_error( [ 'message' => 'API request failed: ' . $resp->get_error_message() ] );
    }

    $data = json_decode( wp_remote_retrieve_body( $resp ), true );
    $script = $data['choices'][0]['message']['content'] ?? '';
    if ( ! $script ) {
        wp_send_json_error( [ 'message' => 'Empty response from AI.' ] );
    }

    // ── ZERO-LEAK METERING: charge the paid OpenAI generation now that it
    // succeeded. Internal/unmetered users bypass inside vmo_deduct_tokens.
    if ( function_exists( 'vmo_deduct_tokens' ) ) {
        $script_cost = function_exists( 'vmo_get_token_cost' ) ? (int) vmo_get_token_cost( 'sales_script' ) : 216;
        if ( $script_cost < 1 ) { $script_cost = 216; }
        vmo_deduct_tokens( get_current_user_id(), $script_cost, 'sales_script', 'AI sales script generation' );
    }

    wp_send_json_success( [ 'script' => $script ] );
} );

// ── CRM: Get contact interaction history ───────────────────────
add_action( 'wp_ajax_vmo_get_contact_interactions', function() {
    check_ajax_referer( 'vmo_nonce', '_ajax_nonce' );
    if ( ! current_user_can( 'read' ) ) wp_send_json_error( [ 'message' => 'Unauthorized' ], 403 );

    $contact_id = sanitize_text_field( wp_unslash( $_POST['contact_id'] ?? '' ) );
    if ( ! $contact_id ) wp_send_json_error( [ 'message' => 'contact_id required' ] );

    global $wpdb;
    $user_id = get_current_user_id();
    $table = $wpdb->prefix . 'vmo_contact_interactions';
    if ( ! vmo_table_exists( $table ) ) wp_send_json_success( [ 'interactions' => [] ] );

    $rows = $wpdb->get_results( $wpdb->prepare(
        "SELECT * FROM {$table} WHERE user_id = %d AND contact_id = %s ORDER BY created_at DESC LIMIT 100",
        $user_id, $contact_id
    ), ARRAY_A );

    wp_send_json_success( [ 'interactions' => $rows ?: [] ] );
} );

// ── CRM: Add manual retention action / interaction ─────────────
add_action( 'wp_ajax_vmo_add_retention_action', function() {
    check_ajax_referer( 'vmo_nonce', '_ajax_nonce' );
    if ( ! current_user_can( 'read' ) ) wp_send_json_error( [ 'message' => 'Unauthorized' ], 403 );

    $contact_id = sanitize_text_field( wp_unslash( $_POST['contact_id'] ?? '' ) );
    $type       = sanitize_key( wp_unslash( $_POST['type'] ?? 'follow_up' ) );
    $summary    = sanitize_textarea_field( wp_unslash( $_POST['summary'] ?? '' ) );
    $outcome    = sanitize_key( wp_unslash( $_POST['outcome'] ?? 'pending' ) );

    if ( ! $contact_id || ! $summary ) {
        wp_send_json_error( [ 'message' => 'contact_id and summary required' ] );
    }

    $user_id = get_current_user_id();
    if ( function_exists( 'vmo_log_interaction' ) ) {
        $ok = vmo_log_interaction( $user_id, $contact_id, [
            'interaction_type' => $type,
            'subtype'          => 'retention',
            'direction'        => 'outbound',
            'summary'          => $summary,
            'outcome'          => $outcome,
            'next_action'      => sanitize_text_field( wp_unslash( $_POST['next_action'] ?? '' ) ),
            'follow_up_date'   => sanitize_text_field( wp_unslash( $_POST['follow_up_date'] ?? '' ) ),
        ] );
        if ( $ok ) {
            wp_send_json_success( [ 'message' => 'Retention action logged' ] );
        }
    }
    wp_send_json_error( [ 'message' => 'Failed to log action' ] );
} );

// ── CRM: AI relationship summary for communications modal ──────
add_action( 'wp_ajax_vmo_contact_ai_summary', function() {
    check_ajax_referer( 'vmo_nonce', '_ajax_nonce' );
    if ( ! current_user_can( 'read' ) ) wp_send_json_error( [ 'message' => 'Unauthorized' ], 403 );

    $contact_id = sanitize_text_field( wp_unslash( $_POST['contact_id'] ?? '' ) );
    if ( ! $contact_id ) wp_send_json_error( [ 'message' => 'contact_id required' ] );

    global $wpdb;
    $user_id = get_current_user_id();

    // Gather contact info
    $contacts_table = $wpdb->prefix . 'vmo_contacts';
    $contact = $wpdb->get_row( $wpdb->prepare(
        "SELECT * FROM {$contacts_table} WHERE user_id = %d AND contact_id = %s LIMIT 1",
        $user_id, $contact_id
    ), ARRAY_A );
    if ( ! $contact ) wp_send_json_error( [ 'message' => 'Contact not found' ] );

    // Gather interaction history
    $int_table = $wpdb->prefix . 'vmo_contact_interactions';
    $interactions = [];
    if ( vmo_table_exists( $int_table ) ) {
        $interactions = $wpdb->get_results( $wpdb->prepare(
            "SELECT interaction_type, interaction_subtype, direction, subject, summary, outcome, created_at FROM {$int_table} WHERE user_id = %d AND contact_id = %s ORDER BY created_at DESC LIMIT 30",
            $user_id, $contact_id
        ), ARRAY_A ) ?: [];
    }

    // Gather purchase history
    $purchases_table = $wpdb->prefix . 'vmo_contact_purchases';
    $purchases = [];
    if ( vmo_table_exists( $purchases_table ) ) {
        $purchases = $wpdb->get_results( $wpdb->prepare(
            "SELECT product_service, amount, currency, purchase_date FROM {$purchases_table} WHERE user_id = %d AND contact_id = %s ORDER BY purchase_date DESC LIMIT 10",
            $user_id, $contact_id
        ), ARRAY_A ) ?: [];
    }

    // Build contextual prompt
    $context = "Contact: " . ( $contact['full_name'] ?: 'Unknown' ) . "\n"
             . "Email: " . ( $contact['email'] ?: 'N/A' ) . " | Phone: " . ( $contact['phone'] ?: 'N/A' ) . "\n"
             . "Company: " . ( $contact['company'] ?: 'N/A' ) . " | Stage: " . ( $contact['lifecycle_stage'] ?: 'unknown' ) . "\n"
             . "Since: " . ( $contact['created_at'] ?: 'unknown' ) . "\n\n";

    if ( $interactions ) {
        $context .= "Recent Interactions:\n";
        foreach ( $interactions as $i ) {
            $context .= "- [{$i['created_at']}] {$i['interaction_type']}" . ( $i['direction'] ? " ({$i['direction']})" : '' ) . ": {$i['summary']} → {$i['outcome']}\n";
        }
    }
    if ( $purchases ) {
        $context .= "\nPurchase History:\n";
        foreach ( $purchases as $p ) {
            $context .= "- [{$p['purchase_date']}] {$p['product_service']}: {$p['amount']} {$p['currency']}\n";
        }
    }

    $api_key = get_option( 'vmo_openai_api_key', '' );
    if ( ! $api_key ) {
        wp_send_json_error( [ 'message' => 'OpenAI API key not configured' ] );
    }

    $model = function_exists('vmo_get_model') ? vmo_get_model('mini') : (defined('VMO_MODEL_MINI') ? VMO_MODEL_MINI : 'gpt-5.5-mini');
    $resp = wp_remote_post( 'https://api.openai.com/v1/chat/completions', [
        'timeout' => 25,
        'headers' => [
            'Content-Type'  => 'application/json',
            'Authorization' => 'Bearer ' . $api_key,
        ],
        'body' => wp_json_encode( array_merge( [
            'model'       => $model,
            'messages'    => [
                [ 'role' => 'system', 'content' => 'You are a CRM analyst. Given a contact profile and interaction history, provide a concise 3-paragraph relationship summary: (1) Who they are and their value, (2) Communication timeline highlights, (3) Recommended next action. Keep it under 150 words.' ],
                [ 'role' => 'user', 'content' => $context ],
            ],
            'temperature' => 0.5,
        ], function_exists('vmo_get_token_param') ? vmo_get_token_param($model, 300) : [ 'max_completion_tokens' => 300 ] ) ),
    ] );

    if ( is_wp_error( $resp ) ) {
        wp_send_json_error( [ 'message' => 'AI request failed' ] );
    }
    $data = json_decode( wp_remote_retrieve_body( $resp ), true );
    $summary = $data['choices'][0]['message']['content'] ?? '';
    if ( ! $summary ) {
        wp_send_json_error( [ 'message' => 'Empty AI response' ] );
    }

    // ── ZERO-LEAK METERING: charge the paid OpenAI summary now that it
    // succeeded. Internal/unmetered users bypass inside vmo_deduct_tokens.
    if ( function_exists( 'vmo_deduct_tokens' ) ) {
        $summary_cost = function_exists( 'vmo_get_token_cost' ) ? (int) vmo_get_token_cost( 'contact_summary' ) : 126;
        if ( $summary_cost < 1 ) { $summary_cost = 126; }
        vmo_deduct_tokens( $user_id, $summary_cost, 'contact_summary', 'CRM contact AI summary' );
    }

    wp_send_json_success( [
        'summary'      => $summary,
        'contact'      => [
            'name'      => $contact['full_name'] ?: 'Unknown',
            'email'     => $contact['email'],
            'phone'     => $contact['phone'],
            'company'   => $contact['company'],
            'stage'     => $contact['lifecycle_stage'],
            'since'     => $contact['created_at'],
        ],
        'interactions_count' => count( $interactions ),
        'purchases_count'    => count( $purchases ),
    ] );
} );

// ─── Daily Briefing for Jarvis ───
add_action('wp_ajax_vmo_get_daily_briefing', function() {
    if (!current_user_can('read')) wp_send_json_error(['message' => 'Unauthorized'], 403);
    $user_id = get_current_user_id();
    $user = wp_get_current_user();
    global $wpdb;

    $today = current_time('Y-m-d');
    $table = $wpdb->prefix . 'vmo_follow_ups';

    // Today's tasks
    $tasks = $wpdb->get_results($wpdb->prepare(
        "SELECT description, priority, status FROM {$table} WHERE user_id = %d AND DATE(due_date) = %s ORDER BY priority DESC",
        $user_id, $today
    ));
    $pending = 0; $completed = 0;
    $todays_goals = [];
    if ($tasks) {
        foreach ($tasks as $t) {
            if ($t->status === 'completed') { $completed++; }
            else { $pending++; if (count($todays_goals) < 3) $todays_goals[] = $t->description; }
        }
    }

    // Overdue tasks = urgent items
    $urgent_items = [];
    $overdue = $wpdb->get_results($wpdb->prepare(
        "SELECT description FROM {$table} WHERE user_id = %d AND DATE(due_date) < %s AND status != 'completed' ORDER BY due_date ASC LIMIT 3",
        $user_id, $today
    ));
    if ($overdue) {
        foreach ($overdue as $o) {
            $urgent_items[] = $o->description . ' (overdue)';
        }
    }

    // Low token balance alert
    $balance = (int) get_user_meta($user_id, 'vmo_token_balance', true);
    if ($balance > 0 && $balance <= 100) {
        $urgent_items[] = 'Token balance low (' . number_format_i18n($balance) . ' remaining)';
    }

    // Streak
    $streak = (int) get_user_meta($user_id, 'vmo_daily_streak', true);

    // Summary
    $summary = $pending . ' task' . ($pending !== 1 ? 's' : '') . ' for today';
    if ($completed > 0) $summary .= ', ' . $completed . ' already done';
    $summary .= '.';

    // Roadmap progress
    $roadmap = get_user_meta($user_id, 'vmo_business_roadmap', true);
    if (!empty($roadmap)) {
        if (is_string($roadmap)) $roadmap = json_decode($roadmap, true);
        $total_m = 0; $done_m = 0;
        if (!empty($roadmap['milestones'])) {
            foreach ($roadmap['milestones'] as $m) {
                $total_m++;
                if (!empty($m['completed'])) $done_m++;
            }
        }
        if ($total_m > 0) {
            $pct = round(($done_m / $total_m) * 100);
            $summary .= ' Business roadmap: ' . $pct . '% complete.';
        }
    }

    wp_send_json_success([
        'user_name'     => $user->display_name,
        'summary'       => $summary,
        'todays_goals'  => $todays_goals,
        'urgent_items'  => $urgent_items,
        'streak'        => $streak,
        'pending_tasks' => $pending,
        'completed_tasks' => $completed,
    ]);
});

// ─── Email Template Library ───
add_action('wp_ajax_vmo_get_email_template', function() {
    if (!current_user_can('read')) wp_send_json_error(['message' => 'Unauthorized'], 403);
    $type = isset($_POST['template_type']) ? sanitize_text_field($_POST['template_type']) : '';
    $user = wp_get_current_user();
    $site_name = get_bloginfo('name') ?: 'Our Company';
    $first_name = $user->first_name ?: $user->display_name;

    $templates = [
        'welcome' => [
            'subject' => "Welcome to {$site_name}! 🎉",
            'body'    => "Hi [Name],\n\nThank you for joining {$site_name}! We're thrilled to have you.\n\nHere's what you can do next:\n• Explore our services\n• Set up your profile\n• Check out our latest offerings\n\nIf you have any questions, simply reply to this email.\n\nWarm regards,\n{$first_name}\n{$site_name}",
        ],
        'follow-up' => [
            'subject' => "Great speaking with you! — {$site_name}",
            'body'    => "Hi [Name],\n\nThank you for taking the time to [meet/speak] with us today. It was great learning about your needs.\n\nAs discussed:\n• [Key point 1]\n• [Key point 2]\n• [Next steps]\n\nPlease don't hesitate to reach out if you have any questions.\n\nBest regards,\n{$first_name}\n{$site_name}",
        ],
        'newsletter' => [
            'subject' => "📰 {$site_name} Monthly Update — [Month Year]",
            'body'    => "Hi [Name],\n\nHere's what's been happening at {$site_name} this month:\n\n📌 HIGHLIGHTS\n• [Update 1]\n• [Update 2]\n\n📊 BY THE NUMBERS\n• [Stat 1]\n• [Stat 2]\n\n🔮 COMING SOON\n• [Upcoming feature/event]\n\nThank you for being part of our community!\n\n{$first_name}\n{$site_name}",
        ],
        'promotion' => [
            'subject' => "🎁 Special Offer Just for You — {$site_name}",
            'body'    => "Hi [Name],\n\nWe have an exclusive offer we think you'll love!\n\n🎉 [OFFER DETAILS]\n[Describe the promotion, discount, or special deal]\n\n⏰ This offer expires [DATE].\n\n👉 [CTA: Shop Now / Book Now / Learn More]\n\nDon't miss out!\n\n{$first_name}\n{$site_name}",
        ],
        'thank-you' => [
            'subject' => "Thank you for your purchase! 🙏 — {$site_name}",
            'body'    => "Hi [Name],\n\nThank you for your recent purchase from {$site_name}!\n\n📦 Order Details:\n• [Product/Service]\n• [Amount]\n\nYour order is being processed and you'll receive a confirmation shortly.\n\nIf you have any questions about your order, simply reply to this email.\n\nThank you for choosing us!\n\n{$first_name}\n{$site_name}",
        ],
        'appointment' => [
            'subject' => "📅 Appointment Confirmed — {$site_name}",
            'body'    => "Hi [Name],\n\nYour appointment has been confirmed!\n\n📅 Date: [DATE]\n🕐 Time: [TIME]\n📍 Location: [LOCATION/ZOOM LINK]\n\nPlease arrive 5 minutes early. If you need to reschedule, please let us know at least 24 hours in advance.\n\nLooking forward to seeing you!\n\n{$first_name}\n{$site_name}",
        ],
    ];

    if (isset($templates[$type])) {
        wp_send_json_success($templates[$type]);
    } else {
        wp_send_json_error(['message' => 'Unknown template type']);
    }
});

// VMO_JERREME_PRONUNCIATION_NOTE_v1
