<?php
// includes/token-manager.php
if (!defined('ABSPATH')) exit;

/**
 * VMO Token Economics v8.2.0 - Next-Generation Pricing Model
 * 
 * New Token Structure:
 * - Complete Webpage: 2,500 VMO tokens (hero + 4 sections + menu + form)
 * - Full Website Generation: 26,250 VMO tokens (25x markup)
 * - Single Section: 400-600 VMO tokens (average 500)
 * - AI Refinements: 150-300 VMO tokens (average 225)
 * - Image Generation: 200-400 VMO tokens (average 300)
 * - Advanced Effects: 100-250 VMO tokens (average 175)
 * 
 * Publishing Costs:
 * - Free Users: 625 VMO tokens per page
 * - Subscribers: 500 VMO tokens per page  
 * - Top-Up Users: 550 VMO tokens per page
 * 
 * Token Rollover Policy:
 * - Subscriber Credits: 6 months rollover
 * - Top-Up Credits: 3 months rollover
 * - Free Trial: 7 days expiration
 * 
 * Rate: 1 VMO token = $0.000667
 * Markup: 7-9x standard, 25x for full website generation
 */

// ═══════════════════════════════════════════════════════════════════════════════
// GUARDRAIL 5A: Adaptive Model Fallback
// Automatically downgrades from strategic to fast model when balance is low.
// Threshold: 5,000 tokens (~$3.33 in wallet value). Below this, the system
// uses the cheaper/faster model to stretch remaining balance.
// ═══════════════════════════════════════════════════════════════════════════════

/** Balance threshold (in tokens) below which we downgrade to cheaper model */
if (!defined('VMO_LOW_BALANCE_MODEL_THRESHOLD')) {
    define('VMO_LOW_BALANCE_MODEL_THRESHOLD', 5000);
}

if (!defined('VMO_LOW_CREDIT_REMAINING_PERCENT')) {
    define('VMO_LOW_CREDIT_REMAINING_PERCENT', 40);
}

if (!defined('VMO_CRITICAL_CREDIT_REMAINING_PERCENT')) {
    define('VMO_CRITICAL_CREDIT_REMAINING_PERCENT', 20);
}

if (!defined('VMO_RECURRING_SERVICE_GRACE_DAYS')) {
    define('VMO_RECURRING_SERVICE_GRACE_DAYS', 2);
}

/**
 * Resolve which OpenAI model to use based on the user's token balance.
 *
 * When balance is above VMO_LOW_BALANCE_MODEL_THRESHOLD, returns the
 * requested (or strategic) model. When below, returns the fast model
 * to conserve tokens.
 *
 * @param int    $user_id         WordPress user ID (0 = current user)
 * @param string $preferred_model The caller's preferred model (default: strategic)
 * @return array ['model' => string, 'downgraded' => bool, 'reason' => string]
 */
function vmo_resolve_model($user_id = 0, $preferred_model = '') {
    if (!$user_id) {
        $user_id = get_current_user_id();
    }

    if (function_exists('vmo_is_internal_unmetered_user') && vmo_is_internal_unmetered_user($user_id)) {
        $model = $preferred_model ?: (function_exists('vmo_get_model') ? vmo_get_model('strategic') : (defined('VMO_OPENAI_STRATEGIC_MODEL') ? VMO_OPENAI_STRATEGIC_MODEL : 'gpt-5.5'));
        return ['model' => $model, 'downgraded' => false, 'reason' => 'admin'];
    }

    $balance = vmo_get_token_balance($user_id);
    $threshold = defined('VMO_LOW_BALANCE_MODEL_THRESHOLD') ? VMO_LOW_BALANCE_MODEL_THRESHOLD : 5000;
    $fast_model = function_exists('vmo_get_model') ? vmo_get_model('fast') : (defined('VMO_OPENAI_FAST_MODEL') ? VMO_OPENAI_FAST_MODEL : 'gpt-5.5-nano');
    $strategic_model = function_exists('vmo_get_model') ? vmo_get_model('strategic') : (defined('VMO_OPENAI_STRATEGIC_MODEL') ? VMO_OPENAI_STRATEGIC_MODEL : 'gpt-5.5');

    if (empty($preferred_model)) {
        $preferred_model = $strategic_model;
    }

    // If the caller already wants the fast model, no downgrade needed
    if ($preferred_model === $fast_model) {
        return ['model' => $fast_model, 'downgraded' => false, 'reason' => 'already_fast'];
    }

    // Low balance → force downgrade to fast model
    if ($balance < $threshold) {
        error_log(sprintf(
            'VMO Model Fallback: User %d balance %d < %d threshold — downgrading from %s to %s',
            $user_id, $balance, $threshold, $preferred_model, $fast_model
        ));
        return [
            'model'      => $fast_model,
            'downgraded' => true,
            'reason'     => sprintf('Balance (%s tokens) is below %s threshold — using economy model to conserve credits.', number_format($balance), number_format($threshold)),
        ];
    }

    return ['model' => $preferred_model, 'downgraded' => false, 'reason' => 'sufficient_balance'];
}

/**
 * Update user token balance.
 *
 * @param int $user_id
 * @param int $amount
 * @param string $op 'add' or 'deduct'
 */
function vmo_update_token_balance($user_id, $amount, $op = 'add') {
    $current = (int) get_user_meta($user_id, 'vmo_token_balance', true);
    $new_balance = ($op === 'add') ? $current + $amount : max(0, $current - $amount);
    update_user_meta($user_id, 'vmo_token_balance', $new_balance);
    if (function_exists('vmo_sync_license_credit_balance')) {
        vmo_sync_license_credit_balance($user_id, $new_balance);
    }
    if ($op === 'add' && $new_balance > 0 && function_exists('vmo_can_user_use_cost_bearing_services') && vmo_can_user_use_cost_bearing_services($user_id)) {
        vmo_clear_recurring_service_grace($user_id);
    } elseif ($op !== 'add' && $new_balance <= 0 && function_exists('vmo_mark_credit_warning_state')) {
        vmo_mark_credit_warning_state($user_id, vmo_get_credit_status($user_id));
    }
}

if (!function_exists('vmo_append_user_pending_notification')) {
    /**
     * Append a dashboard notification without overwriting unrelated queued messages.
     *
     * @param int   $user_id
     * @param array $notification
     * @return void
     */
    function vmo_append_user_pending_notification($user_id, $notification) {
        $user_id = absint($user_id);
        if ($user_id <= 0 || !is_array($notification)) {
            return;
        }

        $existing = get_user_meta($user_id, 'vmo_pending_notification', true);
        $items = array();

        if (is_string($existing) && $existing !== '') {
            $decoded = json_decode($existing, true);
            if (is_array($decoded)) {
                $is_list = empty($decoded) || array_keys($decoded) === range(0, count($decoded) - 1);
                $items = $is_list ? $decoded : array($decoded);
            }
        } elseif (is_array($existing)) {
            $is_list = empty($existing) || array_keys($existing) === range(0, count($existing) - 1);
            $items = $is_list ? $existing : array($existing);
        }

        $items[] = $notification;
        if (count($items) > 5) {
            $items = array_slice($items, -5);
        }

        update_user_meta($user_id, 'vmo_pending_notification', wp_json_encode($items));
    }
}

if (!function_exists('vmo_admin_reward_grants_service_access')) {
    /**
     * Admin-applied rewards are trusted overrides. These rewards should clear
     * stale payment locks because the owner/support team intentionally granted access.
     *
     * @param string $reward_kind
     * @return bool
     */
    function vmo_admin_reward_grants_service_access($reward_kind) {
        $reward_kind = sanitize_key((string) $reward_kind);
        return in_array($reward_kind, array(
            'user_discount',
            'tester_access',
            'reviewer_access',
            'credit_bonus',
            'service_bonus',
            'compensation',
            'admin_override',
            'free_access',
        ), true);
    }
}

if (!function_exists('vmo_apply_admin_user_reward_entitlement')) {
    /**
     * Apply the actual entitlement side effects of a master-admin reward.
     *
     * The old flow only inserted a vmo_master_rewards row. That meant credits
     * could appear in the UI while the service lock still blocked Jarvis/AI.
     * This function makes the reward operational: credits, unlock state,
     * license balance, access window, and a centered user-facing modal.
     *
     * @param int   $user_id
     * @param array $reward
     * @return array
     */
    function vmo_apply_admin_user_reward_entitlement($user_id, $reward = array()) {
        $user_id = absint($user_id);
        if ($user_id <= 0) {
            return array('applied' => false, 'reason' => 'invalid_user');
        }

        $reward = is_array($reward) ? $reward : array();
        $reward_kind = sanitize_key((string) ($reward['reward_kind'] ?? 'user_reward'));
        $value_decimal = max(0, (float) ($reward['value_decimal'] ?? 0));
        $ends_at = sanitize_text_field((string) ($reward['ends_at'] ?? ''));
        $reason = sanitize_textarea_field((string) ($reward['reason'] ?? 'Admin reward applied'));
        $discount_code = sanitize_text_field((string) ($reward['discount_code'] ?? ''));
        $access_granted = vmo_admin_reward_grants_service_access($reward_kind);
        $credit_reward_kinds = array('credit_bonus', 'service_bonus', 'compensation');
        $credits_added = in_array($reward_kind, $credit_reward_kinds, true) ? (int) round($value_decimal) : 0;

        if ($credits_added > 0) {
            vmo_update_token_balance($user_id, $credits_added, 'add');
        }

        $current_balance = (int) get_user_meta($user_id, 'vmo_token_balance', true);
        $plan = function_exists('vmo_get_user_recoverable_plan') ? vmo_get_user_recoverable_plan($user_id, (string) ($reward['plan'] ?? 'tester')) : 'tester';
        if ($plan === '') {
            $plan = 'tester';
        }

        if ($access_granted) {
            update_user_meta($user_id, 'vmo_admin_reward_access_active', 1);
            update_user_meta($user_id, 'vmo_admin_reward_kind', $reward_kind);
            update_user_meta($user_id, 'vmo_admin_reward_reason', $reason);
            update_user_meta($user_id, 'vmo_admin_reward_updated_at', current_time('mysql'));

            if ($discount_code !== '') {
                update_user_meta($user_id, 'vmo_active_discount_code', $discount_code);
            }

            if ($ends_at !== '') {
                update_user_meta($user_id, 'vmo_admin_reward_access_expires_at', $ends_at);
                update_user_meta($user_id, 'vmo_free_access_expires_at', $ends_at);
                update_user_meta($user_id, 'vmo_discount_access_expires_at', $ends_at);
                update_user_meta($user_id, 'vmo_service_access_expires_at', $ends_at);
            }

            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);
            update_user_meta($user_id, 'vmo_subscription_payment_status', 'paid');
            update_user_meta($user_id, 'vmo_services_suspended', 0);
            update_user_meta($user_id, 'vmo_service_access_enabled', 1);

            if (function_exists('vmo_unlock_user_services')) {
                $unlock_context = array(
                    'plan' => $plan,
                    'payment_status' => 'paid',
                    'reason' => 'admin_reward',
                    'restore_pre_lock_tokens' => true,
                );
                if ($credits_added > 0) {
                    $unlock_context['token_balance'] = $current_balance;
                }
                vmo_unlock_user_services($user_id, $unlock_context);
                $current_balance = (int) get_user_meta($user_id, 'vmo_token_balance', true);
            }

            if (function_exists('vmo_sync_license_credit_balance')) {
                vmo_sync_license_credit_balance($user_id, $current_balance);
            }
        }

        $title = '🎁 VMO reward applied';
        $message_parts = array();
        if ($credits_added > 0) {
            $message_parts[] = number_format_i18n($credits_added) . ' VMO credits were added.';
        } elseif ($reward_kind === 'user_discount') {
            $message_parts[] = 'Your VMO discount has been applied.';
        } elseif (in_array($reward_kind, array('tester_access', 'reviewer_access'), true)) {
            $message_parts[] = 'Your VMO access window has been activated.';
        } else {
            $message_parts[] = 'Your VMO reward has been applied.';
        }
        $message_parts[] = 'Your current balance is ' . number_format_i18n($current_balance) . ' credits.';
        if ($access_granted) {
            $message_parts[] = 'Your VMO services are now active.';
        }

        vmo_append_user_pending_notification($user_id, array(
            'type' => 'reward_applied',
            'level' => 'success',
            'title' => $title,
            'message' => implode(' ', $message_parts),
            'credits_added' => $credits_added,
            'current_balance' => $current_balance,
            'access_active' => $access_granted,
            'reward_kind' => $reward_kind,
            'expires_at' => $ends_at,
            'requires_ack' => true,
            'action_label' => 'Continue Building',
            'action_url' => admin_url('admin.php?page=vmo-ai-builder-main'),
        ));

        return array(
            'applied' => true,
            'access_granted' => $access_granted,
            'credits_added' => $credits_added,
            'current_balance' => $current_balance,
            'plan' => $plan,
        );
    }
}

