<?php
if ( ! defined( 'ABSPATH' ) ) exit;

/**
 * VMO Twilio Token Economics — Complete Usage-Based Billing
 *
 * Plugs EVERY Twilio cost leak so VMO never absorbs charges meant for users.
 * Uses a ~2.5× markup on Twilio wholesale costs, converted to VMO tokens.
 *
 * ───────────────────────────────────────────────────────────────────
 * TOKEN CONVERSION:  1 VMO Token  = $0.000667
 *                    1,500 Tokens = $1.00
 *                    750 Tokens   = $0.50
 * ───────────────────────────────────────────────────────────────────
 *
 * MARKUP PHILOSOPHY (2.5× — Fair & Profitable):
 * ┌──────────────────────────┬────────────┬───────────┬─────────┬────────┐
 * │ Service                  │ Twilio $   │ VMO $     │ Tokens  │ Markup │
 * ├──────────────────────────┼────────────┼───────────┼─────────┼────────┤
 * │ Phone Rental (monthly)   │ $1.15/mo   │ $2.50/mo  │ 3,750   │ 2.17×  │
 * │ Phone Activation         │ $1.00      │ $2.50     │ 3,750   │ 2.50×  │
 * │ Outbound Call (per min)  │ $0.014     │ $0.035    │ 53      │ 2.50×  │
 * │ Inbound Call (per min)   │ $0.0085    │ $0.022    │ 33      │ 2.59×  │
 * │ Outbound SMS             │ $0.0079    │ $0.02     │ 30      │ 2.53×  │
 * │ Inbound SMS              │ $0.0075    │ $0.019    │ 29      │ 2.53×  │
 * │ A2P SMS Surcharge        │ $0.005     │ $0.013    │ 20      │ 2.60×  │
 * │ Recording Storage (/min) │ $0.0025    │ $0.007    │ 11      │ 2.80×  │
 * │ A2P Brand (one-time)     │ $4.00      │ $9.00     │ 13,500  │ +$5 flat │
 * │ A2P Campaign (monthly)   │ $15.00     │ $20.00    │ 30,000  │ +$5 flat │
 * │ AI TTS (per 1K chars)    │ $0.015     │ $0.04     │ 60      │ 2.67×  │
 * │ AI GPT Turn (per turn)   │ $0.003     │ $0.008    │ 12      │ 2.67×  │
 * └──────────────────────────┴────────────┴───────────┴─────────┴────────┘
 *
 * RESULT: Average ~2.5× markup. User pays roughly $2.50 for every $1 of
 * Twilio/OpenAI cost. This covers infrastructure, support, profit, and
 * is competitive with comparable platforms (Twilio resellers charge 3-5×).
 *
 * @package VMO_AI_Website_Builder
 * @since 1.0.52
 */

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

class VMO_Twilio_Token_Economics {

    /** @var self|null */
    private static $instance = null;

    // ═══════════════════════════════════════════════════════════════════
    // TOKEN COST CONSTANTS (in VMO Tokens)
    // All can be overridden via wp-config.php defines
    // ═══════════════════════════════════════════════════════════════════

    // ── Voice Calling ──
    /** Outbound call per-minute cost (tokens). Twilio $0.014 × 2.5 = $0.035 = 53 tokens */
    public static function CALL_OUTBOUND_PER_MIN(): int {
        return defined('VMO_TOKENS_CALL_OUTBOUND_MIN') ? VMO_TOKENS_CALL_OUTBOUND_MIN : 53;
    }
    /** Inbound call per-minute cost (tokens). Twilio $0.0085 × 2.5 = $0.022 = 33 tokens */
    public static function CALL_INBOUND_PER_MIN(): int {
        return defined('VMO_TOKENS_CALL_INBOUND_MIN') ? VMO_TOKENS_CALL_INBOUND_MIN : 33;
    }