if (!function_exists('vmo_sync_license_credit_balance')) {
    function vmo_sync_license_credit_balance($user_id, $balance) {
        global $wpdb;

        $user_id = absint($user_id);
        $balance = max(0, (int) $balance);
        if ($user_id <= 0 || !function_exists('vmo_ensure_license_credit_columns')) {
            return false;
        }

        $licenses_table = $wpdb->prefix . 'vmo_licenses';
        if (!vmo_ensure_license_credit_columns($licenses_table)) {
            return false;
        }

        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal constant
        return $wpdb->query($wpdb->prepare(
            "UPDATE {$licenses_table}
                SET current_credit_balance = %d,
                    updated_at = %s
              WHERE user_id = %d AND status = 'active'",
            $balance,
            current_time('mysql', true),
            $user_id
        ));
    }
}

/**
 * Get current token balance.
 *
 * @param int $user_id
 * @return int
 */
function vmo_get_token_balance($user_id) {
    return (int) get_user_meta($user_id, 'vmo_token_balance', true);
}

/**
 * Get the user's recurring plan credit allocation.
 *
 * @param int $user_id
 * @return int
 */
function vmo_get_user_plan_credit_allocation($user_id) {
    $user_id = (int) $user_id;
    if ($user_id <= 0) {
        return 0;
    }

    if (function_exists('vmo_is_internal_unmetered_user') && vmo_is_internal_unmetered_user($user_id)) {
        return 999999999;
    }

    $stored_allocation = (int) get_user_meta($user_id, 'vmo_plan_recurring_tokens', true);
    $plan = (string) get_user_meta($user_id, 'vmo_subscription_status', true);
    $policy_allocation = 0;

    if (class_exists('VMO_Plan_Policy')) {
        $plan_policy = VMO_Plan_Policy::get_plan($plan);
        $policy_allocation = (int) ($plan_policy['recurring_tokens'] ?? 0);
    } else {
        $normalized_plan = strtolower(trim($plan));
        $aliases = array(
            'tester_pack' => 'tester',
            'starter_pack' => 'starter',
            'pro_pack' => 'pro',
            'professional' => 'pro',
            'premium' => 'pro',
            'business' => 'elite',
            'business_pack' => 'elite',
            'enterprise_pack' => 'enterprise',
        );
        $normalized_plan = $aliases[$normalized_plan] ?? $normalized_plan;

        switch ($normalized_plan) {
            case 'tester':
                $policy_allocation = defined('VMO_TOKENS_TESTER_PACKAGE') ? (int) VMO_TOKENS_TESTER_PACKAGE : 36034;
                break;
            case 'starter':
                $policy_allocation = defined('VMO_TOKENS_STARTER_PLAN') ? (int) VMO_TOKENS_STARTER_PLAN : 72326;
                break;
            case 'pro':
                $policy_allocation = defined('VMO_TOKENS_PRO_PLAN') ? (int) VMO_TOKENS_PRO_PLAN : 144909;
                break;
            case 'elite':
                $policy_allocation = defined('VMO_TOKENS_ELITE_PLAN') ? (int) VMO_TOKENS_ELITE_PLAN : 362659;
                break;
            case 'enterprise':
                $policy_allocation = defined('VMO_TOKENS_ENTERPRISE_PLAN') ? (int) VMO_TOKENS_ENTERPRISE_PLAN : 500000;
                break;
        }
    }

    return max($stored_allocation, $policy_allocation, 0);
}

function vmo_service_lock_blocking_statuses() {
    return array(
        'payment_failed',
        'past_due',
        'unpaid',
        'suspended',
        'canceled',
        'cancelled',
        'paused',
        'incomplete',
        'incomplete_expired',
        'requires_payment_method',
    );
}

function vmo_service_lock_active_statuses() {
    return array('trialing', 'active', 'paid');
}

function vmo_service_lock_plan_keys() {
    return array('tester', 'starter', 'pro', 'elite', 'enterprise', 'premium', 'business');
}

function vmo_normalize_service_plan_key($plan) {
    $plan = strtolower(trim((string) $plan));
    if ($plan === '') {
        return '';
    }

    if (function_exists('vmo_normalize_plan_key')) {
        $normalized = strtolower(trim((string) vmo_normalize_plan_key($plan)));
        if ($normalized !== '') {
            $plan = $normalized;
        }
    } elseif (class_exists('VMO_Plan_Policy')) {
        $plan = strtolower(trim((string) VMO_Plan_Policy::normalize_plan($plan)));
    }

    $aliases = array(
        'tester_pack' => 'tester',
        'starter_pack' => 'starter',
        'pro_pack' => 'pro',
        'professional' => 'pro',
        'business_pack' => 'elite',
        'enterprise_pack' => 'enterprise',
    );

    return $aliases[$plan] ?? $plan;
}

function vmo_user_has_selected_subscription_plan($plan) {
    return in_array(vmo_normalize_service_plan_key($plan), vmo_service_lock_plan_keys(), true);
}

function vmo_parse_service_access_timestamp($value) {
    if (is_numeric($value)) {
        return (int) $value;
    }

    $value = trim((string) $value);
    if ($value === '') {
        return 0;
    }

    $timestamp = strtotime($value);
    return $timestamp ? (int) $timestamp : 0;
}

function vmo_get_user_free_access_expiration($user_id) {
    $user_id = (int) $user_id;
    if ($user_id <= 0) {
        return 0;
    }

    $keys = array(
        'vmo_free_access_expires_at',
        'vmo_trial_access_expires_at',
        'vmo_discount_access_expires_at',
        'vmo_service_access_expires_at',
        'vmo_quicklaunch_free_access_expires_at',
        'vmo_promo_access_expires_at',
        'vmo_admin_reward_access_expires_at',
    );

    $expiration = 0;
    foreach ($keys as $key) {
        $expiration = max($expiration, vmo_parse_service_access_timestamp(get_user_meta($user_id, $key, true)));
    }

    return $expiration;
}

function vmo_get_user_cost_access_state($user_id, $required_tokens = 0) {
    $user_id = (int) $user_id;
    $required_tokens = max(0, (int) $required_tokens);
    $now = current_time('timestamp');
    $blocking = vmo_service_lock_blocking_statuses();
    $balance = $user_id > 0 ? vmo_get_token_balance($user_id) : 0;
    $allocation = $user_id > 0 ? vmo_get_user_plan_credit_allocation($user_id) : 0;
    $effective_allocation = $allocation > 0 ? $allocation : max($balance, $required_tokens, 0);
    $remaining_percent = $effective_allocation > 0 ? round(min(100, max(0, ($balance / $effective_allocation) * 100)), 1) : 0;
    $low_threshold = defined('VMO_LOW_CREDIT_REMAINING_PERCENT') ? (float) VMO_LOW_CREDIT_REMAINING_PERCENT : 40.0;
    $critical_threshold = defined('VMO_CRITICAL_CREDIT_REMAINING_PERCENT') ? (float) VMO_CRITICAL_CREDIT_REMAINING_PERCENT : 20.0;

    $subscription_status = $user_id > 0 ? sanitize_key((string) get_user_meta($user_id, 'vmo_subscription_status', true)) : '';
    $payment_status = $user_id > 0 ? sanitize_key((string) get_user_meta($user_id, 'vmo_subscription_payment_status', true)) : '';
    $explicit_lock = $user_id > 0 ? strtolower((string) get_user_meta($user_id, 'vmo_service_lock_status', true)) : '';
    $recoverable_plan = $user_id > 0 ? vmo_get_user_recoverable_plan($user_id) : '';
    $status_plan = vmo_normalize_service_plan_key($subscription_status);
    $has_selected_plan = $recoverable_plan !== '' || in_array($status_plan, vmo_service_lock_plan_keys(), true);
    $free_access_expires_at = $user_id > 0 ? vmo_get_user_free_access_expiration($user_id) : 0;
    $free_access_active = $free_access_expires_at > 0 && $free_access_expires_at >= $now;
    $free_access_expired = $free_access_expires_at > 0 && $free_access_expires_at < $now;
    $internal_unmetered = $user_id > 0 && function_exists('vmo_is_internal_unmetered_user') && vmo_is_internal_unmetered_user($user_id);
    $admin_reward_flag = $user_id > 0 && (string) get_user_meta($user_id, 'vmo_admin_reward_access_active', true) === '1';
    $admin_reward_expires_at = $user_id > 0 ? vmo_parse_service_access_timestamp(get_user_meta($user_id, 'vmo_admin_reward_access_expires_at', true)) : 0;
    $admin_reward_active = $admin_reward_flag && ($admin_reward_expires_at <= 0 || $admin_reward_expires_at >= $now);

    if ($admin_reward_active) {
        if ($recoverable_plan === '') {
            $recoverable_plan = 'tester';
        }
        $has_selected_plan = true;
        $free_access_active = true;
        if (in_array($payment_status, $blocking, true)) {
            $payment_status = 'paid';
        }
        if (in_array($subscription_status, $blocking, true) || $subscription_status === '') {
            $subscription_status = $recoverable_plan;
        }
        if ($explicit_lock === 'locked') {
            $explicit_lock = 'unlocked';
        }
    }

    $billing_blocked = $explicit_lock === 'locked' || in_array($subscription_status, $blocking, true) || in_array($payment_status, $blocking, true);
    $trialing = in_array($subscription_status, array('trialing'), true) || in_array($payment_status, array('trialing'), true);
    $paid_active = in_array($subscription_status, array('active', 'paid'), true) || in_array($payment_status, array('active', 'paid'), true);
    $plan_only_active = $has_selected_plan && $payment_status === '' && !in_array($subscription_status, $blocking, true);
    $trial_active = $has_selected_plan && $trialing && $free_access_active;
    $entitlement_active = $internal_unmetered || (!$billing_blocked && $has_selected_plan && ($paid_active || $trial_active || $free_access_active || $plan_only_active));

    $reason = '';
    if ($internal_unmetered) {
        $reason = '';
    } elseif ($billing_blocked) {
        $reason = $explicit_lock === 'locked' ? sanitize_key((string) get_user_meta($user_id, 'vmo_service_lock_reason', true)) : '';
        if ($reason === '') {
            $reason = in_array($payment_status, $blocking, true) ? $payment_status : $subscription_status;
        }
    } elseif (!$has_selected_plan) {
        $reason = 'subscription_required';
    } elseif ($free_access_expired && !$paid_active) {
        $reason = 'free_access_expired';
        $entitlement_active = false;
    } elseif (!$entitlement_active) {
        $reason = 'subscription_required';
    } elseif ($balance <= 0) {
        $reason = 'credits_exhausted';
    } elseif ($required_tokens > 0 && $balance < $required_tokens) {
        $reason = 'insufficient_tokens';
    }

    $critical_credit = $balance > 0 && $effective_allocation > 0 && $remaining_percent <= $critical_threshold;
    $low_credit = $balance > 0 && $effective_allocation > 0 && $remaining_percent <= $low_threshold;
    $no_credit = !$internal_unmetered && $balance <= 0;
    $insufficient = !$internal_unmetered && $required_tokens > 0 && $balance < $required_tokens;
    $warning_level = 'none';
    if ($reason === 'credits_exhausted' || ($entitlement_active && $no_credit)) {
        $warning_level = 'exhausted';
    } elseif ($critical_credit) {
        $warning_level = 'critical';
    } elseif ($low_credit) {
        $warning_level = 'low';
    }

    return array(
        'user_id' => $user_id,
        'internal_unmetered' => $internal_unmetered,
        'balance' => $balance,
        'plan_tokens' => $allocation,
        'effective_plan_tokens' => $effective_allocation,
        'required_tokens' => $required_tokens,
        'remaining_percent' => $remaining_percent,
        'low_credit_threshold_percent' => $low_threshold,
        'critical_credit_threshold_percent' => $critical_threshold,
        'low_credit' => $low_credit,
        'critical_credit' => $critical_credit,
        'warning_level' => $warning_level,
        'no_credit' => $no_credit,
        'insufficient' => $insufficient,
        'has_selected_plan' => $has_selected_plan,
        'entitlement_active' => $entitlement_active,
        'billing_blocked' => $billing_blocked,
        'free_access_active' => $free_access_active,
        'free_access_expired' => $free_access_expired,
        'free_access_expires_at' => $free_access_expires_at,
        'admin_reward_active' => $admin_reward_active,
        'subscription_status' => $subscription_status,
        'payment_status' => $payment_status,
        'recoverable_plan' => $recoverable_plan,
        'block_reason' => $reason,
        'can_use_cost_bearing_services' => $internal_unmetered || ($entitlement_active && $reason === ''),
    );
}

function vmo_can_user_use_cost_bearing_services($user_id, $required_tokens = 0) {
    $state = vmo_get_user_cost_access_state($user_id, $required_tokens);
    return !empty($state['can_use_cost_bearing_services']);
}

function vmo_get_user_recoverable_plan($user_id, $fallback = '') {
    $user_id = (int) $user_id;
    $candidates = array(
        $fallback,
        $user_id > 0 ? get_user_meta($user_id, 'vmo_service_lock_plan', true) : '',
        $user_id > 0 ? get_user_meta($user_id, 'vmo_last_active_subscription_plan', true) : '',
        $user_id > 0 ? get_user_meta($user_id, 'vmo_subscription_status', true) : '',
    );

    foreach ($candidates as $candidate) {
        $plan = vmo_normalize_service_plan_key($candidate);
        if (in_array($plan, vmo_service_lock_plan_keys(), true)) {
            return $plan;
        }
    }

    return '';
}

function vmo_get_service_lock_message($reason = '') {
    $reason = sanitize_key((string) $reason);
    switch ($reason) {
        case 'payment_failed':
        case 'past_due':
        case 'unpaid':
        case 'requires_payment_method':
            return 'Your VMO services are paused because the subscription payment could not be collected. Your website and domain records remain in place, but AI, phone, SMS, and email services stay off until payment is updated.';
        case 'subscription_canceled':
        case 'canceled':
        case 'cancelled':
            return 'Your VMO services are paused because the subscription is canceled. Your website and domain records remain in place, but paid services stay off until the subscription is restored.';
        case 'paused':
        case 'incomplete':
        case 'incomplete_expired':
            return 'Your VMO services are paused until the subscription is active and paid.';
        case 'subscription_required':
            return 'Choose a VMO subscription plan to power the plugin. Until a paid plan, active trial, or approved free-access window is active, AI, phone, SMS, email provisioning, and other provider-backed services stay off.';
        case 'free_access_expired':
            return 'Your free VMO access period has ended. Your generated website remains in place, but AI, phone, SMS, email, and recurring provider-backed services stay paused until billing is activated.';
        case 'credits_exhausted':
            return 'Your VMO Credit balance is empty. Your generated website remains in place, and metered AI, communication, media, and provider-backed actions need a one-time booster/top-up or a higher-credit plan before they can continue.';
        case 'insufficient_tokens':
            return 'This action needs more VMO Credit than is currently available. Add a one-time booster or upgrade before continuing.';
        default:
            return 'Your VMO services are paused until billing is resolved.';
    }
}

function vmo_is_user_service_locked($user_id) {
    $user_id = (int) $user_id;
    if ($user_id <= 0) {
        return false;
    }

    $state = vmo_get_user_cost_access_state($user_id);
    return empty($state['can_use_cost_bearing_services']);
}

function vmo_get_service_lock_payload($user_id, $operation = '') {
    $user_id = (int) $user_id;
    $state = $user_id > 0 ? vmo_get_user_cost_access_state($user_id) : array('can_use_cost_bearing_services' => true);
    $reason = $user_id > 0 ? sanitize_key((string) get_user_meta($user_id, 'vmo_service_lock_reason', true)) : '';
    $subscription_status = $user_id > 0 ? sanitize_key((string) get_user_meta($user_id, 'vmo_subscription_status', true)) : '';
    $payment_status = $user_id > 0 ? sanitize_key((string) get_user_meta($user_id, 'vmo_subscription_payment_status', true)) : '';

    if ($reason === '') {
        $blocking = vmo_service_lock_blocking_statuses();
        if (in_array($payment_status, $blocking, true)) {
            $reason = $payment_status;
        } elseif (in_array($subscription_status, $blocking, true)) {
            $reason = $subscription_status;
        } elseif (!empty($state['block_reason'])) {
            $reason = sanitize_key((string) $state['block_reason']);
        } else {
            $reason = 'billing_hold';
        }
    }

    if ($user_id > 0 && empty($state['can_use_cost_bearing_services']) && in_array($reason, array('payment_failed', 'past_due', 'unpaid', 'paused', 'incomplete', 'incomplete_expired', 'canceled', 'subscription_canceled', 'free_access_expired', 'credits_exhausted'), true)) {
        vmo_schedule_recurring_service_grace($user_id, $reason, array('operation' => $operation));
    }

    return array(
        'code' => 'service_locked',
        'status' => 'service_locked',
        'operation' => (string) $operation,
        'message' => vmo_get_service_lock_message($reason),
        'service_locked' => empty($state['can_use_cost_bearing_services']),
        'service_lock_reason' => $reason,
        'service_locked_at' => $user_id > 0 ? (string) get_user_meta($user_id, 'vmo_service_locked_at', true) : '',
        'subscription_status' => $subscription_status,
        'payment_status' => $payment_status,
        'recoverable_plan' => (string) ($state['recoverable_plan'] ?? ''),
        'has_selected_plan' => !empty($state['has_selected_plan']),
        'entitlement_active' => !empty($state['entitlement_active']),
        'free_access_active' => !empty($state['free_access_active']),
        'free_access_expired' => !empty($state['free_access_expired']),
        'free_access_expires_at' => (int) ($state['free_access_expires_at'] ?? 0),
        'billing_recovery_required' => true,
        'payment_required' => true,
        'payment_portal_url' => admin_url('admin.php?page=vmo-my-plan'),
        'action_label' => 'Update Payment Method',
        'action_url' => admin_url('admin.php?page=vmo-my-plan'),
    );
}

function vmo_lock_user_services($user_id, $reason = 'payment_failed', $context = array()) {
    $user_id = (int) $user_id;
    if ($user_id <= 0) {
        return false;
    }

    $context = is_array($context) ? $context : array();
    $reason = sanitize_key((string) $reason);
    if ($reason === '') {
        $reason = 'payment_failed';
    }

    $current_plan = vmo_get_user_recoverable_plan($user_id, $context['plan'] ?? '');
    if ($current_plan !== '') {
        update_user_meta($user_id, 'vmo_last_active_subscription_plan', $current_plan);
        update_user_meta($user_id, 'vmo_service_lock_plan', $current_plan);
    }

    if ((string) get_user_meta($user_id, 'vmo_service_lock_status', true) !== 'locked') {
        update_user_meta($user_id, 'vmo_service_pre_lock_token_balance', (int) get_user_meta($user_id, 'vmo_token_balance', true));
        update_user_meta($user_id, 'vmo_service_locked_at', current_time('mysql'));
    }

    update_user_meta($user_id, 'vmo_service_lock_status', 'locked');
    update_user_meta($user_id, 'vmo_service_lock_reason', $reason);
    update_user_meta($user_id, 'vmo_service_lock_updated_at', current_time('mysql'));
    update_user_meta($user_id, 'vmo_service_lock_context', $context);
    update_user_meta($user_id, 'vmo_subscription_payment_status', $reason);
    update_user_meta($user_id, 'vmo_services_suspended', 1);
    update_user_meta($user_id, 'vmo_service_access_enabled', 0);
    update_user_meta($user_id, 'vmo_service_assets_preserved', 1);
    update_user_meta($user_id, 'vmo_token_balance', 0);
    update_user_meta($user_id, 'vmo_quicklaunch_available_tokens', 0);

    vmo_schedule_recurring_service_grace($user_id, $reason, $context);

    global $wpdb;
    if (isset($wpdb) && $wpdb instanceof wpdb) {
        $email_accounts_table = $wpdb->prefix . 'vmo_email_accounts';
        if ($wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $email_accounts_table)) === $email_accounts_table) {
            $wpdb->query($wpdb->prepare(
                "UPDATE {$email_accounts_table} SET status = 'billing_suspended' WHERE user_id = %d AND status = 'active'",
                $user_id
            ));
        }
    }

    if (function_exists('vmo_log_billing_event')) {
        vmo_log_billing_event($user_id, 'service_lock', 0, 'Services locked: ' . $reason, 'internal', 0.0, '', true);
    }

    if (function_exists('vmo_append_user_pending_notification')) {
        vmo_append_user_pending_notification($user_id, array(
            'type' => 'service_locked',
            'level' => 'error',
            'title' => 'Your VMO services are paused',
            'message' => vmo_get_service_lock_message($reason),
            'service_locked' => true,
            'payment_required' => true,
            'service_lock_reason' => $reason,
            'action_label' => 'Update Payment Method',
            'action_url' => admin_url('admin.php?page=vmo-my-plan'),
            'payment_portal_url' => admin_url('admin.php?page=vmo-my-plan'),
            'requires_ack' => true,
        ));
    }

    do_action('vmo_user_services_locked', $user_id, $reason, $context);
    return true;
}

function vmo_unlock_user_services($user_id, $context = array()) {
    $user_id = (int) $user_id;
    if ($user_id <= 0) {
        return false;
    }

    $context = is_array($context) ? $context : array();
    $was_explicitly_locked = strtolower((string) get_user_meta($user_id, 'vmo_service_lock_status', true)) === 'locked';
    $plan = vmo_get_user_recoverable_plan($user_id, $context['plan'] ?? '');
    if ($plan !== '') {
        update_user_meta($user_id, 'vmo_subscription_status', $plan);
        update_user_meta($user_id, 'vmo_last_active_subscription_plan', $plan);

        global $wpdb;
        if (isset($wpdb) && $wpdb instanceof wpdb) {
            $licenses_table = $wpdb->prefix . 'vmo_licenses';
            if ($wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $licenses_table)) === $licenses_table) {
                $wpdb->query($wpdb->prepare(
                    "UPDATE {$licenses_table} SET status = 'active', plan = %s WHERE user_id = %d AND status IN ('suspended','expired')",
                    $plan,
                    $user_id
                ));
            }
        }
    }

    global $wpdb;
    if (isset($wpdb) && $wpdb instanceof wpdb) {
        $email_accounts_table = $wpdb->prefix . 'vmo_email_accounts';
        if ($wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $email_accounts_table)) === $email_accounts_table) {
            $wpdb->query($wpdb->prepare(
                "UPDATE {$email_accounts_table} SET status = 'active' WHERE user_id = %d AND status = 'billing_suspended'",
                $user_id
            ));
        }
    }

    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_service_unlocked_at', current_time('mysql'));
    update_user_meta($user_id, 'vmo_subscription_payment_status', sanitize_key((string) ($context['payment_status'] ?? 'paid')));
    update_user_meta($user_id, 'vmo_services_suspended', 0);
    update_user_meta($user_id, 'vmo_service_access_enabled', 1);

    $unlock_reason = sanitize_key((string) ($context['reason'] ?? ''));
    $paid_credit_renewal = in_array($unlock_reason, array('invoice_payment_succeeded', 'stripe_invoice_payment_succeeded'), true);

    if (array_key_exists('token_balance', $context)) {
        update_user_meta($user_id, 'vmo_token_balance', max(0, (int) $context['token_balance']));
    } elseif (!empty($context['restore_pre_lock_tokens']) && ($was_explicitly_locked || $paid_credit_renewal)) {
        $current_balance = (int) get_user_meta($user_id, 'vmo_token_balance', true);
        $pre_lock_balance = (int) get_user_meta($user_id, 'vmo_service_pre_lock_token_balance', true);
        $plan_allocation = function_exists('vmo_get_user_plan_credit_allocation') ? (int) vmo_get_user_plan_credit_allocation($user_id) : 0;
        $restore_balance = max($current_balance, $pre_lock_balance, $plan_allocation, 0);
        if ($restore_balance > $current_balance) {
            update_user_meta($user_id, 'vmo_token_balance', $restore_balance);
        }
    }

    if (function_exists('vmo_can_user_use_cost_bearing_services') && vmo_can_user_use_cost_bearing_services($user_id)) {
        vmo_clear_recurring_service_grace($user_id);
    }

    if (function_exists('vmo_log_billing_event')) {
        vmo_log_billing_event($user_id, 'service_unlock', 0, 'Services unlocked after payment recovery', 'internal', 0.0, '', true);
    }

    do_action('vmo_user_services_unlocked', $user_id, $context);
    return true;
}

function vmo_get_recurring_service_grace_due_timestamp($user_id, $reason = '', $context = array()) {
    $days = defined('VMO_RECURRING_SERVICE_GRACE_DAYS') ? (int) VMO_RECURRING_SERVICE_GRACE_DAYS : 2;
    $days = (int) apply_filters('vmo_recurring_service_grace_days', $days, (int) $user_id, sanitize_key((string) $reason), is_array($context) ? $context : array());
    $days = max(0, min(7, $days));
    return current_time('timestamp') + ($days * DAY_IN_SECONDS);
}

function vmo_schedule_recurring_service_grace($user_id, $reason = 'billing_hold', $context = array()) {
    $user_id = (int) $user_id;
    if ($user_id <= 0) {
        return false;
    }

    $reason = sanitize_key((string) $reason);
    if ($reason === '') {
        $reason = 'billing_hold';
    }
    $context = is_array($context) ? $context : array();
    $existing_due = vmo_parse_service_access_timestamp(get_user_meta($user_id, 'vmo_recurring_services_termination_due_at', true));
    $due_at = $existing_due > current_time('timestamp') ? $existing_due : vmo_get_recurring_service_grace_due_timestamp($user_id, $reason, $context);

    update_user_meta($user_id, 'vmo_recurring_services_grace_status', 'grace_period');
    update_user_meta($user_id, 'vmo_recurring_services_grace_reason', $reason);
    update_user_meta($user_id, 'vmo_recurring_services_grace_started_at', current_time('mysql'));
    update_user_meta($user_id, 'vmo_recurring_services_termination_due_at', gmdate('Y-m-d H:i:s', $due_at));
    update_user_meta($user_id, 'vmo_recurring_services_grace_context', $context);

    if (!wp_next_scheduled('vmo_enforce_user_recurring_service_grace', array($user_id))) {
        wp_schedule_single_event($due_at, 'vmo_enforce_user_recurring_service_grace', array($user_id));
    }

    do_action('vmo_user_recurring_services_grace_started', $user_id, $reason, $due_at, $context);
    return true;
}