    // ── SMS ──
    /** Outbound SMS per-message (tokens). Twilio $0.0079 × 2.5 = $0.02 = 30 tokens */
    public static function SMS_OUTBOUND(): int {
        return defined('VMO_TOKENS_SMS_OUTBOUND') ? VMO_TOKENS_SMS_OUTBOUND : 30;
    }
    /** Inbound SMS per-message (tokens). Twilio $0.0075 × 2.5 = $0.019 = 29 tokens */
    public static function SMS_INBOUND(): int {
        return defined('VMO_TOKENS_SMS_INBOUND') ? VMO_TOKENS_SMS_INBOUND : 29;
    }
    /** A2P surcharge per outbound SMS (tokens). Twilio $0.005 × 2.5 = $0.013 = 20 tokens */
    public static function SMS_A2P_SURCHARGE(): int {
        return defined('VMO_TOKENS_SMS_A2P_SURCHARGE') ? VMO_TOKENS_SMS_A2P_SURCHARGE : 20;
    }

    // ── Recording ──
    /** Recording storage per-minute (tokens). Twilio $0.0025 × 2.8 = $0.007 = 11 tokens */
    public static function RECORDING_PER_MIN(): int {
        return defined('VMO_TOKENS_RECORDING_MIN') ? VMO_TOKENS_RECORDING_MIN : 11;
    }

    // ── AI Voice (OpenAI TTS used in phone calls) ──
    /** TTS per 1K characters (tokens). Mirrors central token registry. */
    public static function TTS_PER_1K(): int {
        if (function_exists('vmo_get_token_cost')) {
            return (int) vmo_get_token_cost('phone_tts');
        }
        return defined('VMO_TOKENS_PHONE_TTS_1K') ? VMO_TOKENS_PHONE_TTS_1K : 203;
    }

    // ── AI GPT (per conversation turn in AI receptionist) ──
    /** GPT per turn (tokens). Mirrors central token registry. */
    public static function GPT_PER_TURN(): int {
        if (function_exists('vmo_get_token_cost')) {
            return (int) vmo_get_token_cost('phone_ai_turn');
        }
        if (defined('VMO_TOKENS_PHONE_AI_TURN')) {
            return VMO_TOKENS_PHONE_AI_TURN;
        }
        return defined('VMO_TOKENS_PHONE_GPT_TURN') ? VMO_TOKENS_PHONE_GPT_TURN : 41;
    }

    // ── Phone Provisioning ──
    /** Phone number activation one-time (tokens). Twilio $1 × 2.5 = $2.50 = 3,750 tokens */
    public static function PHONE_ACTIVATION(): int {
        return defined('VMO_TOKENS_PHONE_ACTIVATION') ? VMO_TOKENS_PHONE_ACTIVATION : 3750;
    }
    /** Phone number monthly rental (tokens). Twilio $1.15 × 2.17 = $2.50 = 3,750 tokens */
    public static function PHONE_MONTHLY_RENTAL(): int {
        return defined('VMO_TOKENS_PHONE_MONTHLY_RENTAL') ? VMO_TOKENS_PHONE_MONTHLY_RENTAL : 3750;
    }

    // ── A2P 10DLC ──
    /** A2P Brand registration one-time (tokens). Optional SMS add-on: carrier $4 + $5 flat = $9 = 13,500 tokens */
    public static function A2P_BRAND(): int {
        return defined('VMO_TOKENS_A2P_BRAND') ? VMO_TOKENS_A2P_BRAND : 13500;
    }
    /** A2P Campaign monthly (tokens). Optional SMS add-on: carrier $15 + $5 flat = $20 = 30,000 tokens */
    public static function A2P_CAMPAIGN_MONTHLY(): int {
        if (defined('VMO_TOKENS_A2P_CAMPAIGN_MONTHLY')) {
            return VMO_TOKENS_A2P_CAMPAIGN_MONTHLY;
        }

        return defined('VMO_TOKENS_A2P_CAMPAIGN') ? VMO_TOKENS_A2P_CAMPAIGN : 30000;
    }

    // ── Simultaneous Ring / Conference Join (extra outbound leg) ──
    /** Extra outbound call leg per-minute. Same as outbound call. */
    public static function EXTRA_LEG_PER_MIN(): int {
        return self::CALL_OUTBOUND_PER_MIN();
    }


    public static function get_instance(): self {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    private function __construct() {
        // ── Monthly billing crons ──
        add_action('vmo_monthly_phone_rental_billing', array($this, 'process_monthly_phone_rentals'));
        add_action('vmo_monthly_a2p_campaign_billing', array($this, 'process_monthly_a2p_campaign'));

        // Schedule crons if not already scheduled
        if (!wp_next_scheduled('vmo_monthly_phone_rental_billing')) {
            wp_schedule_event(time(), 'daily', 'vmo_monthly_phone_rental_billing');
        }
        if (!wp_next_scheduled('vmo_monthly_a2p_campaign_billing')) {
            wp_schedule_event(time(), 'daily', 'vmo_monthly_a2p_campaign_billing');
        }
    }

    // ═══════════════════════════════════════════════════════════════════
    // DEDUCTION METHODS (called from Twilio Manager hooks)
    // ═══════════════════════════════════════════════════════════════════

    /**
     * Deduct tokens for a completed call (post-call billing).
     * Called from handle_status_callback / handle_dial_status.
     *
     * @param int    $user_id
     * @param int    $duration_seconds  Call duration in seconds
     * @param string $direction         'inbound' or 'outbound'
     * @param bool   $was_recorded      Whether the call was recorded
     * @param int    $ai_turns          Number of AI conversation turns (GPT calls)
     * @param int    $tts_chars         Total TTS characters generated
     * @param string $call_sid          Twilio Call SID for audit trail
     * @return int   Total tokens deducted
     */
    public function deduct_call_tokens(int $user_id, int $duration_seconds, string $direction = 'outbound', bool $was_recorded = false, int $ai_turns = 0, int $tts_chars = 0, string $call_sid = ''): int {
        if ($user_id <= 0 || $duration_seconds <= 0) return 0;

        $duration_seconds = $this->normalize_billable_call_duration($call_sid, $duration_seconds);
        if ($duration_seconds <= 0) return 0;

        $minutes = (int) ceil($duration_seconds / 60);
        $total_tokens = 0;

        // 1. Talk time
        $per_min = ($direction === 'inbound') ? self::CALL_INBOUND_PER_MIN() : self::CALL_OUTBOUND_PER_MIN();
        $talk_tokens = $minutes * $per_min;
        $total_tokens += $talk_tokens;

        // 2. Recording storage
        if ($was_recorded) {
            $rec_tokens = $minutes * self::RECORDING_PER_MIN();
            $total_tokens += $rec_tokens;
        }

        // 3. AI TTS (if AI receptionist spoke)
        if ($tts_chars > 0) {
            $tts_units = (int) ceil($tts_chars / 1000);
            $tts_tokens = $tts_units * self::TTS_PER_1K();
            $total_tokens += $tts_tokens;
        }

        // 4. AI GPT turns (if AI receptionist had conversation)
        if ($ai_turns > 0) {
            $gpt_tokens = $ai_turns * self::GPT_PER_TURN();
            $total_tokens += $gpt_tokens;
        }

        // Post-call billing: deduct directly (can't block a completed call)
        $description = sprintf('%s Call: %d min%s%s%s',
            ucfirst($direction),
            $minutes,
            $was_recorded ? ' + recording' : '',
            $ai_turns > 0 ? " + {$ai_turns} AI turns" : '',
            $tts_chars > 0 ? ' + TTS' : ''
        );
        $this->force_deduct($user_id, $total_tokens, 'phone_call', $description);

        // Centralized billing log with call SID reference
        if (!empty($call_sid) && function_exists('vmo_log_billing_event')) {
            vmo_log_billing_event($user_id, 'phone_call_detailed', $total_tokens, $description, 'twilio', 0.0, $call_sid);
        }

        return $total_tokens;
    }

    private function normalize_billable_call_duration(string $call_sid, int $duration_seconds): int {
        if ($duration_seconds <= 0) {
            return 0;
        }

        $safe_call_sid = sanitize_key($call_sid);
        if ($safe_call_sid !== '') {
            $marker = get_transient('vmo_call_teardown_' . $safe_call_sid);
            $reason = is_array($marker) ? sanitize_key((string) ($marker['reason'] ?? '')) : '';
            if (in_array($reason, array('stale_cleanup', 'live_poll_stale', 'expired_cleanup'), true)) {
                error_log('[VMO Billing Guard] Skipping phone token deduction for stale teardown call ' . $call_sid . ' reason=' . $reason . ' reported_duration=' . $duration_seconds);
                return 0;
            }
        }

        $max_billable_seconds = (int) apply_filters('vmo_phone_max_billable_call_seconds', 7200, $call_sid, $duration_seconds);
        if ($max_billable_seconds > 0 && $duration_seconds > $max_billable_seconds) {
            error_log('[VMO Billing Guard] Capping anomalous phone call duration for ' . $call_sid . ' from ' . $duration_seconds . 's to ' . $max_billable_seconds . 's');
            return $max_billable_seconds;
        }

        return $duration_seconds;
    }

    /**
     * Deduct tokens for an outbound SMS.
     *
     * @param int  $user_id
     * @param int  $segments     Number of SMS segments (long messages split into multiple)
     * @param bool $a2p_enabled  Whether A2P surcharge applies
     * @return int  Total tokens deducted
     */
    public function deduct_sms_outbound(int $user_id, int $segments = 1, bool $a2p_enabled = true): int {
        if ($user_id <= 0) return 0;

        $base = self::SMS_OUTBOUND() * $segments;
        $surcharge = $a2p_enabled ? (self::SMS_A2P_SURCHARGE() * $segments) : 0;
        $total = $base + $surcharge;

        $this->force_deduct($user_id, $total, 'sms_outbound',
            sprintf('Outbound SMS (%d segment%s)%s',
                $segments, $segments > 1 ? 's' : '',
                $a2p_enabled ? ' + A2P surcharge' : ''
            )
        );

        return $total;
    }

    /**
     * Deduct tokens for an inbound SMS.
     *
     * @param int $user_id
     * @param int $segments
     * @return int Total tokens deducted  
     */
    public function deduct_sms_inbound(int $user_id, int $segments = 1): int {
        if ($user_id <= 0) return 0;

        $total = self::SMS_INBOUND() * $segments;

        $this->force_deduct($user_id, $total, 'sms_inbound',
            sprintf('Inbound SMS (%d segment%s)', $segments, $segments > 1 ? 's' : '')
        );

        return $total;
    }

    /**
     * Deduct tokens for AI TTS in a phone call (OpenAI speech).
     *
     * @param int $user_id
     * @param int $char_count  Characters synthesized
     * @return int Tokens deducted
     */
    public function deduct_tts(int $user_id, int $char_count): int {
        if ($user_id <= 0 || $char_count <= 0) return 0;

        $units = (int) ceil($char_count / 1000);
        $tokens = $units * self::TTS_PER_1K();

        $this->force_deduct($user_id, $tokens, 'phone_tts',
            sprintf('AI Voice TTS (%s chars)', number_format($char_count))
        );

        return $tokens;
    }

    /**
     * Deduct tokens for an AI GPT turn in the AI receptionist.
     *
     * @param int $user_id
     * @return int Tokens deducted
     */
    public function deduct_gpt_turn(int $user_id): int {
        if ($user_id <= 0) return 0;

        $tokens = self::GPT_PER_TURN();
        $this->force_deduct($user_id, $tokens, 'phone_ai_turn', 'AI Receptionist conversation turn');

        return $tokens;
    }

    /**
     * Deduct tokens for extra outbound call leg (simultaneous ring, conference join).
     *
     * @param int    $user_id
     * @param int    $duration_seconds
     * @param string $reason  e.g. 'Simultaneous ring', 'Conference barge'
     * @return int   Tokens deducted
     */
    public function deduct_extra_leg(int $user_id, int $duration_seconds, string $reason = 'Extra call leg'): int {
        if ($user_id <= 0 || $duration_seconds <= 0) return 0;

        $minutes = (int) ceil($duration_seconds / 60);
        $tokens = $minutes * self::EXTRA_LEG_PER_MIN();

        $this->force_deduct($user_id, $tokens, 'extra_call_leg',
            sprintf('%s: %d min', $reason, $minutes)
        );

        return $tokens;
    }

    /**
     * Check if user has sufficient balance BEFORE initiating an outbound call.
     * Returns true if balance is sufficient for at least 1 minute.
     *
     * @param int    $user_id
     * @param string $direction 'inbound' or 'outbound'
     * @return bool
     */
    public function can_make_call(int $user_id, string $direction = 'outbound'): bool {
        if (function_exists('vmo_is_internal_unmetered_user') && vmo_is_internal_unmetered_user($user_id)) return true;

        $balance = (int) get_user_meta($user_id, 'vmo_token_balance', true);
        $min_cost = ($direction === 'inbound') ? self::CALL_INBOUND_PER_MIN() : self::CALL_OUTBOUND_PER_MIN();

        // Require at least 3 minutes worth of tokens
        return $balance >= ($min_cost * 3);
    }

    /**
     * Check if user has sufficient balance to send an SMS.
     *
     * @param int $user_id
     * @return bool
     */
    public function can_send_sms(int $user_id): bool {
        if (function_exists('vmo_is_internal_unmetered_user') && vmo_is_internal_unmetered_user($user_id)) return true;

        $balance = (int) get_user_meta($user_id, 'vmo_token_balance', true);
        return $balance >= (self::SMS_OUTBOUND() + self::SMS_A2P_SURCHARGE());
    }

    // ═══════════════════════════════════════════════════════════════════
    // MONTHLY BILLING CRONS
    // ═══════════════════════════════════════════════════════════════════

    /**
     * Process monthly phone number rentals for all users.
     * Runs daily via cron, checks each user's next billing date.
     */
    public function process_monthly_phone_rentals() {
        $args = array(
            'meta_key'     => 'vmo_twilio_phone_number',
            'meta_compare' => 'EXISTS',
            'number'       => -1,
            'fields'       => 'ID',
        );

        $users = get_users($args);
        if (empty($users)) return;

        // Also check the enhanced wizard meta
        $args2 = array(
            'meta_key' => 'vmo_phone_number',
            'meta_compare' => 'EXISTS',
            'number' => -1,
            'fields' => 'ID',
        );
        $users2 = get_users($args2);
        $all_users = array_unique(array_merge(
            is_array($users) ? $users : array(),
            is_array($users2) ? $users2 : array()
        ));

        foreach ($all_users as $user_id) {
            $next_due = (int) get_user_meta($user_id, 'vmo_phone_next_rent_due', true);

            // Grace period for new users
            if (empty($next_due)) {
                update_user_meta($user_id, 'vmo_phone_next_rent_due', strtotime('+30 days'));
                continue;
            }

            if (time() >= $next_due) {
                $rent_cost = self::PHONE_MONTHLY_RENTAL();

                // Pre-check: skip force_deduct if balance is clearly insufficient
                $current_balance = (int) get_user_meta($user_id, 'vmo_token_balance', true);
                if (!user_can($user_id, 'manage_options') && $current_balance < $rent_cost) {
                    error_log('[VMO Token Economics] Phone rental SKIPPED user #' . $user_id . ' — balance ' . $current_balance . ' < cost ' . $rent_cost);
                    $this->handle_rental_failure($user_id);
                    continue;
                }

                $deducted = $this->force_deduct($user_id, $rent_cost, 'phone_rental',
                    'Monthly Phone Number Rental ($' . number_format($rent_cost / 1500, 2) . ')'
                );

                if ($deducted) {
                    update_user_meta($user_id, 'vmo_phone_next_rent_due', strtotime('+30 days'));
                    delete_user_meta($user_id, 'vmo_phone_rent_fail_start');
                    error_log('[VMO Token Economics] Phone rental SUCCESS user #' . $user_id . ' — ' . $rent_cost . ' tokens');
                } else {
                    // Handle failure (grace period, warnings, eventual release)
                    $this->handle_rental_failure($user_id);
                }
            }
        }
    }

    /**
     * Process monthly A2P campaign billing.
     */
    public function process_monthly_a2p_campaign() {
        $args = array(
            'meta_key'     => 'vmo_a2p_campaign_sid',
            'meta_compare' => 'EXISTS',
            'number'       => -1,
            'fields'       => 'ID',
        );

        $users = get_users($args);
        if (empty($users)) return;

        foreach ($users as $user_id) {
            $next_billing = (int) get_user_meta($user_id, 'vmo_a2p_campaign_next_billing', true);
            if (empty($next_billing)) continue;

            if (time() >= $next_billing) {
                $campaign_status = get_user_meta($user_id, 'vmo_a2p_campaign_status', true);
                // Only bill if campaign is active/verified
                if (!in_array(strtoupper($campaign_status), array('VERIFIED', 'APPROVED', 'ACTIVE'), true)) {
                    continue;
                }

                $cost = self::A2P_CAMPAIGN_MONTHLY();

                // Pre-check: skip force_deduct if balance is clearly insufficient
                $current_balance = (int) get_user_meta($user_id, 'vmo_token_balance', true);
                if (!user_can($user_id, 'manage_options') && $current_balance < $cost) {
                    error_log('[VMO Token Economics] A2P campaign billing SKIPPED user #' . $user_id . ' — balance ' . $current_balance . ' < cost ' . $cost);
                    update_user_meta($user_id, 'vmo_a2p_billing_failure', current_time('mysql'));
                    continue;
                }

                $deducted = $this->force_deduct($user_id, $cost, 'a2p_campaign_monthly',
                    'Monthly A2P Campaign Renewal ($' . number_format($cost / 1500, 2) . ')'
                );

                if ($deducted) {
                    update_user_meta($user_id, 'vmo_a2p_campaign_next_billing', strtotime('+30 days'));
                    error_log('[VMO Token Economics] A2P campaign billing SUCCESS user #' . $user_id);
                } else {
                    error_log('[VMO Token Economics] A2P campaign billing FAILED user #' . $user_id . ' — insufficient tokens');
                    // Don't deactivate immediately — Twilio still charges us.
                    // But flag for follow-up
                    update_user_meta($user_id, 'vmo_a2p_billing_failure', current_time('mysql'));
                }
            }
        }
    }

    // ═══════════════════════════════════════════════════════════════════
    // PRE-FLIGHT AFFORDABILITY CHECKS
    // Use these BEFORE initiating any costly action.
    // force_deduct() is ONLY for post-event recovery (provider already charged).
    // ═══════════════════════════════════════════════════════════════════

    /**
     * Check if user can afford a phone rental cycle.
     */
    public function can_afford_phone_rental(int $user_id): bool {
        if (user_can($user_id, 'manage_options')) return true;
        $balance = (int) get_user_meta($user_id, 'vmo_token_balance', true);
        return $balance >= self::PHONE_MONTHLY_RENTAL();
    }

    /**
     * Check if user can afford an A2P campaign renewal.
     */
    public function can_afford_a2p_campaign(int $user_id): bool {
        if (user_can($user_id, 'manage_options')) return true;
        $balance = (int) get_user_meta($user_id, 'vmo_token_balance', true);
        return $balance >= self::A2P_CAMPAIGN_MONTHLY();
    }

    /**
     * Check if user can afford phone activation.
     */
    public function can_afford_phone_activation(int $user_id): bool {
        if (user_can($user_id, 'manage_options')) return true;
        $balance = (int) get_user_meta($user_id, 'vmo_token_balance', true);
        return $balance >= self::PHONE_ACTIVATION();
    }

    // ═══════════════════════════════════════════════════════════════════
    // INTERNAL HELPERS
    // ═══════════════════════════════════════════════════════════════════

    /**
     * Force-deduct tokens even if balance goes to zero.
     *
     * ⚠️  POST-EVENT RECOVERY ONLY — Do NOT use as a pre-action blocker.
     *
     * This method exists for situations where the provider has ALREADY charged
     * VMO (e.g. a completed call, delivered SMS, or monthly Twilio rental that
     * has already been billed). The cost must be captured even if the user's
     * balance is insufficient; otherwise VMO absorbs the loss.
     *
     * For pre-action checks, use:
     *   - can_make_call()           — before outbound/inbound calls
     *   - can_send_sms()            — before SMS
     *   - can_afford_phone_rental() — before monthly rental deduction
     *   - can_afford_a2p_campaign() — before A2P campaign renewal
     *   - can_afford_phone_activation() — before provisioning
     *
     * @param int    $user_id
     * @param int    $amount         Token cost to deduct
     * @param string $operation_type Machine-readable action key
     * @param string $description    Human-readable description for logs
     * @return bool  True if user had sufficient balance, false if went to zero
     */
    private function force_deduct(int $user_id, int $amount, string $operation_type, string $description): bool {
        if (function_exists('vmo_is_internal_unmetered_user') && vmo_is_internal_unmetered_user($user_id)) {
            return true;
        }

        $current = (int) get_user_meta($user_id, 'vmo_token_balance', true);
        $had_sufficient = ($current >= $amount);
        $new_balance = max(0, $current - $amount);
        update_user_meta($user_id, 'vmo_token_balance', $new_balance);

        // Log via FIFO system if available, otherwise manual log
        if (function_exists('vmo_log_token_usage')) {
            vmo_log_token_usage($user_id, $operation_type, $amount);
        }

        // Centralized billing event log with provider attribution
        if (function_exists('vmo_log_billing_event')) {
            $provider = (strpos($operation_type, 'call_') === 0 || strpos($operation_type, 'sms_') === 0
                || strpos($operation_type, 'phone_') === 0 || strpos($operation_type, 'a2p_') === 0
                || strpos($operation_type, 'recording') === 0) ? 'twilio' : 'openai';
            vmo_log_billing_event($user_id, $operation_type, $amount, $description, $provider, 0.0, '', $had_sufficient);
        }

        // Check upgrade notification
        if (function_exists('vmo_check_upgrade_notification')) {
            vmo_check_upgrade_notification($user_id);
        }

        if (!$had_sufficient) {
            error_log('[VMO Token Economics] WARNING: User #' . $user_id . ' went to zero on ' . $operation_type . '. Deducted ' . $amount . ' from ' . $current);
        }

        return $had_sufficient;
    }

    /**
     * Handle phone rental payment failure (grace period + warnings).
     */
    private function handle_rental_failure(int $user_id) {
        $fail_start = (int) get_user_meta($user_id, 'vmo_phone_rent_fail_start', true);
        if (!$fail_start) {
            $fail_start = time();
            update_user_meta($user_id, 'vmo_phone_rent_fail_start', $fail_start);
            error_log('[VMO Token Economics] Phone rental FIRST FAILURE user #' . $user_id);
        }

        $days_overdue = (time() - $fail_start) / DAY_IN_SECONDS;

        // After 25 days of non-payment, release the number
        if ($days_overdue >= 25) {
            $this->release_unpaid_number($user_id);
            error_log('[VMO Token Economics] Phone RELEASED for non-payment user #' . $user_id . ' after ' . round($days_overdue) . ' days');
        }
    }

    /**
     * Release a phone number due to non-payment.
     */
    private function release_unpaid_number(int $user_id) {
        $phone_sid = get_user_meta($user_id, 'vmo_twilio_phone_sid', true);
        if (empty($phone_sid)) return;

        // Try to release via Twilio API
        if (class_exists('VMO_Twilio_REST')) {
            $rest = new VMO_Twilio_REST();
            $rest->release_number($phone_sid);
        }

        // Clean up user meta
        delete_user_meta($user_id, 'vmo_twilio_phone_number');
        delete_user_meta($user_id, 'vmo_twilio_phone_sid');
        delete_user_meta($user_id, 'vmo_phone_next_rent_due');
        delete_user_meta($user_id, 'vmo_phone_rent_fail_start');
        delete_user_meta($user_id, 'vmo_sms_activated');
    }

    // ═══════════════════════════════════════════════════════════════════
    // TOKEN COST REFERENCE (for token-manager.php integration)
    // ═══════════════════════════════════════════════════════════════════

    /**
     * Get all Twilio-related token costs as an array.
     * Useful for displaying in the dashboard or admin panels.
     *
     * @return array
     */
    public static function get_cost_table(): array {
        return array(
            'call_outbound_per_min'  => self::CALL_OUTBOUND_PER_MIN(),
            'call_inbound_per_min'   => self::CALL_INBOUND_PER_MIN(),
            'sms_outbound'           => self::SMS_OUTBOUND(),
            'sms_inbound'            => self::SMS_INBOUND(),
            'sms_a2p_surcharge'      => self::SMS_A2P_SURCHARGE(),
            'recording_per_min'      => self::RECORDING_PER_MIN(),
            'tts_per_1k_chars'       => self::TTS_PER_1K(),
            'gpt_per_turn'           => self::GPT_PER_TURN(),
            'phone_activation'       => self::PHONE_ACTIVATION(),
            'phone_monthly_rental'   => self::PHONE_MONTHLY_RENTAL(),
            'a2p_brand_onetime'      => self::A2P_BRAND(),
            'a2p_campaign_monthly'   => self::A2P_CAMPAIGN_MONTHLY(),
        );
    }
}