function vmo_clear_recurring_service_grace($user_id) {
    $user_id = (int) $user_id;
    if ($user_id <= 0) {
        return false;
    }

    delete_user_meta($user_id, 'vmo_recurring_services_grace_status');
    delete_user_meta($user_id, 'vmo_recurring_services_grace_reason');
    delete_user_meta($user_id, 'vmo_recurring_services_grace_context');
    delete_user_meta($user_id, 'vmo_recurring_services_grace_started_at');
    delete_user_meta($user_id, 'vmo_recurring_services_termination_due_at');
    delete_user_meta($user_id, 'vmo_recurring_services_termination_started_at');
    wp_clear_scheduled_hook('vmo_enforce_user_recurring_service_grace', array($user_id));
    do_action('vmo_user_recurring_services_grace_cleared', $user_id);
    return true;
}

function vmo_enforce_user_recurring_service_grace($user_id) {
    $user_id = (int) $user_id;
    if ($user_id <= 0) {
        return false;
    }

    if (vmo_can_user_use_cost_bearing_services($user_id)) {
        return vmo_clear_recurring_service_grace($user_id);
    }

    $due_at = vmo_parse_service_access_timestamp(get_user_meta($user_id, 'vmo_recurring_services_termination_due_at', true));
    if ($due_at > current_time('timestamp')) {
        if (!wp_next_scheduled('vmo_enforce_user_recurring_service_grace', array($user_id))) {
            wp_schedule_single_event($due_at, 'vmo_enforce_user_recurring_service_grace', array($user_id));
        }
        return false;
    }

    $reason = sanitize_key((string) get_user_meta($user_id, 'vmo_recurring_services_grace_reason', true));
    if ($reason === '') {
        $reason = 'billing_hold';
    }
    $context = get_user_meta($user_id, 'vmo_recurring_services_grace_context', true);
    $context = is_array($context) ? $context : array();

    update_user_meta($user_id, 'vmo_recurring_services_grace_status', 'termination_due');
    update_user_meta($user_id, 'vmo_recurring_services_termination_started_at', current_time('mysql'));

    global $wpdb;
    if (isset($wpdb) && $wpdb instanceof wpdb) {
        $email_accounts_table = $wpdb->prefix . 'vmo_email_accounts';
        if ($wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $email_accounts_table)) === $email_accounts_table) {
            $wpdb->query($wpdb->prepare(
                "UPDATE {$email_accounts_table} SET status = 'provider_cancel' WHERE user_id = %d AND status IN ('active','billing_suspended')",
                $user_id
            ));
        }
    }

    if (function_exists('vmo_log_billing_event')) {
        vmo_log_billing_event($user_id, 'recurring_provider_termination_due', 0, 'Recurring provider services reached grace deadline: ' . $reason, 'internal', 0.0, '', true, 'cost_recovery');
    }

    do_action('vmo_user_recurring_services_grace_expired', $user_id, $reason, $context);
    do_action('vmo_cancel_user_recurring_provider_services', $user_id, $reason, $context);
    return true;
}
add_action('vmo_enforce_user_recurring_service_grace', 'vmo_enforce_user_recurring_service_grace');

function vmo_enforce_recurring_service_grace_sweep() {
    global $wpdb;
    $rows = $wpdb->get_results($wpdb->prepare(
        "SELECT user_id FROM {$wpdb->usermeta} WHERE meta_key = %s AND meta_value <> '' LIMIT 100",
        'vmo_recurring_services_termination_due_at'
    ));
    foreach (($rows ?: array()) as $row) {
        vmo_enforce_user_recurring_service_grace((int) $row->user_id);
    }
}
add_action('vmo_recurring_service_grace_sweep', 'vmo_enforce_recurring_service_grace_sweep');
add_action('init', function() {
    if (!wp_next_scheduled('vmo_recurring_service_grace_sweep')) {
        wp_schedule_event(time() + HOUR_IN_SECONDS, 'hourly', 'vmo_recurring_service_grace_sweep');
    }
});

function vmo_service_lock_provider_host_requires_active_billing($host) {
    $host = strtolower((string) $host);
    if ($host === '') {
        return false;
    }

    if ($host === 'api.openai.com') {
        return true;
    }

    return $host === 'api.twilio.com' || substr($host, -11) === '.twilio.com';
}

function vmo_block_locked_user_provider_http_requests($preempt, $parsed_args, $url) {
    if (!function_exists('is_user_logged_in') || !is_user_logged_in()) {
        return $preempt;
    }

    $user_id = get_current_user_id();
    if ($user_id <= 0 || !vmo_is_user_service_locked($user_id)) {
        return $preempt;
    }

    $host = wp_parse_url((string) $url, PHP_URL_HOST);
    if (!vmo_service_lock_provider_host_requires_active_billing($host)) {
        return $preempt;
    }

    $payload = vmo_get_service_lock_payload($user_id, 'provider_http_request');
    return new WP_Error('vmo_service_locked', $payload['message'], array(
        'status' => 402,
        'service_locked' => true,
        'provider_host' => $host,
    ));
}
add_filter('pre_http_request', 'vmo_block_locked_user_provider_http_requests', 10, 3);

/**
 * Normalize the user's current credit state for PHP gates and frontend toasts.
 *
 * @param int $user_id
 * @param int $required_tokens Optional cost of the attempted operation.
 * @return array
 */
function vmo_get_credit_status($user_id, $required_tokens = 0) {
    $user_id = (int) $user_id;
    $required_tokens = max(0, (int) $required_tokens);
    $state = vmo_get_user_cost_access_state($user_id, $required_tokens);
    $balance = (int) $state['balance'];
    $allocation = (int) $state['plan_tokens'];
    $effective_allocation = (int) $state['effective_plan_tokens'];
    $remaining_percent = (float) $state['remaining_percent'];
    $usage_percent = $effective_allocation > 0 ? round(min(100, max(0, 100 - $remaining_percent)), 1) : 100;
    $threshold = (float) $state['low_credit_threshold_percent'];
    $critical_threshold = (float) $state['critical_credit_threshold_percent'];
    $no_credit = !empty($state['no_credit']);
    $insufficient = !empty($state['insufficient']);
    $low_credit = !empty($state['low_credit']);
    $critical_credit = !empty($state['critical_credit']);
    $plan = $user_id > 0 ? (string) get_user_meta($user_id, 'vmo_subscription_status', true) : 'free';
    $normalized_plan = class_exists('VMO_Plan_Policy') ? VMO_Plan_Policy::normalize_plan($plan) : strtolower(trim($plan ?: 'free'));
    $next_plan = array(
        'free' => 'Tester',
        'tester' => 'Starter',
        'starter' => 'Pro',
        'pro' => 'Elite',
        'elite' => 'Enterprise',
        'enterprise' => 'Enterprise',
    );

    $lock_payload = $user_id > 0 ? vmo_get_service_lock_payload($user_id) : array('service_locked' => false);

    return array(
        'balance' => $balance,
        'plan_tokens' => $allocation,
        'effective_plan_tokens' => $effective_allocation,
        'required_tokens' => $required_tokens,
        'remaining_percent' => $remaining_percent,
        'usage_percent' => $usage_percent,
        'low_credit_threshold_percent' => $threshold,
        'critical_credit_threshold_percent' => $critical_threshold,
        'low_credit' => $low_credit,
        'critical_credit' => $critical_credit,
        'credit_warning_level' => (string) $state['warning_level'],
        'no_credit' => $no_credit,
        'insufficient' => $insufficient,
        'plan' => $normalized_plan ?: 'free',
        'next_plan' => $next_plan[$normalized_plan] ?? 'Starter',
        'has_selected_plan' => !empty($state['has_selected_plan']),
        'entitlement_active' => !empty($state['entitlement_active']),
        'free_access_active' => !empty($state['free_access_active']),
        'free_access_expired' => !empty($state['free_access_expired']),
        'free_access_expires_at' => (int) ($state['free_access_expires_at'] ?? 0),
        'cost_access_block_reason' => (string) ($state['block_reason'] ?? ''),
        'can_use_cost_bearing_services' => !empty($state['can_use_cost_bearing_services']),
        'service_locked' => !empty($lock_payload['service_locked']),
        'service_lock_reason' => (string) ($lock_payload['service_lock_reason'] ?? ''),
        'service_locked_at' => (string) ($lock_payload['service_locked_at'] ?? ''),
        'subscription_payment_status' => (string) ($lock_payload['payment_status'] ?? ''),
        'recoverable_plan' => (string) ($lock_payload['recoverable_plan'] ?? ''),
    );
}

/**
 * Persist credit warning flags for dashboard/frontend display.
 *
 * @param int $user_id
 * @param array|null $status
 */
function vmo_mark_credit_warning_state($user_id, $status = null) {
    $user_id = (int) $user_id;
    if ($user_id <= 0) {
        return;
    }

    $status = is_array($status) ? $status : vmo_get_credit_status($user_id);
    if (!empty($status['no_credit']) || !empty($status['insufficient']) || !empty($status['low_credit']) || !empty($status['critical_credit'])) {
        update_user_meta($user_id, 'vmo_needs_upgrade_notification', true);
        update_user_meta($user_id, 'vmo_last_credit_status', $status);
        update_user_meta($user_id, 'vmo_last_credit_warning_at', time());
        update_user_meta($user_id, 'vmo_last_credit_warning_level', sanitize_key((string) ($status['credit_warning_level'] ?? 'low')));
        if (!empty($status['critical_credit'])) {
            update_user_meta($user_id, 'vmo_last_critical_credit_warning_at', time());
        }
        if (!empty($status['no_credit']) && !empty($status['entitlement_active'])) {
            vmo_schedule_recurring_service_grace($user_id, 'credits_exhausted', array('credit_status' => $status));
        }
        vmo_maybe_send_credit_warning_email($user_id, $status);
    } else {
        delete_user_meta($user_id, 'vmo_needs_upgrade_notification');
        delete_user_meta($user_id, 'vmo_last_credit_status');
        delete_user_meta($user_id, 'vmo_last_credit_warning_level');
    }
}

function vmo_maybe_send_credit_warning_email($user_id, $status) {
    $user_id = (int) $user_id;
    if ($user_id <= 0 || !is_array($status)) {
        return false;
    }

    if (function_exists('vmo_is_internal_unmetered_user') && vmo_is_internal_unmetered_user($user_id)) {
        return false;
    }

    $level = sanitize_key((string) ($status['credit_warning_level'] ?? 'none'));
    if ($level === 'none') {
        return false;
    }

    $interval = $level === 'critical' ? 6 * HOUR_IN_SECONDS : DAY_IN_SECONDS;
    if ($level === 'exhausted') {
        $interval = 3 * HOUR_IN_SECONDS;
    }

    $last_key = 'vmo_credit_warning_email_sent_' . $level;
    $last_sent = (int) get_user_meta($user_id, $last_key, true);
    if ($last_sent > 0 && (time() - $last_sent) < $interval) {
        return false;
    }

    $user = get_userdata($user_id);
    if (!$user || empty($user->user_email)) {
        return false;
    }

    $percent = isset($status['remaining_percent']) ? rtrim(rtrim((string) $status['remaining_percent'], '0'), '.') : '0';
    $balance = isset($status['balance']) ? number_format_i18n((int) $status['balance']) : '0';
    $next_plan = (string) ($status['next_plan'] ?? 'the next plan');

    if ($level === 'exhausted') {
        $subject = 'VMO Credit is empty - services are paused';
        $message = "Your VMO Credit balance is empty. Your generated website remains available, and metered AI, phone, SMS, email sending/provisioning, media generation, and other provider-backed actions need more available credit before they can continue. Recurring provider services may be suspended or cancelled after the grace period to avoid charges on your behalf.";
    } elseif ($level === 'critical') {
        $subject = 'VMO Credit critical warning';
        $message = "Your VMO Credit balance is critically low at {$percent}% ({$balance} credits). Temporary loss can include paused AI, phone, SMS, email sending/provisioning, media generation, and automation. If the balance reaches zero and remains unresolved through the grace period, recurring provider services may be suspended or cancelled and some provider-held resources could require reprovisioning.";
    } else {
        $subject = 'VMO Credit low warning';
        $message = "Your VMO Credit balance is below the warning threshold at {$percent}% ({$balance} credits). Add a one-time booster or upgrade to {$next_plan} before larger tasks pause. Your generated website remains in place, but provider-backed actions need available VMO Credit.";
    }

    $message .= "\n\nYou can recover access by adding credit, upgrading your plan, or resolving billing from your VMO dashboard.";

    $sent = wp_mail($user->user_email, $subject, $message);
    if ($sent) {
        update_user_meta($user_id, $last_key, time());
    }

    return (bool) $sent;
}

/**
 * Format the standard AJAX payload used when credit blocks an operation.
 *
 * @param int $user_id
 * @param int $required_tokens
 * @param string $operation
 * @return array
 */
function vmo_get_credit_gate_payload($user_id, $required_tokens = 0, $operation = '') {
    $status = vmo_get_credit_status($user_id, $required_tokens);
    vmo_mark_credit_warning_state($user_id, $status);

    if (!empty($status['service_locked'])) {
        $payload = vmo_get_service_lock_payload($user_id, $operation);
        $payload['credit_status'] = $status;
        $payload['tokens_required'] = (int) $status['required_tokens'];
        $payload['tokens_available'] = (int) $status['balance'];
        return $payload;
    }

    if (!empty($status['no_credit'])) {
        $message = 'Your VMO Credit balance is empty. Purchase a one-time booster or upgrade to a higher-credit plan to continue metered AI, communication, media, and provider-backed actions.';
    } elseif (!empty($status['insufficient'])) {
        $message = sprintf(
            'This task needs %s VMO Credits, but you only have %s. Purchase a one-time booster or upgrade to %s to continue.',
            number_format_i18n((int) $status['required_tokens']),
            number_format_i18n((int) $status['balance']),
            (string) $status['next_plan']
        );
    } else {
        $message = sprintf(
            'Your VMO Credit balance is down to %s%%. Upgrade to %s or purchase a one-time booster before larger tasks pause.',
            rtrim(rtrim((string) $status['remaining_percent'], '0'), '.'),
            (string) $status['next_plan']
        );
    }

    return array(
        'code' => 'insufficient_tokens',
        'status' => 'insufficient_tokens',
        'operation' => $operation,
        'message' => $message,
        'credit_status' => $status,
        'tokens_required' => (int) $status['required_tokens'],
        'tokens_available' => (int) $status['balance'],
    );
}

/**
 * Frontend-friendly credit config for localized scripts.
 *
 * @param int $user_id
 * @return array
 */
function vmo_get_frontend_credit_config($user_id = 0) {
    $user_id = $user_id ? (int) $user_id : get_current_user_id();
    $status = vmo_get_credit_status($user_id);

    return array(
        'token_balance' => (int) $status['balance'],
        'plan_tokens' => (int) $status['effective_plan_tokens'],
        'credit_remaining_percent' => (float) $status['remaining_percent'],
        'credit_usage_percent' => (float) $status['usage_percent'],
        'low_credit_remaining_percent' => (float) $status['low_credit_threshold_percent'],
        'critical_credit_remaining_percent' => (float) $status['critical_credit_threshold_percent'],
        'low_credit' => (bool) $status['low_credit'],
        'critical_credit' => (bool) $status['critical_credit'],
        'credit_warning_level' => (string) ($status['credit_warning_level'] ?? 'none'),
        'no_credit' => (bool) $status['no_credit'],
        'next_plan' => (string) $status['next_plan'],
        'has_selected_plan' => !empty($status['has_selected_plan']),
        'entitlement_active' => !empty($status['entitlement_active']),
        'free_access_active' => !empty($status['free_access_active']),
        'free_access_expired' => !empty($status['free_access_expired']),
        'free_access_expires_at' => (int) ($status['free_access_expires_at'] ?? 0),
        'cost_access_block_reason' => (string) ($status['cost_access_block_reason'] ?? ''),
        'can_use_cost_bearing_services' => !empty($status['can_use_cost_bearing_services']),
        'service_locked' => !empty($status['service_locked']),
        'service_lock_reason' => (string) ($status['service_lock_reason'] ?? ''),
        'subscription_payment_status' => (string) ($status['subscription_payment_status'] ?? ''),
    );
}

/**
 * Check if user has sufficient tokens for an operation.
 *
 * @param int $user_id
 * @param string $operation 'content', 'image', or 'refinement'
 * @return bool
 */
function vmo_check_token_balance($user_id, $operation) {
    if (function_exists('vmo_is_user_service_locked') && vmo_is_user_service_locked($user_id)) {
        vmo_mark_credit_warning_state($user_id, vmo_get_credit_status($user_id, vmo_get_token_cost($operation)));
        return false;
    }

    if (function_exists('vmo_is_internal_unmetered_user') && vmo_is_internal_unmetered_user($user_id)) {
        return true;
    }

    $current = vmo_get_token_balance($user_id);
    $required = vmo_get_token_cost($operation);
    $status = vmo_get_credit_status($user_id, $required);

    if ($current < $required) {
        vmo_mark_credit_warning_state($user_id, $status);
        return false;
    }

    if (!empty($status['low_credit'])) {
        vmo_mark_credit_warning_state($user_id, $status);
    }

    return true;
}

/**
 * Get token cost for an operation (v8.1.0 updated - all operations now tracked).
 *
 * @param string $operation Operation type
 * @return int Token cost for the operation
 */
function vmo_get_token_cost($operation) {
    // ── Catalog-first (May 2026 lock) ─────────────────────────────────────
    // The canonical operation catalog in includes/vmo-operation-catalog.php
    // returns the dollar-anchored token cost using the 12×/20×/5×/2.5× markup
    // rules. If a key is present there we honor it as the single source of
    // truth. Legacy switch below remains for backward compatibility for any
    // operation key not yet registered in the catalog.
    if (function_exists('vmo_catalog_get_token_cost')) {
        $catalog_cost = vmo_catalog_get_token_cost($operation);
        if ($catalog_cost !== null && $catalog_cost > 0) {
            return (int) $catalog_cost;
        }
    }

    switch ($operation) {
        // Content Generation
        case 'full_page':
        case 'content':
            // 20.8x markup: OpenAI cost ~$0.08 → $1.67 → 2,500 VMO tokens
            return defined('VMO_TOKENS_FULL_PAGE') ? VMO_TOKENS_FULL_PAGE : 2500;
        case 'section':
            return defined('VMO_TOKENS_SECTION') ? VMO_TOKENS_SECTION : 500;
        case 'refinement':
            return defined('VMO_TOKENS_REFINEMENT') ? VMO_TOKENS_REFINEMENT : 225;
        
        // Image Generation
        case 'image':
        case 'gpt_image':
            return defined('VMO_TOKENS_IMAGE_GENERATION') ? VMO_TOKENS_IMAGE_GENERATION : 1080;
        case 'dalle':
        case 'dalle_image':
            return defined('VMO_TOKENS_DALLE_IMAGE') ? VMO_TOKENS_DALLE_IMAGE : 1080;
        
        // Video Generation (Sora 2 Pro)
        case 'sora_video_4s':
        case 'video_4s':
            return defined('VMO_TOKENS_SORA_VIDEO_4S') ? VMO_TOKENS_SORA_VIDEO_4S : 25200;
        case 'sora_video_8s':
        case 'video_8s':
            return defined('VMO_TOKENS_SORA_VIDEO_8S') ? VMO_TOKENS_SORA_VIDEO_8S : 50400;
        case 'sora_video_12s':
        case 'video_12s':
            return defined('VMO_TOKENS_SORA_VIDEO_12S') ? VMO_TOKENS_SORA_VIDEO_12S : 75600;
        // Sora 2 Standard (lower cost option)
        case 'sora_std_4s':
            return defined('VMO_TOKENS_SORA_STD_4S') ? VMO_TOKENS_SORA_STD_4S : 12600;
        case 'sora_std_8s':
            return defined('VMO_TOKENS_SORA_STD_8S') ? VMO_TOKENS_SORA_STD_8S : 25200;
        case 'sora_std_12s':
            return defined('VMO_TOKENS_SORA_STD_12S') ? VMO_TOKENS_SORA_STD_12S : 37800;
        
        // Effects & Animation
        case 'effects':
        case 'animation':
            return defined('VMO_TOKENS_ADVANCED_EFFECTS') ? VMO_TOKENS_ADVANCED_EFFECTS : 175;
        
        // Chat & Conversation
        case 'chat':
            return defined('VMO_TOKENS_AI_CONVERSATION') ? VMO_TOKENS_AI_CONVERSATION : 50;
        case 'website_chat':
            return defined('VMO_TOKENS_WEBSITE_CHAT') ? VMO_TOKENS_WEBSITE_CHAT : 150;
        case 'consultation':
        case 'consultation_message':
            return defined('VMO_TOKENS_CONSULTATION_MESSAGE') ? VMO_TOKENS_CONSULTATION_MESSAGE : 405;
        case 'ai_recommendations':
            return defined('VMO_TOKENS_AI_RECOMMENDATIONS') ? VMO_TOKENS_AI_RECOMMENDATIONS : 600;
        case 'voice_message':
            return defined('VMO_TOKENS_VOICE_MESSAGE') ? VMO_TOKENS_VOICE_MESSAGE : 375;
        case 'info_extraction':
            return defined('VMO_TOKENS_INFO_EXTRACTION') ? VMO_TOKENS_INFO_EXTRACTION : 300;
        case 'business_summary':
            return defined('VMO_TOKENS_BUSINESS_SUMMARY') ? VMO_TOKENS_BUSINESS_SUMMARY : 600;
        
        // Business Wizard
        case 'business_wizard':
            // 16.7x markup: OpenAI cost ~$0.10 → $1.67 → 2,500 VMO tokens
            return defined('VMO_TOKENS_BUSINESS_WIZARD') ? VMO_TOKENS_BUSINESS_WIZARD : 2500;
        case 'generate_website':
        case 'website_generation':
            // 9x markup: OpenAI cost ~$0.70 → $6.30 → 9,450 VMO tokens
            return defined('VMO_TOKENS_GENERATE_WEBSITE') ? VMO_TOKENS_GENERATE_WEBSITE : 9450;
        case 'website_preview':
            return defined('VMO_TOKENS_WEBSITE_PREVIEW') ? VMO_TOKENS_WEBSITE_PREVIEW : 5000;
        case 'website_analysis':
            return defined('VMO_TOKENS_WEBSITE_ANALYSIS') ? VMO_TOKENS_WEBSITE_ANALYSIS : 400;
        case 'ai_qa_evaluation':
            return defined('VMO_TOKENS_AI_QA_EVALUATION') ? VMO_TOKENS_AI_QA_EVALUATION : 800;
        
        // TTS (Text-to-Speech)
        case 'tts':
        case 'tts_response':
            return defined('VMO_TOKENS_TTS_RESPONSE') ? VMO_TOKENS_TTS_RESPONSE : 81;
        case 'greeting_audio':
            return defined('VMO_TOKENS_GREETING_AUDIO') ? VMO_TOKENS_GREETING_AUDIO : 120;
        case 'tts_long':
        case 'tts_per_1k':
            return defined('VMO_TOKENS_TTS_PER_1K_CHARS') ? VMO_TOKENS_TTS_PER_1K_CHARS : 405;
        
        // Transcription (Whisper)
        case 'transcription':
        case 'whisper':
            return defined('VMO_TOKENS_TRANSCRIPTION_PER_MIN') ? VMO_TOKENS_TRANSCRIPTION_PER_MIN : 81;
        
        // Vision Analysis
        case 'vision':
        case 'image_analysis':
            return defined('VMO_TOKENS_VISION_ANALYSIS') ? VMO_TOKENS_VISION_ANALYSIS : 270;
        
        // Deep Research
        case 'deep_research':
        case 'research':
            // 9x markup: OpenAI cost ~$1.00 → $9.00 → 13,500 VMO tokens
            return defined('VMO_TOKENS_DEEP_RESEARCH') ? VMO_TOKENS_DEEP_RESEARCH : 13500;
        
        // Document Analysis
        case 'document':
        case 'document_analysis':
            return defined('VMO_TOKENS_DOCUMENT_ANALYSIS') ? VMO_TOKENS_DOCUMENT_ANALYSIS : 405;
        
        // Email Operations
        case 'email':
            return defined('VMO_TOKENS_EMAIL') ? VMO_TOKENS_EMAIL : 50;
        case 'email_campaign':
        case 'email_campaign_generation':
            return defined('VMO_TOKENS_EMAIL_CAMPAIGN_GENERATION') ? VMO_TOKENS_EMAIL_CAMPAIGN_GENERATION : 945;
        
        // Contact & CRM
        case 'contact_insights':
        case 'contact_recommendations':
            return defined('VMO_TOKENS_CONTACT_INSIGHTS') ? VMO_TOKENS_CONTACT_INSIGHTS : 405;
        case 'lead_discovery':
            return defined('VMO_TOKENS_LEAD_DISCOVERY') ? VMO_TOKENS_LEAD_DISCOVERY : 1500;
        
        // Quarterly Business Insights (AI-generated performance report)
        case 'quarterly_insights':
        case 'quarterly_report':
            // 12x markup: OpenAI cost ~$0.06 → $0.72 → 1,080 VMO tokens
            return defined('VMO_TOKENS_QUARTERLY_INSIGHTS') ? VMO_TOKENS_QUARTERLY_INSIGHTS : 1080;
        
        // Tax Calculation (AI-powered tax law interpretation)
        case 'tax_calculation':
        case 'tax_estimate':
            // 12x markup: OpenAI cost ~$0.08 → $0.96 → 1,440 VMO tokens
            return defined('VMO_TOKENS_TAX_CALCULATION') ? VMO_TOKENS_TAX_CALCULATION : 1440;
        
        // Business Planning
        case 'roadmap':
        case 'roadmap_generation':
            return defined('VMO_TOKENS_ROADMAP_GENERATION') ? VMO_TOKENS_ROADMAP_GENERATION : 2025;
        
        // Media Integration - AI-powered creative placement (9x markup on ~$0.06 API cost)
        case 'media_integration':
            return defined('VMO_TOKENS_MEDIA_INTEGRATION') ? VMO_TOKENS_MEDIA_INTEGRATION : 900;
        
        // ═══════════════════════════════════════════════════════════════════
        // ✅ RECURRING OPERATIONS v8.1.0 - Weekly/Monthly/Daily automated AI
        // These justify ongoing subscription value
        // ═══════════════════════════════════════════════════════════════════
        
        // Weekly Deep Research - Business intelligence scan
        case 'weekly_deep_research':
        case 'weekly_research':
            // 20x markup: OpenAI cost ~$0.05 → $1.00 → 1,500 VMO tokens
            return defined('VMO_TOKENS_WEEKLY_DEEP_RESEARCH') ? VMO_TOKENS_WEEKLY_DEEP_RESEARCH : 1500;
        
        // Weekly Email Research - Full email marketing automation
        case 'weekly_email_research':
        case 'email_research':
            // 20x markup: OpenAI cost ~$0.10 → $2.00 → 3,000 VMO tokens
            return defined('VMO_TOKENS_WEEKLY_EMAIL_RESEARCH') ? VMO_TOKENS_WEEKLY_EMAIL_RESEARCH : 3000;
        
        // Weekly SEO Audit - Comprehensive SEO analysis
        case 'weekly_seo_audit':
        case 'seo_audit':
            // 20x markup: OpenAI cost ~$0.08 → $1.60 → 2,400 VMO tokens
            return defined('VMO_TOKENS_WEEKLY_SEO_AUDIT') ? VMO_TOKENS_WEEKLY_SEO_AUDIT : 2400;
        
        // Monthly Blog Generation - AI-written SEO blog post
        case 'monthly_blog':
        case 'automated_blog':
            // 20x markup: OpenAI cost ~$0.05 → $1.00 → 1,500 VMO tokens
            return defined('VMO_TOKENS_MONTHLY_BLOG') ? VMO_TOKENS_MONTHLY_BLOG : 1500;
        
        // Competitor Monitoring - Market intelligence scan
        case 'competitor_monitoring':
        case 'competitor_analysis':
            // 20x markup: OpenAI cost ~$0.05 → $1.00 → 1,500 VMO tokens
            return defined('VMO_TOKENS_COMPETITOR_MONITORING') ? VMO_TOKENS_COMPETITOR_MONITORING : 1500;
        
        // Email Strategy Review - Monthly performance analysis
        case 'email_strategy_review':
        case 'email_strategy':
            // 20x markup: OpenAI cost ~$0.04 → $0.80 → 1,200 VMO tokens
            return defined('VMO_TOKENS_EMAIL_STRATEGY_REVIEW') ? VMO_TOKENS_EMAIL_STRATEGY_REVIEW : 1200;
        
        // Content Embeddings - Daily AI knowledge base update
        case 'content_embeddings':
        case 'embeddings':
            return defined('VMO_TOKENS_CONTENT_EMBEDDINGS') ? VMO_TOKENS_CONTENT_EMBEDDINGS : 300;
        
        // Call Summary - Phone call transcription + AI summary
        case 'call_summary':
        case 'call_transcription':
            return defined('VMO_TOKENS_CALL_SUMMARY') ? VMO_TOKENS_CALL_SUMMARY : 450;
        
        // Publishing
        case 'publish':
            return vmo_get_publish_cost();
        
        // Premium Features
        case 'premium':
            return defined('VMO_TOKENS_PREMIUM_FEATURES') ? VMO_TOKENS_PREMIUM_FEATURES : 1000;
        
        // ═══════════════════════════════════════════════════════════════════
        // ✅ TWILIO TELEPHONY COSTS v9.0.0 - Per-usage billing at 2.5x markup
        // 1 VMO Token = $0.000667 (1,500 tokens = $1.00)
        // ═══════════════════════════════════════════════════════════════════
        
        // Outbound call: Twilio $0.014/min → 2.5x = $0.035/min ≈ 53 tokens/min
        case 'call_outbound':
        case 'call_outbound_min':
            return defined('VMO_TOKENS_CALL_OUTBOUND_MIN') ? VMO_TOKENS_CALL_OUTBOUND_MIN : 53;
        
        // Inbound call: Twilio $0.0085/min → 2.5x = $0.021/min ≈ 33 tokens/min
        case 'call_inbound':
        case 'call_inbound_min':
            return defined('VMO_TOKENS_CALL_INBOUND_MIN') ? VMO_TOKENS_CALL_INBOUND_MIN : 33;
        
        // Outbound SMS: Twilio $0.0079 → 2.5x = $0.020 ≈ 30 tokens/msg
        case 'sms_outbound':
            return defined('VMO_TOKENS_SMS_OUTBOUND') ? VMO_TOKENS_SMS_OUTBOUND : 30;
        
        // Inbound SMS: Twilio $0.0075 → 2.5x = $0.019 ≈ 29 tokens/msg
        case 'sms_inbound':
            return defined('VMO_TOKENS_SMS_INBOUND') ? VMO_TOKENS_SMS_INBOUND : 29;
        
        // A2P surcharge per message: Twilio $0.005 → 2.5x = $0.013 ≈ 20 tokens
        case 'a2p_surcharge':
            return defined('VMO_TOKENS_A2P_SURCHARGE') ? VMO_TOKENS_A2P_SURCHARGE : 20;
        
        // Call recording: Twilio $0.0025/min → 2.5x = $0.006/min ≈ 11 tokens/min
        case 'recording':
        case 'call_recording':
            return defined('VMO_TOKENS_RECORDING_MIN') ? VMO_TOKENS_RECORDING_MIN : 11;
        
        // Phone TTS (OpenAI Speech): $0.015/1K chars → 9x = $0.135 → 203 tokens/1K
        // AI operation connected to phone system - charged at 9x vs OpenAI cost
        case 'phone_tts':
        case 'phone_tts_1k':
            return defined('VMO_TOKENS_PHONE_TTS_1K') ? VMO_TOKENS_PHONE_TTS_1K : 203;
        
        // Phone AI conversation turn (GPT-5.5-nano): $0.003/turn → 9x = $0.027 → 41 tokens
        // AI operation connected to phone system - charged at 9x vs OpenAI cost
        case 'phone_ai_turn':
        case 'phone_gpt_turn':
            return defined('VMO_TOKENS_PHONE_AI_TURN') ? VMO_TOKENS_PHONE_AI_TURN : 41;
        
        // Phone number activation: Twilio $1 → 2.5x = $2.50 ≈ 3,750 tokens
        case 'phone_activation':
            return defined('VMO_TOKENS_PHONE_ACTIVATION') ? VMO_TOKENS_PHONE_ACTIVATION : 3750;
        
        // Phone number monthly rental: Twilio $1.15/mo → 2.5x = $2.50/mo ≈ 3,750 tokens
        case 'phone_rental':
        case 'phone_monthly':
            return defined('VMO_TOKENS_PHONE_RENTAL') ? VMO_TOKENS_PHONE_RENTAL : 3750;
        
        // Optional SMS add-on brand registration: carrier $4 one-time + $5 VMO flat = $9 ≈ 13,500 tokens
        case 'a2p_brand':
            return defined('VMO_TOKENS_A2P_BRAND') ? VMO_TOKENS_A2P_BRAND : 13500;
        
        // Optional SMS add-on campaign monthly: carrier $15/mo + $5 VMO flat = $20/mo ≈ 30,000 tokens
        case 'a2p_campaign':
            return defined('VMO_TOKENS_A2P_CAMPAIGN_MONTHLY') ? VMO_TOKENS_A2P_CAMPAIGN_MONTHLY : 30000;
        
        // Simultaneous ring extra leg: Twilio $0.014/min → 2.5x ≈ 53 tokens/min
        case 'extra_call_leg':
            return defined('VMO_TOKENS_EXTRA_LEG_MIN') ? VMO_TOKENS_EXTRA_LEG_MIN : 53;
        
        default:
            // Log unknown operation for debugging
            error_log('VMO Token Manager: Unknown operation type: ' . $operation);
            return 0;
    }
}

/**
 * Get publishing cost based on user subscription status.
 *
 * @param int $user_id Optional user ID, defaults to current user
 * @return int Token cost for publishing
 */
function vmo_get_publish_cost($user_id = null) {
    if (!$user_id) $user_id = get_current_user_id();
    
    $subscription_status = get_user_meta($user_id, 'vmo_subscription_status', true);
    $token_type = get_user_meta($user_id, 'vmo_token_type', true);
    
    // Treat all paid plans (legacy and new packs) as subscribers for publish cost
    $subscriber_statuses = [
        'starter', 'pro', 'elite', // legacy
        'starter_pack', 'pro_pack', 'business_pack', 'enterprise_pack', // new packs
        'business', 'enterprise', 'professional' // other legacy aliases
    ];
    if (in_array($subscription_status, $subscriber_statuses, true)) {
        return defined('VMO_TOKENS_PUBLISH_SUBSCRIBER') ? VMO_TOKENS_PUBLISH_SUBSCRIBER : 500;
    } elseif ($token_type === 'topup') {
        return defined('VMO_TOKENS_PUBLISH_TOPUP') ? VMO_TOKENS_PUBLISH_TOPUP : 550;
    } else {
        return defined('VMO_TOKENS_PUBLISH_FREE') ? VMO_TOKENS_PUBLISH_FREE : 625;
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// PLAN-BASED BACKGROUND OPERATIONS v9.0.0
// Tester: no background ops
// Starter: monthly background ops
// Pro: twice-monthly background ops
// Elite/Enterprise: weekly background ops
// ═══════════════════════════════════════════════════════════════════════════════

/**
 * Get background operations configuration based on user's plan.
 * 
 * @param int $user_id
 * @return array Configuration with enabled, frequency, cron_interval, reason
 */
function vmo_get_background_ops_config($user_id) {
    $subscription_status = get_user_meta($user_id, 'vmo_subscription_status', true);

    if (function_exists('vmo_get_plan_background_ops_config')) {
        return vmo_get_plan_background_ops_config($subscription_status);
    }

    return [
        'enabled' => false,
        'frequency' => 'none',
        'cron_interval' => null,
        'reason' => 'Background operations are not available until a paid plan is active.',
        'monthly_ops' => 0,
        'plan' => 'free',
    ];
}

/**
 * Check if user's plan allows background operations.
 * 
 * @param int $user_id
 * @return bool True if background ops are enabled for this plan
 */
function vmo_can_run_background_ops($user_id) {
    $config = vmo_get_background_ops_config($user_id);
    return $config['enabled'] === true;
}

/**
 * Get the WordPress cron interval for a user's background ops.
 * 
 * @param int $user_id
 * @return string|null WordPress cron interval ('weekly', 'twicemonthly', 'monthly') or null if disabled
 */
function vmo_get_background_ops_interval($user_id) {
    $config = vmo_get_background_ops_config($user_id);
    return $config['cron_interval'];
}

/**
 * Send upgrade prompt when Tester user attempts to use background ops feature.
 * 
 * @param int $user_id
 * @param string $operation The operation that was blocked
 */
function vmo_send_background_ops_upgrade_prompt($user_id, $operation) {
    $config = vmo_get_background_ops_config($user_id);
    
    if ($config['enabled']) {
        return; // User has access, no prompt needed
    }
    
    // Operation display names
    $operation_names = [
        'weekly_deep_research' => 'Weekly Deep Business Research',
        'weekly_email_research' => 'Weekly Email Marketing Automation',
        'seo_audit' => 'SEO Performance Audit',
        'competitor_monitoring' => 'Competitor Intelligence Monitoring',
        'blog_generation' => 'Automated SEO Blog Generation',
        'ranking_tracking' => 'Keyword Ranking Tracking',
        'email_strategy_review' => 'Email Strategy Review',
    ];
    
    $op_name = $operation_names[$operation] ?? ucwords(str_replace('_', ' ', (string) $operation));
    
    // Store notification for dashboard display
    $notifications = get_user_meta($user_id, 'vmo_upgrade_prompts', true) ?: [];
    $notifications[] = [
        'operation' => $operation,
        'operation_name' => $op_name,
        'message' => "💡 {$op_name} is a premium feature. Upgrade to Starter ($99.99/mo) for monthly automation, Pro ($199.99/mo) for twice-monthly automation, or Elite ($499.99/mo) for weekly automation.",
        'reason' => $config['reason'],
        'timestamp' => current_time('mysql'),
    ];
    
    // Keep only last 10 notifications
    $notifications = array_slice($notifications, -10);
    update_user_meta($user_id, 'vmo_upgrade_prompts', $notifications);
    
    // Log for debugging
    error_log("VMO Background Ops: Upgrade prompt sent to user {$user_id} for {$operation}");
}

/**
 * Register custom cron schedules for plan-based recurring operations.
 */
add_filter('cron_schedules', function($schedules) {
    if (!isset($schedules['weekly'])) {
        $schedules['weekly'] = [
            'interval' => 7 * DAY_IN_SECONDS,
            'display' => __('Weekly (Every 7 Days)', 'vmo'),
        ];
    }
    if (!isset($schedules['monthly'])) {
        $schedules['monthly'] = [
            'interval' => 30 * DAY_IN_SECONDS,
            'display' => __('Monthly (Every 30 Days)', 'vmo'),
        ];
    }
    if (!isset($schedules['twicemonthly'])) {
        $schedules['twicemonthly'] = [
            'interval' => 14 * DAY_IN_SECONDS, // 14 days
            'display' => __('Twice Monthly (Every 14 Days)', 'vmo'),
        ];
    }
    return $schedules;
});

/**
 * Add tokens with expiration tracking (v7.0.1).
 *
 * @param int $user_id
 * @param int $amount
 * @param string $type 'trial', 'subscriber', 'topup'
 */
function vmo_add_tokens_with_expiration($user_id, $amount, $type) {
    $token_history = get_user_meta($user_id, 'vmo_token_history', true) ?: [];
    
    // Calculate expiration based on type
    $expiration = 0;
    switch ($type) {
        case 'trial':
            $expiration = current_time('timestamp') + (7 * 24 * 60 * 60); // 7 days
            break;
        case 'subscriber':
            $expiration = current_time('timestamp') + (6 * 30 * 24 * 60 * 60); // 6 months
            break;
        case 'topup':
            $expiration = current_time('timestamp') + (3 * 30 * 24 * 60 * 60); // 3 months
            break;
    }
    
    // Add new token batch
    $token_history[] = [
        'amount' => $amount,
        'type' => $type,
        'added_at' => current_time('timestamp'),
        'expires_at' => $expiration,
        'remaining' => $amount
    ];
    
    update_user_meta($user_id, 'vmo_token_history', $token_history);
    
    // Update total balance
    $current_balance = vmo_get_token_balance($user_id);
    update_user_meta($user_id, 'vmo_token_balance', $current_balance + $amount);
    update_user_meta($user_id, 'vmo_token_type', $type);
}

if (!function_exists('vmo_add_tokens')) {
    /**
     * Add tokens back to a user's balance using the same batch tracking as purchases.
     *
     * @param int $user_id
     * @param int $amount
     * @param string $type
     * @param string $description
     * @return bool
     */
    function vmo_add_tokens($user_id, $amount, $type = 'topup', $description = '') {
        $user_id = intval($user_id);
        $amount = intval($amount);

        if ($user_id <= 0 || $amount <= 0) {
            return false;
        }

        vmo_add_tokens_with_expiration($user_id, $amount, $type ?: 'topup');

        $logs = get_option('vmo_token_usage_logs', array());
        $logs[] = array(
            'user_id' => $user_id,
            'operation' => $description ?: 'token_credit',
            'tokens_used' => -$amount,
            'dollar_value' => -($amount * VMO_TOKEN_RATE),
            'timestamp' => current_time('mysql'),
        );

        if (count($logs) > 10000) {
            $logs = array_slice($logs, -10000);
        }

        update_option('vmo_token_usage_logs', $logs);

        if (function_exists('vmo_can_user_use_cost_bearing_services') && vmo_can_user_use_cost_bearing_services($user_id)) {
            vmo_clear_recurring_service_grace($user_id);
        }

        return true;
    }
}

/**
 * Deduct tokens using FIFO (oldest first) system.
 *
 * @param int $user_id
 * @param int $amount
 * @return bool Success
 */
function vmo_deduct_tokens_fifo($user_id, $amount) {
    global $wpdb;

    // Acquire a user-specific lock to prevent concurrent double-spend
    $lock_key = 'vmo_token_lock_' . (int) $user_id;
    $lock_acquired = $wpdb->query($wpdb->prepare(
        "SELECT GET_LOCK(%s, 5)",
        $lock_key
    ));
    if (!$lock_acquired) {
        error_log('[VMO Tokens] Could not acquire lock for user ' . $user_id);
        return false;
    }

    try {
        // Clean expired tokens first
        vmo_clean_expired_tokens($user_id);

        // Re-read balance inside the lock — this is the authoritative read
        $current_balance = vmo_get_token_balance($user_id);
        if ($current_balance < $amount) {
            return false;
        }

        $token_history = get_user_meta($user_id, 'vmo_token_history', true) ?: [];
        $remaining_to_deduct = $amount;

        // Sort by added_at (FIFO)
        usort($token_history, function($a, $b) {
            return $a['added_at'] - $b['added_at'];
        });

        // Deduct from oldest tokens first
        foreach ($token_history as &$batch) {
            if ($remaining_to_deduct <= 0) break;
            if ($batch['remaining'] <= 0) continue;

            $deduct_from_batch = min($batch['remaining'], $remaining_to_deduct);
            $batch['remaining'] -= $deduct_from_batch;
            $remaining_to_deduct -= $deduct_from_batch;
        }

        // Atomic update: history and balance in one transaction
        $new_balance = $current_balance - $amount;
        update_user_meta($user_id, 'vmo_token_history', $token_history);
        update_user_meta($user_id, 'vmo_token_balance', $new_balance);
        if (function_exists('vmo_sync_license_credit_balance')) {
            vmo_sync_license_credit_balance($user_id, $new_balance);
        }

        return true;
    } finally {
        $wpdb->query($wpdb->prepare("SELECT RELEASE_LOCK(%s)", $lock_key));
    }
}

/**
 * Clean expired tokens from user's history.
 *
 * @param int $user_id
 */
function vmo_clean_expired_tokens($user_id) {
    $token_history = get_user_meta($user_id, 'vmo_token_history', true) ?: [];
    $current_time = current_time('timestamp');
    $total_expired = 0;
    
    // Remove expired batches and calculate expired amount
    $token_history = array_filter($token_history, function($batch) use ($current_time, &$total_expired) {
        if ($current_time > $batch['expires_at']) {
            $total_expired += $batch['remaining'];
            return false;
        }
        return true;
    });
    
    if ($total_expired > 0) {
        // Update history and balance
        update_user_meta($user_id, 'vmo_token_history', $token_history);
        $current_balance = vmo_get_token_balance($user_id);
        update_user_meta($user_id, 'vmo_token_balance', max(0, $current_balance - $total_expired));
    }
}

/**
 * Deduct tokens for a specific operation (v7.0.1 updated).
 *
 * @param int $user_id
 * @param string $operation 'full_page', 'section', 'refinement', 'image', 'effects', 'publish'
 * @return bool Success
 */
function vmo_deduct_tokens_for_operation($user_id, $operation) {
    if (function_exists('vmo_is_user_service_locked') && vmo_is_user_service_locked($user_id)) {
        if (function_exists('vmo_log_billing_event')) {
            vmo_log_billing_event($user_id, $operation, 0, 'Blocked: services locked for billing recovery', 'internal', 0.0, '', false);
        }
        return false;
    }

    if (function_exists('vmo_is_internal_unmetered_user') && vmo_is_internal_unmetered_user($user_id)) {
        return true;
    }

    $required = vmo_get_token_cost($operation);
    $current = vmo_get_token_balance($user_id);
    $status = vmo_get_credit_status($user_id, $required);
    
    if ($current < $required) {
        vmo_mark_credit_warning_state($user_id, $status);
        if (function_exists('vmo_log_billing_event')) {
            vmo_log_billing_event($user_id, $operation, $required, 'Blocked: insufficient VMO Credits', 'internal', 0.0, '', false);
        }
        return false;
    }
    
    // Use FIFO deduction system
    $success = vmo_deduct_tokens_fifo($user_id, $required);
    
    if ($success) {
        // Log the operation for analytics
        vmo_log_token_usage($user_id, $operation, $required);
        
        // Check if user needs upgrade notification
        vmo_check_upgrade_notification($user_id);
        vmo_mark_credit_warning_state($user_id, vmo_get_credit_status($user_id));
    }
    
    return $success;
}

/**
 * Log token usage for analytics (v7.0.1).
 *
 * @param int $user_id
 * @param string $operation
 * @param int $tokens_used
 */
function vmo_log_token_usage($user_id, $operation, $tokens_used) {
    $log_entry = [
        'user_id' => $user_id,
        'operation' => $operation,
        'tokens_used' => $tokens_used,
        'dollar_value' => $tokens_used * VMO_TOKEN_RATE,
        'timestamp' => current_time('mysql')
    ];
    
    // Store in WordPress option for analytics
    $logs = get_option('vmo_token_usage_logs', []);
    $logs[] = $log_entry;
    
    // Keep only last 10,000 entries for performance
    if (count($logs) > 10000) {
        $logs = array_slice($logs, -10000);
    }
    
    update_option('vmo_token_usage_logs', $logs);

    if (function_exists('vmo_cfo_log_token_usage')) {
        vmo_cfo_log_token_usage($user_id, $operation, $operation, $tokens_used, [
            'description' => 'Token manager usage event',
            'metadata' => ['legacy_option_log' => true],
        ]);
    }

    // Also write to centralized billing DB table
    vmo_log_billing_event($user_id, $operation, $tokens_used);
}

// ═══════════════════════════════════════════════════════════════════════════════
// GUARDRAIL G2: Internal Reserve-Income Classification
// Classifies every billing event as:
//   - cost_recovery: VMO passes through the provider's actual cost with minimal/no markup
//                    (hosting, domain registration, domain connection, email/mailbox)
//   - margin_income: VMO charges a markup above provider cost for profit
//                    (AI generation, content, images, research, refinements, subscriptions)
//   - passthrough:   Direct provider cost pass-through with 2.5x markup (Twilio voice/SMS)
// ═══════════════════════════════════════════════════════════════════════════════

/**
 * Classify a billing event for internal accounting.
 *
 * @param string $action_type  The billing event action key
 * @param string $provider     Provider name (openai, twilio, vultr, etc.)
 * @return string 'cost_recovery' | 'margin_income' | 'passthrough'
 */
function vmo_classify_billing_event($action_type, $provider = 'internal') {
    // Cost recovery: infrastructure & registration fees VMO pays on behalf of the user
    $cost_recovery_actions = [
        'hosting', 'domain_registration', 'domain_connection', 'mailbox', 'email_setup',
        'vultr_hosting', 'dns_setup', 'ssl_setup', 'server_provision',
    ];

    // Passthrough: Twilio telephony costs passed at 2.5x markup
    $passthrough_actions = [
        'call_outbound', 'call_outbound_min', 'call_inbound', 'call_inbound_min',
        'sms_outbound', 'sms_inbound', 'a2p_surcharge', 'recording', 'call_recording',
        'phone_activation', 'phone_rental', 'phone_monthly',
        'a2p_brand', 'a2p_campaign', 'a2p_campaign_monthly', 'extra_call_leg',
    ];

    if (in_array($action_type, $cost_recovery_actions, true) || $provider === 'vultr' || $provider === 'namecheap' || $provider === 'mailcow') {
        return 'cost_recovery';
    }

    if (in_array($action_type, $passthrough_actions, true) || ($provider === 'twilio' && strpos($action_type, 'phone') !== false)) {
        return 'passthrough';
    }

    // Everything else is margin income (AI features, content generation, etc.)
    return 'margin_income';
}

/**
 * Centralized billing event log — single source of truth for all charges.
 *
 * Writes every billable event to {prefix}vmo_billing_events with full audit fields.
 * This replaces the fragmented wp_options / user_meta / error_log approach.
 *
 * @param int    $user_id
 * @param string $action_type     Machine-readable key (e.g. 'phone_rental', 'media_integration')
 * @param int    $tokens_charged   Number of tokens deducted
 * @param string $description     Human-readable description (optional)
 * @param string $provider        Provider name: 'openai', 'twilio', 'internal' (optional)
 * @param float  $provider_cost   Estimated provider cost in USD (optional)
 * @param string $reference_id    Call SID, job ID, or request ID for tracing (optional)
 * @param bool   $success         Whether the deduction succeeded
 * @param string $billing_class   'cost_recovery', 'margin_income', or 'passthrough' (Guardrail G2)
 */
function vmo_log_billing_event(
    int $user_id,
    string $action_type,
    int $tokens_charged,
    string $description = '',
    string $provider = 'internal',
    float $provider_cost = 0.0,
    string $reference_id = '',
    bool $success = true,
    string $billing_class = ''
) {
    global $wpdb;
    $table = $wpdb->prefix . 'vmo_billing_events';

    // Self-healing: create table if it doesn't exist
    if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $wpdb->esc_like($table))) !== $table) {
        $charset_collate = $wpdb->get_charset_collate();
        require_once ABSPATH . 'wp-admin/includes/upgrade.php';
        dbDelta("CREATE TABLE IF NOT EXISTS {$table} (
            id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
            user_id BIGINT UNSIGNED NOT NULL,
            action_type VARCHAR(100) NOT NULL,
            provider VARCHAR(50) DEFAULT 'internal',
            tokens_charged INT UNSIGNED DEFAULT 0,
            dollar_value DECIMAL(10,4) DEFAULT 0.0000,
            provider_cost_usd DECIMAL(10,6) DEFAULT 0.000000,
            billing_class VARCHAR(20) DEFAULT '' COMMENT 'cost_recovery|margin_income|passthrough (Guardrail G2)',
            reference_id VARCHAR(255) DEFAULT '',
            description TEXT DEFAULT '',
            success TINYINT(1) DEFAULT 1,
            client_ip VARCHAR(45) DEFAULT '',
            created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
            INDEX idx_user_id (user_id),
            INDEX idx_action_type (action_type),
            INDEX idx_created_at (created_at),
            INDEX idx_reference (reference_id),
            INDEX idx_billing_class (billing_class)
        ) {$charset_collate};");
    }

    // ── Guardrail G2: Auto-classify billing events ──
    if (empty($billing_class)) {
        $billing_class = vmo_classify_billing_event($action_type, $provider);
    }

    $dollar_value = round($tokens_charged * (defined('VMO_TOKEN_RATE') ? VMO_TOKEN_RATE : 0.000667), 4);
    $client_ip = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field($_SERVER['REMOTE_ADDR']) : '';

    $wpdb->insert($table, array(
        'user_id'           => $user_id,
        'action_type'       => $action_type,
        'provider'          => $provider,
        'tokens_charged'    => $tokens_charged,
        'dollar_value'      => $dollar_value,
        'provider_cost_usd' => $provider_cost,
        'billing_class'     => $billing_class,
        'reference_id'      => $reference_id,
        'description'       => $description,
        'success'           => $success ? 1 : 0,
        'client_ip'         => $client_ip,
        'created_at'        => current_time('mysql'),
    ), array('%d','%s','%s','%d','%f','%f','%s','%s','%s','%d','%s','%s'));
}

/**
 * Check if user needs upgrade notification (70% depletion threshold).
 *
 * @param int $user_id
 */
function vmo_check_upgrade_notification($user_id) {
    $status = vmo_get_credit_status($user_id);
    $last_notification = get_user_meta($user_id, 'vmo_last_upgrade_notification', true);
    
    if ((!empty($status['no_credit']) || !empty($status['low_credit'])) && (time() - (int)$last_notification) > HOUR_IN_SECONDS) {
        update_user_meta($user_id, 'vmo_last_upgrade_notification', time());
        vmo_mark_credit_warning_state($user_id, $status);
    }
}

/**
 * Legacy function for backward compatibility.
 * 
 * @deprecated Use vmo_deduct_tokens_for_operation() instead
 * @param int $user_id
 * @param int $openai_token_cost
 * @return bool Success
 */
function vmo_deduct_tokens_by_openai_usage($user_id, $openai_token_cost) {
    // Estimate operation type based on token usage
    if ($openai_token_cost > 1000) {
        $operation = 'content';
    } elseif ($openai_token_cost > 500) {
        $operation = 'image';
    } else {
        $operation = 'refinement';
    }
    
    return vmo_deduct_tokens_for_operation($user_id, $operation);
}

/**
 * Generic token deduction with internal unmetered bypass and logging.
 *
 * @param int $user_id
 * @param int $amount
 * @param string $operation_type
 * @param string $description Optional description
 * @return bool Success
 */
function vmo_deduct_tokens($user_id, $amount, $operation_type = 'generic', $description = '') {
    if (function_exists('vmo_is_user_service_locked') && vmo_is_user_service_locked($user_id)) {
        if (function_exists('vmo_log_billing_event')) {
            vmo_log_billing_event($user_id, $operation_type, (int) $amount, 'Blocked: services locked for billing recovery' . ($description ? ' - ' . $description : ''), 'internal', 0.0, '', false);
        }
        return false;
    }

    if (function_exists('vmo_is_internal_unmetered_user') && vmo_is_internal_unmetered_user($user_id)) {
        return true;
    }

    $current = vmo_get_token_balance($user_id);
    $status = vmo_get_credit_status($user_id, $amount);
    if ($current < $amount) {
        vmo_mark_credit_warning_state($user_id, $status);
        if (function_exists('vmo_log_billing_event')) {
            vmo_log_billing_event($user_id, $operation_type, (int) $amount, 'Blocked: insufficient VMO Credits' . ($description ? ' - ' . $description : ''), 'internal', 0.0, '', false);
        }
        return false;
    }

    // Use FIFO deduction system
    $success = vmo_deduct_tokens_fifo($user_id, $amount);
    
    if ($success) {
        // Log the operation for analytics
        vmo_log_token_usage($user_id, $operation_type, $amount);
        
        // Check if user needs upgrade notification
        vmo_check_upgrade_notification($user_id);
        vmo_mark_credit_warning_state($user_id, vmo_get_credit_status($user_id));
    }
    
    return $success;
}

/**
 * Decide whether a logged-in VMO AJAX action should be hard-blocked when balance is zero.
 *
 * @param string $action
 * @return bool
 */
function vmo_should_credit_gate_ajax_action($action) {
    $action = sanitize_key((string) $action);

    if ($action === '' || strpos($action, 'vmo_') !== 0) {
        return false;
    }

    $allowed_exact = array(
        'vmo_get_token_balance',
        'vmo_create_checkout',
        'vmo_subscription_checkout',
        'vmo_a2p_status_webhook',
        'vmo_twilio_voice_webhook',
        'vmo_twilio_sms_webhook',
        // Realtime usage meter must run even at zero balance so it can charge
        // the final elapsed minute(s) and return halt=true to end the live
        // voice call. It only ever deducts or signals halt — never grants credit.
        'vmo_realtime_usage',
        'vmo_sd_realtime_usage',
    );

    if (in_array($action, $allowed_exact, true)) {
        return false;
    }

    $allowed_fragments = array(
        'checkout',
        'topup',
        'top_up',
        'upgrade',
        'purchase',
        'billing_portal',
        'payment_success',
        'stripe_webhook',
        'webhook',
    );

    foreach ($allowed_fragments as $fragment) {
        if (strpos($action, $fragment) !== false) {
            return false;
        }
    }

    return true;
}

/**
 * Decide whether an authenticated VMO REST route should be hard-blocked at zero balance.
 *
 * @param string $route
 * @return bool
 */
function vmo_should_credit_gate_rest_route($route) {
    $route = (string) $route;
    if (!preg_match('#^/(vmo|vmo-builder|vmo-manager|vmo-phone)(/|$)#', $route)) {
        return false;
    }

    $allowed_fragments = array(
        '/invoice/',
        '/email/track/',
        '/email/unsubscribe',
        '/track/',
        'checkout',
        'topup',
        'top-up',
        'upgrade',
        'purchase',
        'webhook',
        'callback',
        'stripe',
        '/quicklaunch/plans',
        '/quicklaunch/check-domain',
        '/quicklaunch/regions',
        '/quicklaunch/embed-bootstrap',
        '/quicklaunch/create-checkout',
        '/quicklaunch/status',
        '/subscription/status',
        '/validate-license',
        '/ping',
    );

    foreach ($allowed_fragments as $fragment) {
        if (stripos($route, $fragment) !== false) {
            return false;
        }
    }

    return true;
}

add_action('admin_init', function() {
    if (!(defined('DOING_AJAX') && DOING_AJAX) || !is_user_logged_in()) {
        return;
    }

    $raw_action = isset($_REQUEST['action']) && !is_array($_REQUEST['action']) ? wp_unslash($_REQUEST['action']) : '';
    $action = sanitize_key((string) $raw_action);
    if (!vmo_should_credit_gate_ajax_action($action)) {
        return;
    }

    $user_id = get_current_user_id();
    $status = vmo_get_credit_status($user_id);
    if (!empty($status['service_locked'])) {
        wp_send_json_error(vmo_get_credit_gate_payload($user_id, 0, $action), 402);
    }

    if (function_exists('vmo_is_internal_unmetered_user') && vmo_is_internal_unmetered_user($user_id)) {
        return;
    }

    if (empty($status['no_credit'])) {
        if (!empty($status['low_credit'])) {
            vmo_mark_credit_warning_state($user_id, $status);
        }
        return;
    }

    wp_send_json_error(vmo_get_credit_gate_payload($user_id, 0, $action), 402);
}, 1);

add_filter('rest_request_before_callbacks', function($response, $handler, $request) {
    if (!empty($response) || !($request instanceof WP_REST_Request)) {
        return $response;
    }

    $route = $request->get_route();
    if (!vmo_should_credit_gate_rest_route($route)) {
        return $response;
    }

    $user_id = get_current_user_id();
    if ($user_id <= 0) {
        return $response;
    }

    $status = vmo_get_credit_status($user_id);
    if (!empty($status['service_locked'])) {
        $payload = vmo_get_credit_gate_payload($user_id, 0, $route);
        return new WP_Error('service_locked', $payload['message'], array(
            'status' => 402,
            'code' => 'service_locked',
            'credit_status' => $payload['credit_status'],
            'service_lock_reason' => $payload['service_lock_reason'],
            'recoverable_plan' => $payload['recoverable_plan'],
        ));
    }

    if (function_exists('vmo_is_internal_unmetered_user') && vmo_is_internal_unmetered_user($user_id)) {
        return $response;
    }

    if (empty($status['no_credit'])) {
        if (!empty($status['low_credit'])) {
            vmo_mark_credit_warning_state($user_id, $status);
        }
        return $response;
    }

    $payload = vmo_get_credit_gate_payload($user_id, 0, $route);
    return new WP_Error('insufficient_tokens', $payload['message'], array(
        'status' => 402,
        'code' => 'insufficient_tokens',
        'credit_status' => $payload['credit_status'],
        'tokens_available' => $payload['tokens_available'],
    ));
}, 1, 3);

// ═══════════════════════════════════════════════════════════════════════════
// ZERO-LEAK WRAPPER (May 2026)
// vmo_paid_api_call() is the mandatory gateway for every external paid
// provider call (OpenAI, Twilio, Sora, etc). It:
//   1. Pre-flights credit balance against the catalog's token cost
//   2. Executes the caller-provided request closure
//   3. On successful response, deducts the correct token amount
//   4. Logs the billing event for audit / margin reporting
//
// Callers MUST route paid provider calls through this wrapper to guarantee
// every successful request is charged. Direct wp_remote_post() calls to a
// paid provider without this wrapper are leaks and must be flagged.
// ═══════════════════════════════════════════════════════════════════════════

if (!function_exists('vmo_paid_api_call')) {
    /**
     * Execute a paid provider API call with mandatory credit metering.
     *
     * @param int      $user_id        WP user id being billed
     * @param string   $operation_key  Catalog operation key (must exist in vmo_operation_catalog())
     * @param callable $request_fn     Closure that performs the actual provider call.
     *                                 Must return either a WP_Error or an array/object/response
     *                                 that is_wp_error()==false on success.
     * @param array    $args           Optional. Keys:
     *                                   - 'multiplier' (float): scale the catalog token cost (e.g.
     *                                     for per-minute calls with measured duration). Default 1.0
     *                                   - 'override_tokens' (int): exact token cost override (bypass catalog math)
     *                                   - 'description' (string): human-readable log description
     *                                   - 'provider' (string): provider name for billing log
     *                                   - 'skip_preflight' (bool): true to skip the up-front balance check
     *                                     (use only when the provider returns usage data first)
     * @return mixed WP_Error on failure (insufficient balance / provider error),
     *               otherwise whatever $request_fn returned.
     */
    function vmo_paid_api_call($user_id, $operation_key, callable $request_fn, $args = []) {
        $user_id = (int) $user_id;
        $operation_key = (string) $operation_key;

        // Internal staff / unmetered users still execute the call but skip deduction
        $is_unmetered = function_exists('vmo_is_internal_unmetered_user')
            && vmo_is_internal_unmetered_user($user_id);

        // Resolve token cost from catalog (or override)
        $tokens_required = isset($args['override_tokens']) && (int) $args['override_tokens'] > 0
            ? (int) $args['override_tokens']
            : (int) vmo_get_token_cost($operation_key);

        $multiplier = isset($args['multiplier']) ? max(0.0001, (float) $args['multiplier']) : 1.0;
        $tokens_required = (int) ceil($tokens_required * $multiplier);

        if ($tokens_required <= 0 && function_exists('vmo_catalog_has_operation')
            && !vmo_catalog_has_operation($operation_key)) {
            // Unknown operation key — refuse to leak. Caller must register it in the catalog.
            return new WP_Error('vmo_unknown_operation', sprintf(
                'Operation "%s" is not registered in the VMO operation catalog. Refusing to call provider without a charge rule.',
                $operation_key
            ));
        }

        // Pre-flight balance check
        if (!$is_unmetered && empty($args['skip_preflight'])) {
            if (function_exists('vmo_is_user_service_locked') && vmo_is_user_service_locked($user_id)) {
                return new WP_Error('vmo_service_locked', 'User services are locked pending billing recovery.', ['status' => 402]);
            }
            $balance = function_exists('vmo_get_token_balance') ? (int) vmo_get_token_balance($user_id) : 0;
            if ($balance < $tokens_required) {
                if (function_exists('vmo_log_billing_event')) {
                    vmo_log_billing_event($user_id, $operation_key, $tokens_required,
                        'Blocked: insufficient VMO Credits for paid API call', 'internal', 0.0, '', false);
                }
                return new WP_Error('insufficient_tokens', 'Insufficient VMO Credits for this operation.', [
                    'status'          => 402,
                    'tokens_required' => $tokens_required,
                    'tokens_available'=> $balance,
                ]);
            }
        }

        // Execute the provider call
        $result = call_user_func($request_fn);

        if (is_wp_error($result)) {
            // Provider failed — do not charge
            if (function_exists('vmo_log_billing_event')) {
                vmo_log_billing_event($user_id, $operation_key, 0,
                    'Provider call failed: ' . $result->get_error_message(),
                    isset($args['provider']) ? $args['provider'] : 'internal',
                    0.0, '', false);
            }
            return $result;
        }

        // Successful call — deduct & log (unless unmetered)
        if (!$is_unmetered && $tokens_required > 0) {
            $deducted = function_exists('vmo_deduct_tokens_fifo')
                ? vmo_deduct_tokens_fifo($user_id, $tokens_required)
                : false;

            if ($deducted && function_exists('vmo_log_token_usage')) {
                vmo_log_token_usage($user_id, $operation_key, $tokens_required);
            } elseif (!$deducted && function_exists('vmo_log_billing_event')) {
                // Edge: balance went negative between pre-flight and post-call.
                // Log the leak explicitly so reconciliation can recover it.
                vmo_log_billing_event($user_id, $operation_key, $tokens_required,
                    'WARNING: post-call deduction failed (possible race). Investigate.',
                    isset($args['provider']) ? $args['provider'] : 'internal',
                    0.0, '', false);
            }
        }

        return $result;
    }
}
