<?php

namespace App\Console;

use App\Models\User;
use App\Models\Admin;
use App\Models\Bid;
use App\Models\Sell;
use App\Models\Investment;
use App\Models\Ownership;
use App\Models\Product;
use App\Models\Market;
use Illuminate\Support\Str;
use Carbon\Carbon;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

class Kernel extends ConsoleKernel
{
    /**
     * NEXUS Engine Artisan commands.
     * These coexist with the existing closures below.
     * All NEXUS commands check nexus_status before doing anything.
     */
    protected $commands = [
        \App\Nexus\Console\Commands\NexusSessionCheck::class,
        \App\Nexus\Console\Commands\NexusProcessSells::class,
        \App\Nexus\Console\Commands\NexusBotGovernorCommand::class,
        \App\Nexus\Console\Commands\NexusEconomySnapshotCommand::class,
        \App\Nexus\Console\Commands\NexusCleanup::class,
        \App\Nexus\Console\Commands\NexusProcessBids::class,
        \App\Console\Commands\ResetForICTP::class,
    ];

    private $agentCache = null;

    // ── Product pool cache ────────────────────────────────────
    // Avoids loading all products from DB on every scheduler tick.
    // Pool refreshes every 5 minutes with 50 random in-stock products.
    private $productPoolCache = null;
    private $productPoolTime = null;

    private function getAgent()
    {
        if (is_null($this->agentCache)) {
            $this->agentCache = Admin::where('role_id', 4)->first();
        }
        return $this->agentCache;
    }

    /**
     * Return a cached pool of 50 random in-stock products.
     * Refreshes every 5 minutes. Shared across all bots in a single
     * scheduler tick to avoid loading products per-bot.
     */
    private function getProductPool()
    {
        if (
            is_null($this->productPoolCache)
            || is_null($this->productPoolTime)
            || Carbon::now()->diffInMinutes($this->productPoolTime) >= 5
        ) {
            // Products are virtual (arbitrage platform). No stock check needed.
            // Pool from bid markets only, $5-$50K range (excludes outliers that
            // could drain a bot's balance in one bid). 200 random products
            // refreshed every 5 minutes for variety across bots and cycles.
            $bidMarketIds = Market::where('type', 'bid')->pluck('id');

            $this->productPoolCache = Product::select(['id', 'value', 'quantity', 'user_id', 'market_id'])
                ->whereIn('market_id', $bidMarketIds)
                ->whereBetween('value', [5, 50000])
                ->inRandomOrder()
                ->limit(200)
                ->get();
            $this->productPoolTime = Carbon::now();
        }
        return $this->productPoolCache;
    }

    protected function schedule(Schedule $schedule)
    {
        // QUEUE
        $schedule->command('queue:work --stop-when-empty --tries=3 --timeout=55')
            ->everyMinute()
            ->withoutOverlapping();

        // $schedule->call(function () {
        //     $this->runManualBiddingProcess();
        // })
        //     ->everyTwoMinutes()
        //     ->name('run-manual-bids')
        //     ->withoutOverlapping();

        // $schedule->call(function () {
        //     $this->runManualSellingProcess();
        // })
        //     ->everyTwoMinutes()
        //     ->name('run-manual-sells')
        //     ->withoutOverlapping();

        $schedule->call(function () {
            $this->runAiBiddingProcess();
            $this->runAiSellingProcess();
        })
            ->everyThreeMinutes()
            ->name('run-ai-bids-and-sells')
            ->withoutOverlapping();

        $schedule->call(function () {
            $this->processAiBids();
        })
            ->everyTwoMinutes()
            ->name('process-ai-bids')
            ->withoutOverlapping();

        $schedule->call(function () {
            $this->processAiSells();
        })
            ->everyTwoMinutes()
            ->name('process-ai-sells')
            ->withoutOverlapping();

        $schedule->call(function () {
            $this->finishCompletedAiTrades();
        })
            ->everyFiveMinutes()
            ->name('finish-completed-ai-trades')
            ->withoutOverlapping();

        $schedule->call(function () {
            $this->cleanupOldAiBids();
        })
            ->daily()
            ->at('02:00') // Run at 2 AM when traffic is low
            ->name('cleanup-old-ai-bids')
            ->withoutOverlapping();

        $schedule->call(function () {
            $this->cleanupOldAiSells();
        })
            ->daily()
            ->at('02:30') // Run at 2:30 AM
            ->name('cleanup-old-ai-sells')
            ->withoutOverlapping();

        // -- NEXUS ENGINE SCHEDULER --
        // These commands coexist with the closures above.
        // All exit silently when nexus_status = inactive.
        // No double-processing risk during the build.

        $schedule->command('nexus:session-check')
            ->everyMinute()
            ->withoutOverlapping();

        $schedule->command('nexus:process-sells')
            ->everyTwoMinutes()
            ->withoutOverlapping();

        $schedule->command('nexus:process-bids')
            ->everyTwoMinutes()
            ->withoutOverlapping();

        $schedule->command('nexus:bot-governor')
            ->everyFiveMinutes()
            ->withoutOverlapping();

        $schedule->command('nexus:economy-snapshot')
            ->dailyAt('00:05')
            ->withoutOverlapping();

        $schedule->command('nexus:cleanup')
            ->dailyAt('03:00')
            ->withoutOverlapping();
    }

    // =====================================================================
    //  MANUAL BID/SELL METHODS
    //  Scheduler calls are commented out (NEXUS replaces these).
    //  Methods kept as rollback safety net.
    // =====================================================================

    private function runManualBiddingProcess(): void
    {
        Bid::with(['market', 'product', 'user'])
            ->where('type', 'manual_bid')
            ->where('status', 0)
            ->where('bid_time', '<=', Carbon::now()->subHours(24)) // pre-filter in SQL
            ->chunkById(50, function ($bids) {
                foreach ($bids as $bid) {
                    try {
                        $this->processManualBid($bid);
                    } catch (\Throwable $e) {
                        // Isolate -- one bad bid never kills the whole batch
                        Log::error("processManualBid failed for bid #{$bid->id}: " . $e->getMessage(), [
                            'bid_id'     => $bid->id,
                            'user_id'    => $bid->user_id,
                            'product_id' => $bid->product_id,
                            'trace'      => $e->getTraceAsString(),
                        ]);
                    }
                }
            });
    }

    private function processManualBid(Bid $bid): void
    {
        DB::transaction(function () use ($bid) {

            // -- 1. Lock the bid row first --
            // Re-fetch with a row lock so two concurrent scheduler workers
            // (unlikely but possible during deploy restarts) cannot both
            // process the same bid.
            $lockedBid = Bid::where('id', $bid->id)
                ->where('status', 0)          // only proceed if still pending
                ->lockForUpdate()
                ->first();

            if (is_null($lockedBid)) {
                // Already processed by another worker -- skip silently.
                return;
            }

            // -- 2. Validate relations --
            $product = $lockedBid->product;
            $market  = $lockedBid->market;

            if (!$product || !$market) {
                // Orphaned bid -- mark failed and bail
                $lockedBid->update(['status' => 2]);
                Log::warning("processManualBid: bid #{$lockedBid->id} has no product or market -- marked failed.");
                return;
            }

            // -- 3. Lock and validate the bidding user --
            $user = User::where('id', $lockedBid->user_id)
                ->lockForUpdate()
                ->first();

            if (!$user) {
                $lockedBid->delete();
                return;
            }

            if ($user->status == 0) {
                // Banned user -- leave bid pending, check again next tick
                return;
            }

            // -- 4. Determine wallet source --
            $walletSource = $lockedBid->wallet_source ?? 'main';
            $balanceField = $walletSource === 'ictp' ? 'ictp_balance' : 'balance';

            // -- 5. Evaluate bid against margin --
            $margin = $product->value - ($market->bid_margin / 100) * $product->value;
            $bidWon = $lockedBid->amount >= $margin;

            if (!$bidWon) {
                // -- Bid lost -- refund to originating wallet --
                $refund = $lockedBid->amount * $lockedBid->quantity;
                $user->increment($balanceField, $refund);
                $lockedBid->update(['status' => 2]);
                return;
            }

            // -- 6. Bid won -- lock the product row --
            $lockedProduct = \App\Models\Product::where('id', $product->id)
                ->lockForUpdate()
                ->first();

            if (!$lockedProduct) {
                // Product vanished -- refund
                $refund = $lockedBid->amount * $lockedBid->quantity;
                $user->increment($balanceField, $refund);
                $lockedBid->update(['status' => 2]);
                return;
            }

            // -- 7. Resolve quantity actually obtainable --
            // If the product has less stock than requested, we give what's
            // available and refund the remainder.
            $requestedQty = $lockedBid->quantity;

            if (!is_null($lockedProduct->quantity) && $lockedProduct->quantity <= 0) {
                // Completely out of stock -- full refund
                $refund = $lockedBid->amount * $requestedQty;
                $user->increment($balanceField, $refund);
                $lockedBid->update(['status' => 2]);
                return;
            }

            if (!is_null($lockedProduct->quantity) && $lockedProduct->quantity < $requestedQty) {
                // Partial fill -- obtain what's available, refund the rest
                $obtainedQty  = $lockedProduct->quantity;
                $shortfallQty = $requestedQty - $obtainedQty;
                $refund       = $lockedBid->amount * $shortfallQty;
                $user->increment($balanceField, $refund);
            } else {
                // Full fill
                $obtainedQty = $requestedQty;
            }

            // -- 8. Deduct product stock --
            $newProductQty = is_null($lockedProduct->quantity)
                ? null
                : max(0, $lockedProduct->quantity - $obtainedQty);

            $lockedProduct->update(['quantity' => $newProductQty]);

            // -- 9. Create ownership with wallet_source propagated --
            \App\Models\Ownership::create([
                'slug'          => \Illuminate\Support\Str::random(9),
                'user_id'       => $user->id,
                'product_id'    => $lockedProduct->id,
                'value'         => $lockedBid->amount,
                'quantity'      => $obtainedQty,   // actual qty, not requested qty
                'type'          => 'manual',
                'wallet_source' => $walletSource,  // propagated for sell routing
            ]);

            // -- 10. Mark bid successful --
            $lockedBid->update(['status' => 1]);

            // -- 11. Credit the product owner --
            $this->creditProductOwner($lockedProduct, $lockedBid->amount, $obtainedQty);
        });
    }

    /**
     * Credit the product owner for a successful bid.
     *
     * Must be called INSIDE an existing DB::transaction().
     */
    private function creditProductOwner(\App\Models\Product $product, float $bidAmount, int $obtainedQty): void
    {
        if (!$product->user_id) {
            return;
        }

        // Lock the owner row to prevent concurrent increments
        $owner = User::where('id', $product->user_id)
            ->lockForUpdate()
            ->first();

        if (!$owner) {
            return;
        }

        // 5% profit on top of the bid amount, per unit
        $profit      = (5 / 100) * $bidAmount;
        $totalCredit = ($bidAmount + $profit) * $obtainedQty;

        $owner->increment('balance', $totalCredit);
    }

    // private function runManualSellingProcess()
    // {
    //     $sells = Sell::with(['market', 'product', 'ownership'])->where([
    //         'type' => 'manual_sell',
    //         'status' => 0
    //     ])->get();

    //     foreach ($sells as $sell) {
    //         $user = User::find($sell->user_id);
    //         if (!$user) {
    //             $sell->delete();
    //             continue;
    //         } elseif ($user->status == 0) {
    //             continue;
    //         }

    //         $timestamp = Carbon::parse($sell->sell_time);
    //         if (Carbon::now()->gte($timestamp->copy()->addHours(1))) {
    //             $this->processManualSell($sell, $user);
    //         }
    //     }
    // }

    // private function processManualSell($sell, User $user)
    // {
    //     $margin = $sell->initial_price + (($sell->market->sell_margin / 100) * $sell->initial_price);

    //     if ($sell->amount <= $margin) {
    //         $sell->update(['status' => 1]);
    //         $sell->ownership->delete();
    //         $total = $sell->amount * $sell->quantity;
    //         $profit = $sell->amount - $sell->initial_price;

    //         $user->increment('balance', $total);
    //         $this->fundAgentAndReferrers($profit, $user);
    //     } else {
    //         $sell->update(['status' => 2]);
    //     }
    // }

    private function runManualSellingProcess(): void
    {
        // Pre-filter in SQL -- only picks up sells whose 1-hour window has passed.
        // chunkById streams 50 at a time, safe for millions of rows.
        Sell::with(['market', 'product', 'ownership', 'user'])
            ->where('type', 'manual_sell')
            ->where('status', 0)
            ->where('sell_time', '<=', Carbon::now()->subHours(1))
            ->chunkById(50, function ($sells) {
                foreach ($sells as $sell) {
                    try {
                        $this->processManualSell($sell);
                    } catch (\Throwable $e) {
                        Log::error("processManualSell failed for sell #{$sell->id}: " . $e->getMessage(), [
                            'sell_id'    => $sell->id,
                            'user_id'    => $sell->user_id,
                            'product_id' => $sell->product_id,
                            'trace'      => $e->getTraceAsString(),
                        ]);
                    }
                }
            });
    }


    private function processManualSell(Sell $sell): void
    {
        DB::transaction(function () use ($sell) {

            // -- 1. Lock the sell row and re-check status --
            // If a previous tick already processed this sell, status != 0
            // and we exit immediately without doing anything.
            $lockedSell = Sell::where('id', $sell->id)
                ->where('status', 0)
                ->lockForUpdate()
                ->first();

            if (is_null($lockedSell)) {
                return; // Already processed -- skip silently
            }

            // -- 2. Validate required relations --
            $market = $lockedSell->market;
            if (!$market) {
                $lockedSell->update(['status' => 2]);
                Log::warning("processManualSell: sell #{$lockedSell->id} has no market -- marked failed.");
                return;
            }

            // -- 3. Lock and validate the selling user --
            $user = User::where('id', $lockedSell->user_id)
                ->lockForUpdate()
                ->first();

            if (!$user) {
                $lockedSell->delete();
                return;
            }

            if ($user->status == 0) {
                // Banned user -- leave pending, re-check next tick
                return;
            }

            // -- 4. Evaluate sell against margin --
            // Sell succeeds when the listing price is AT or BELOW the margin
            // ceiling (market controls acceptable profit cap).
            $marginCeiling = $lockedSell->initial_price
                + (($market->sell_margin / 100) * $lockedSell->initial_price);

            $sellSucceeds = $lockedSell->amount <= $marginCeiling;

            if (!$sellSucceeds) {
                // Price too high -- reject. Lock already held, safe to update.
                $lockedSell->update(['status' => 2]);
                return;
            }

            // -- 5. Resolve wallet destination --
            // Prefer the wallet_source recorded on the sell record itself
            // (written by sellItem() controller). Fall back to the ownership's
            // wallet_source (written by processManualBid). Final fallback: main.
            $walletSource = $lockedSell->wallet_source ?? $lockedSell->ownership?->wallet_source ?? 'main';

            $balanceField = $walletSource === 'ictp' ? 'ictp_balance' : 'balance';

            // -- 6. Calculate amounts --
            $total  = $lockedSell->amount * $lockedSell->quantity;

            // FIX: multiply by quantity -- old code omitted this
            $profit = ($lockedSell->amount - $lockedSell->initial_price) * $lockedSell->quantity;

            // -- 7. Lock the ownership before deleting --
            // Prevents a concurrent request from also finding and acting on it.
            if ($lockedSell->ownership_id) {
                $ownership = \App\Models\Ownership::where('id', $lockedSell->ownership_id)
                    ->lockForUpdate()
                    ->first();
            } else {
                $ownership = null;
            }

            // -- 8. Credit the user's correct wallet --
            // Do this BEFORE deleting ownership or updating sell status.
            // If this line throws (e.g. DB connection blip), the transaction
            // rolls back and the sell stays at status=0 for the next tick.
            $user->increment($balanceField, $total);

            // -- 9. Delete ownership (now that user is paid) --
            if ($ownership) {
                $ownership->delete();
            }

            // -- 10. Mark sell successful (commit signal) --
            // This is intentionally last. If any step above threw, the
            // transaction rolled back and status is still 0 -- fully retryable.
            $lockedSell->update(['status' => 1]);

            // -- 11. Fund agent and referrers --
            // Called after status is committed. fundAgentAndReferrers is its
            // own DB operation; if it fails, the sell is already settled and
            // the error is logged separately by that method.
            $this->fundAgentAndReferrers($profit, $user);
        });
    }

    // =====================================================================
    //  AI BOT METHODS -- REWRITTEN FOR SCALE (March 19, 2026)
    //
    //  Design targets: 10K+ users, thousands of concurrent bots.
    //  Key changes from original:
    //    - Product pool cached 5 min, 50 random, shared across all bots
    //    - Each bot bids on max 10 products per cycle (not ALL products)
    //    - chunkById everywhere (streams, never loads all into memory)
    //    - Eager loading (zero N+1 queries)
    //    - SQL-level filtering (settled bots, expired bots never loaded)
    //    - try/catch per record (one bad record never kills the batch)
    //    - NEXUS gates preserved exactly as deployed
    //
    //  Scale ceiling with cron-based processing: ~500 active bots.
    //  Beyond that: migrate to Redis + Laravel Horizon queue workers.
    // =====================================================================

    /**
     * Place AI bids for all active bots.
     *
     * Runs every 3 minutes. Loads bots in chunks of 50 with user eager-loaded.
     * Each bot bids on up to 10 random products from a cached pool of 50.
     * SQL filters: funded, not paused, not settled, not expired.
     */
    private function runAiBiddingProcess(): void
    {
        $products = $this->getProductPool();
        if ($products->isEmpty()) return;

        Investment::with('user')
            ->where('status', 2)
            ->where('funded', 1)
            ->where('is_paused', 0)
            ->where('is_settled', 0)
            ->where('end_date', '>', Carbon::now())
            ->chunkById(50, function ($bots) use ($products) {
                foreach ($bots as $bot) {
                    try {
                        // User already eager-loaded
                        if (!$bot->user || $bot->user->status == 0) continue;

                        $this->placeAiBids($bot, $products);
                    } catch (\Throwable $e) {
                        Log::error("placeAiBids failed for bot #{$bot->id}: " . $e->getMessage(), [
                            'bot_id'  => $bot->id,
                            'user_id' => $bot->user_id,
                            'trace'   => $e->getTraceAsString(),
                        ]);
                    }
                }
            });

        // Handle expired bots: transition to completed and settle
        Investment::with('user')
            ->where('status', 2)
            ->where('end_date', '<=', Carbon::now())
            ->chunkById(50, function ($expiredBots) {
                foreach ($expiredBots as $bot) {
                    try {
                        $bot->update(['status' => 3]);
                        $this->adjustBotBalanceAfterCompletion($bot, false);
                    } catch (\Throwable $e) {
                        Log::error("Bot expiry settlement failed for bot #{$bot->id}: " . $e->getMessage(), [
                            'bot_id'  => $bot->id,
                            'user_id' => $bot->user_id,
                        ]);
                    }
                }
            });
    }

    /**
     * Place bids for a single AI bot.
     *
     * Max 10 bids per cycle per bot. Products shuffled from the cached pool.
     * NEXUS gates: session window check + governed trade ranges.
     */
    private function placeAiBids($bot, $products): void
    {
        // NEXUS: only place AI bids during active session windows
        // This gate only activates when nexus_status = active.
        // During the build (inactive), AI bots run unrestricted.
        if (\App\Nexus\Engine\NexusConfig::isNexusActive()) {
            $sessionController = new \App\Nexus\Engine\SessionController();
            if (!$sessionController->isSessionOpen()) {
                return; // No active session -- AI bots wait
            }
        }

        // NEXUS Phase 8: Check if governance has set trade ranges for this bot.
        // When NEXUS is active, BotGovernor runs every 5 minutes and writes
        // governed trade_min/trade_max onto the bot record. We read those
        // values here instead of the hardcoded rand(1,5).
        // When NEXUS is inactive, existing behavior is unchanged.
        $useGovernedRange = false;
        if (\App\Nexus\Engine\NexusConfig::isNexusActive()) {
            // BotGovernor has already written governed values to bot.trade_min / bot.trade_max
            // Only use governed range if the values look reasonable (not null/zero)
            if ($bot->trade_min > 0 && $bot->trade_max > 0 && $bot->trade_max >= $bot->trade_min) {
                $useGovernedRange = true;
            }
        }

        // Shuffle the product pool. Allow up to 50 bids per cycle so bots
        // can spend their full balance within a single session window.
        // At 3-minute intervals over a 1-hour session: 50 * 20 = 1,000 bids max.
        // Balance gate ($bot->balance < 1) is the real limiter.
        $shuffled = $products->shuffle();
        $maxBidsPerCycle = 50;
        $placed = 0;

        // Prevent duplicate bids on same product. Load product IDs where
        // bot already has a pending bid or an active ownership.
        $existingProductIds = Bid::where('investment_id', $bot->id)
            ->where('status', 0)
            ->pluck('product_id')
            ->merge(
                Ownership::where('investment_id', $bot->id)
                    ->pluck('product_id')
            )
            ->unique()
            ->all();

        // Price cap: no single product should exceed 20% of bot's funded amount.
        // Forces diversification. A $270 bot bids on products up to ~$50,
        // a $16K bot up to ~$3K, a $200K bot up to ~$40K.
        $maxProductValue = ((float) $bot->amount) * 0.20;

        // Market diversity: track bids per market this cycle.
        // Cap at 5 per market to spread across different markets.
        $marketBidCounts = [];
        $maxPerMarket = 5;

        foreach ($shuffled as $product) {
            if ($placed >= $maxBidsPerCycle) break;
            if ($bot->balance < 1) break;

            if (!$product || !$product->value) continue;

            // Skip products this bot already has a pending bid or ownership on
            if (in_array($product->id, $existingProductIds)) continue;

            // Skip products too expensive for this bot's capital tier
            if ($product->value > $maxProductValue) continue;

            // Skip if already placed 5 bids in this market this cycle
            $mId = $product->market_id;
            if (($marketBidCounts[$mId] ?? 0) >= $maxPerMarket) continue;

            // NEXUS Phase 8: use governed range when active, else existing hardcoded range
            if ($useGovernedRange) {
                $rangePct = $this->random_float($bot->trade_min, $bot->trade_max);
            } else {
                $rangePct = rand(1, 5); // Existing logic: 1% to 5%
            }
            $amount = ($rangePct / 100) * $product->value + $product->value;

            $prdQty = rand(1, rand(1, 5));

            $totalCost = $amount * $prdQty;
            if ($bot->balance >= $totalCost && $totalCost > 0) {
                Bid::create([
                    'user_id' => $bot->user_id,
                    'store_name' => $bot->user->username ?? '',
                    'product_id' => $product->id,
                    'market_id' => $product->market_id,
                    'investment_id' => $bot->id,
                    'amount' => $amount,
                    'quantity' => $prdQty,
                    'status' => 0,
                    'type' => 'ai_bid',
                    'bid_time' => now(),
                ]);

                $bot->decrement('balance', $totalCost);
                $existingProductIds[] = $product->id;
                $marketBidCounts[$mId] = ($marketBidCounts[$mId] ?? 0) + 1;
                $placed++;
            }
        }
    }

    /**
     * Evaluate pending AI bids whose 1-hour window has passed.
     *
     * Changed from 24hr to 1hr: bids placed during a session evaluate
     * within the hour, allowing balance to recycle for the next session.
     * chunkById processes ALL eligible bids (no hard cap).
     * SQL pre-filters: status 0, bid_time > 1hr ago, bot not settled.
     * Eager loads: user, product, market, investment.
     * Error isolation: one bad bid never kills the batch.
     */
    private function processAiBids(): void
    {
        Bid::with(['market', 'product', 'user'])
            ->where('type', 'ai_bid')
            ->where('status', 0)
            ->where('bid_time', '<=', Carbon::now()->subHours(1))
            ->chunkById(200, function ($bids) {
                foreach ($bids as $bid) {
                    try {
                        if (!$bid->user) {
                            $bid->delete();
                            continue;
                        }

                        $this->processAiBid($bid, $bid->user);
                    } catch (\Throwable $e) {
                        Log::error("processAiBid failed for bid #{$bid->id}: " . $e->getMessage(), [
                            'bid_id'        => $bid->id,
                            'user_id'       => $bid->user_id,
                            'investment_id' => $bid->investment_id,
                            'trace'         => $e->getTraceAsString(),
                        ]);
                    }
                }
            });
    }

    /**
     * Evaluate a single AI bid.
     *
     * NEXUS gate: shouldTradeSucceed() governance check.
     */
    private function processAiBid($bid, User $user): void
    {
        $bot = Investment::find($bid->investment_id);
        if (is_null($bot)) return;

        // Skip if bot is already settled (shouldn't exist but just in case)
        if ($bot->is_settled == 1) return;

        // NEXUS Phase 8: Bot Governance success/failure check.
        // When NEXUS is active, BotGovernor determines if this trade should
        // succeed or fail based on the bot's profit trajectory vs plan targets.
        // If shouldTradeSucceed() returns false, the bid is marked as lost and
        // the bot's balance is refunded. This creates realistic win/loss patterns
        // (85%-99% success rate) instead of 100% AI wins.
        // When NEXUS is inactive, all trades succeed (existing behavior).
        if (\App\Nexus\Engine\NexusConfig::isNexusActive()) {
            $governor = new \App\Nexus\Engine\BotGovernor();
            if (!$governor->shouldTradeSucceed($bot)) {
                $bid->update(['status' => 2]); // Mark as lost
                $refund = $bid->amount * $bid->quantity;
                $bot->increment('balance', $refund);
                return;
            }
        }

        $bid->update(['status' => 1]);
        Ownership::create([
            'slug' => Str::random(9),
            'user_id' => $user->id,
            'product_id' => $bid->product->id,
            'investment_id' => $bid->investment_id,
            'value' => $bid->amount,
            'quantity' => $bid->quantity,
            'type' => 'ai',
            'wallet_source' => 'main', // AI bots always use bot balance, not user wallet
        ]);
    }

    /**
     * Create AI sell orders from bot ownerships.
     *
     * SQL pre-filters: active, funded, not paused, not settled.
     * chunkById streams 50 bots at a time.
     * Also handles bot expiry for bots with zero ownerships.
     */
    private function runAiSellingProcess(): void
    {
        // Cache sell markets once (typically 5 markets, rarely changes)
        $sellMarketIds = Market::where('type', 'sell')->pluck('id');
        if ($sellMarketIds->isEmpty()) return;

        Investment::with('user')
            ->where('status', 2)
            ->where('funded', 1)
            ->where('is_paused', 0)
            ->where('is_settled', 0)
            ->chunkById(50, function ($bots) use ($sellMarketIds) {
                foreach ($bots as $bot) {
                    try {
                        if (!$bot->user || $bot->user->status == 0) continue;

                        // Load ownerships for this bot that are at least 10 minutes old.
                        // This ensures all bids from a session evaluate and create
                        // ownerships before sells are placed on them as a batch.
                        $ownerships = Ownership::with('product')
                            ->where('user_id', $bot->user_id)
                            ->where('investment_id', $bot->id)
                            ->where('created_at', '<=', Carbon::now()->subMinutes(10))
                            ->get();

                        if ($ownerships->isNotEmpty()) {
                            $this->establishOwnership($bot->user, $bot, $ownerships, $sellMarketIds);
                        }

                        // If end date passed, check ALL ownerships and pending bids/sells
                        // before settling. The 10-min filter above is for sell placement only.
                        $endDate = Carbon::parse($bot->end_date);
                        if (Carbon::now()->gte($endDate)) {
                            $totalOwns = Ownership::where('user_id', $bot->user_id)
                                ->where('investment_id', $bot->id)->count();
                            $pendingBids = Bid::where('investment_id', $bot->id)
                                ->where('status', 0)->count();
                            $pendingSells = Sell::where('investment_id', $bot->id)
                                ->where('status', 0)->count();

                            if ($totalOwns === 0 && $pendingBids === 0 && $pendingSells === 0) {
                                $bot->update(['status' => 3]);
                                $this->adjustBotBalanceAfterCompletion($bot, false);
                            }
                        }
                    } catch (\Throwable $e) {
                        Log::error("runAiSellingProcess failed for bot #{$bot->id}: " . $e->getMessage(), [
                            'bot_id'  => $bot->id,
                            'user_id' => $bot->user_id,
                            'trace'   => $e->getTraceAsString(),
                        ]);
                    }
                }
            });
    }

    /**
     * Create sell records for AI ownerships that don't already have a pending sell.
     *
     * Uses pre-loaded sell market IDs to avoid per-ownership market query.
     */
    private function establishOwnership($user, $bot, $ownerships, $sellMarketIds = null): void
    {
        // Fallback if called from finishCompletedAiTrades (which doesn't pass market IDs)
        if (is_null($sellMarketIds)) {
            $sellMarketIds = Market::where('type', 'sell')->pluck('id');
        }
        if ($sellMarketIds->isEmpty()) return;

        foreach ($ownerships as $ownership) {
            if ($ownership->sells()->where('status', 0)->doesntExist()) {
                // Pick a random sell market from the cached list
                $marketId = $sellMarketIds->random();

                $limit = ($this->random_float($bot->trade_min, $bot->trade_max) / 100) * $ownership->value;
                $amount = $limit + $ownership->value;
                Sell::create([
                    'user_id' => $user->id,
                    'ownership_id' => $ownership->id,
                    'product_id' => $ownership->product->id,
                    'market_id' => $marketId,
                    'investment_id' => $bot->id,
                    'initial_price' => $ownership->value,
                    'amount' => $amount,
                    'quantity' => $ownership->quantity,
                    'status' => 0,
                    'type' => 'ai_sell',
                    'sell_time' => now(),
                    'wallet_source' => $ownership->wallet_source ?? 'main',
                ]);
            }
        }
    }

    /**
     * Process pending AI sell orders.
     *
     * SQL join filter: only loads sells where the bot is still active and not settled.
     * Also filters out sells whose ownership has been deleted (zombie prevention).
     * chunkById processes all eligible, error isolation per sell.
     */
    private function processAiSells(): void
    {
        // Only process sells where:
        // 1. The sell is pending (status = 0)
        // 2. The bot is still active (status = 2) and not settled
        // 3. The ownership still exists (not deleted)
        // 4. The 24-hour evaluation window has passed
        Sell::with(['market', 'product', 'user', 'ownership'])
            ->where('sells.type', 'ai_sell')
            ->where('sells.status', 0)
            ->where('sells.sell_time', '<=', Carbon::now()->subHours(24))
            ->whereHas('ownership') // ownership must still exist
            ->whereExists(function ($query) {
                $query->select(DB::raw(1))
                    ->from('investments')
                    ->whereColumn('investments.id', 'sells.investment_id')
                    ->where('investments.is_settled', 0)
                    ->where('investments.status', 2);
            })
            ->chunkById(200, function ($sells) {
                foreach ($sells as $sell) {
                    try {
                        if (!$sell->user) {
                            $sell->delete();
                            continue;
                        }

                        $this->processAiSell($sell, $sell->user);
                    } catch (\Throwable $e) {
                        Log::error("processAiSell failed for sell #{$sell->id}: " . $e->getMessage(), [
                            'sell_id'       => $sell->id,
                            'user_id'       => $sell->user_id,
                            'investment_id' => $sell->investment_id,
                            'trace'         => $e->getTraceAsString(),
                        ]);
                    }
                }
            });
    }

    /**
     * Process a single AI sell order.
     *
     * Uses lockForUpdate on the bot to prevent concurrent profit updates.
     * AI sells always succeed (governance controls success at the bid step).
     */
    private function processAiSell($sell, $user): void
    {
        DB::transaction(function () use ($sell, $user) {
            // Lock the bot record to prevent concurrent updates
            $bot = Investment::where('id', $sell->investment_id)->lockForUpdate()->first();
            if (is_null($bot) || $bot->is_settled == 1) return;

            $sell->update(['status' => 1]);

            $total = $sell->amount * $sell->quantity;
            $profit = ($sell->amount - $sell->initial_price) * $sell->quantity;

            $bot->update([
                'pause_time' => now(),
                'balance' => $bot->balance + $total,
                'pfit' => $bot->pfit + $profit
            ]);

            if (!is_null($sell->ownership))
                $sell->ownership->delete();

            // Fund agent and referrers
            $this->fundAgentAndReferrers($profit, $user);
        });
    }

    // =====================================================================
    //  BOT SETTLEMENT
    // =====================================================================

    private function adjustBotBalanceAfterCompletion($bot, $lastCheck = true)
    {
        // Skip if this bot has already been settled once
        if ($bot->is_settled == 1) return;

        $newBal = null;

        if (!$lastCheck) {
            $newBal = $bot->f_pfit > 0
                ? $bot->amount + $bot->f_pfit - $bot->moved
                : $bot->amount + $bot->pfit - $bot->moved;
        } else {
            $expDate = Carbon::parse($bot->end_date);
            if ($expDate->isToday() && $bot->moved < 1) {
                $newBal = $bot->f_pfit > 0
                    ? $bot->amount + $bot->f_pfit - $bot->moved
                    : $bot->amount + $bot->pfit - $bot->moved;
            }
        }

        if (!is_null($newBal)) {
            $bot->update([
                'balance' => $newBal,
                'is_settled' => 1,
            ]);

            // Delete all pending trades for this bot
            $this->cleanupPendingTrades($bot);

            // NEXUS Phase 8: Settlement range verification
            // Logs a warning if the bot's final profit falls outside the plan's
            // configured NEXUS range. Warning only -- does not change the settlement.
            // This catches bots that BotGovernor could not steer accurately.
            if (\App\Nexus\Engine\NexusConfig::isNexusActive()) {
                try {
                    $plan = \App\Models\Plan::find($bot->plan_id);
                    if ($plan && $plan->nexus_monthly_min && $plan->nexus_monthly_max) {
                        $settledProfit = $bot->f_pfit > 0 ? (float) $bot->f_pfit : (float) $bot->pfit;
                        $amount = (float) $bot->amount;
                        $actualProfitPct = $amount > 0
                            ? ($settledProfit / $amount) * 100
                            : 0;

                        if (
                            $actualProfitPct < (float) $plan->nexus_monthly_min
                            || $actualProfitPct > (float) $plan->nexus_monthly_max
                        ) {
                            Log::warning('[NEXUS] Bot settled outside plan range', [
                                'bot_id'     => $bot->id,
                                'user_id'    => $bot->user_id,
                                'plan'       => $plan->name,
                                'capital'    => $amount,
                                'profit'     => $settledProfit,
                                'actual_pct' => round($actualProfitPct, 2),
                                'target_min' => $plan->nexus_monthly_min,
                                'target_max' => $plan->nexus_monthly_max,
                                'used_f_pfit' => $bot->f_pfit > 0,
                            ]);
                        }
                    }
                } catch (\Throwable $e) {
                    Log::error('[NEXUS] Settlement verification error', [
                        'bot_id' => $bot->id,
                        'error'  => $e->getMessage(),
                    ]);
                }
            }
        }
    }

    // =====================================================================
    //  COMMISSION AND REFERRAL
    // =====================================================================

    private function fundAgentAndReferrers($profit, User $user)
    {
        // Fund agent
        $agentProfit = ($profit / 2) * 0.15;
        $agent = $this->getAgent();

        if ($agent) {
            // Use increment instead of manual update
            // Admin::where('id', $agent->id)->increment('balance', $agentProfit);
        }

        // Fund referrers with bulk update
        $this->fundReferrersBulk($user, $profit);
    }

    private function fundReferrersBulk($user, $amount)
    {
        $updates = [];

        $referrers = collect([
            $user->referrer,
            $user->referrer?->referrer,
            $user->referrer?->referrer?->referrer,
        ])->filter(fn($r) => $r instanceof User);

        // ✅ Get IDs of referrers who have deposits
        $eligibleIds = \App\Models\Deposit::whereIn('user_id', $referrers->pluck('id'))
            ->where('status', 1)
            ->pluck('user_id')
            ->toArray();

        $rebatePercentages = [
            $user->referrer?->first_rebate,
            $user->referrer?->referrer?->second_rebate,
            $user->referrer?->referrer?->referrer?->third_rebate,
        ];

        foreach ($referrers as $index => $referrer) {
            if (!in_array($referrer->id, $eligibleIds)) {
                continue;
            }

            $percentage = $rebatePercentages[$index] ?? null;

            if ($percentage !== null) {
                $updates[$referrer->id] = ($percentage / 100) * $amount;
            }
        }

        foreach ($updates as $userId => $fundAmount) {
            User::where('id', $userId)->increment('rebates', $fundAmount);
        }
    }

    // =====================================================================
    //  AI TRADE COMPLETION AND CLEANUP
    // =====================================================================

    /**
     * Settle completed AI bots (status 3, not yet settled).
     *
     * Processes remaining ownerships, bids, and sells for each bot,
     * then runs the final settlement.
     * Eager loads user. Error isolation per bot.
     */
    private function finishCompletedAiTrades(): void
    {
        Investment::with('user')
            ->where('status', 3)
            ->where('is_settled', 0)
            ->chunkById(50, function ($trades) {
                foreach ($trades as $trade) {
                    try {
                        if (!$trade->user) {
                            // No user -- force settle to clear the record
                            $trade->update(['is_settled' => 1]);
                            $this->cleanupPendingTrades($trade);
                            continue;
                        }

                        $user = $trade->user;

                        // Process remaining ownerships into sells
                        $ownerships = Ownership::with('product')->where([
                            'user_id' => $user->id,
                            'investment_id' => $trade->id,
                        ])->get();

                        if ($ownerships->isNotEmpty()) {
                            $this->establishOwnership($user, $trade, $ownerships);
                        }

                        // Process remaining pending bids
                        $bids = Bid::with(['product', 'market'])->where([
                            'investment_id' => $trade->id,
                            'user_id' => $user->id,
                            'status' => 0,
                        ])->get();

                        foreach ($bids as $bid) {
                            $this->processAiBid($bid, $user);
                        }

                        // Process remaining pending sells
                        $sells = Sell::with(['market', 'product', 'ownership'])->where([
                            'user_id' => $user->id,
                            'investment_id' => $trade->id,
                            'status' => 0,
                        ])->get();

                        foreach ($sells as $sell) {
                            $this->processAiSell($sell, $user);
                        }

                        $this->adjustBotBalanceAfterCompletion($trade);
                    } catch (\Throwable $e) {
                        Log::error("finishCompletedAiTrades failed for bot #{$trade->id}: " . $e->getMessage(), [
                            'bot_id'  => $trade->id,
                            'user_id' => $trade->user_id,
                            'trace'   => $e->getTraceAsString(),
                        ]);
                    }
                }
            });
    }

    private function random_float($min, $max)
    {
        return $min + mt_rand() / mt_getrandmax() * ($max - $min);
    }

    private function cleanupPendingTrades($bot)
    {
        // Delete pending bids
        Bid::where([
            'investment_id' => $bot->id,
            'status' => 0,
        ])->delete();

        // Delete pending sells
        Sell::where([
            'investment_id' => $bot->id,
            'status' => 0,
        ])->delete();

        // Delete any remaining ownerships
        Ownership::where([
            'investment_id' => $bot->id,
        ])->delete();
    }

    /**
     * Delete old AI bids for completed bots (older than 2 months), keeping only last 100
     */
    private function cleanupOldAiBids()
    {
        try {
            DB::transaction(function () {
                // Get bots completed more than 2 months ago
                $twoMonthsAgo = Carbon::now()->subMonths(2);

                $completedBotIds = Investment::where('status', 3)
                    ->where('is_settled', 1)
                    ->where('updated_at', '<=', $twoMonthsAgo)
                    ->pluck('id');

                if ($completedBotIds->isEmpty()) {
                    Log::info('No old completed bots found for bid cleanup');
                    return;
                }

                $deletedCount = 0;

                foreach ($completedBotIds as $botId) {
                    // Get the IDs of the last 100 bids to keep
                    $bidsToKeep = Bid::where('investment_id', $botId)
                        ->where('type', 'ai_bid')
                        ->orderBy('id', 'desc')
                        ->take(100)
                        ->pluck('id');

                    // Delete all bids except the last 100
                    $deleted = Bid::where('investment_id', $botId)
                        ->where('type', 'ai_bid')
                        ->when($bidsToKeep->isNotEmpty(), function ($query) use ($bidsToKeep) {
                            $query->whereNotIn('id', $bidsToKeep);
                        })
                        ->delete();

                    $deletedCount += $deleted;
                }

                Log::info("Cleanup: Deleted {$deletedCount} old AI bids for {$completedBotIds->count()} bots");
            });
        } catch (\Exception $e) {
            Log::error('Error in cleanupOldAiBids: ' . $e->getMessage());
        }
    }

    /**
     * Delete old AI sells for completed bots (older than 2 months), keeping only last 100
     */
    private function cleanupOldAiSells()
    {
        try {
            DB::transaction(function () {
                // Get bots completed more than 2 months ago
                $twoMonthsAgo = Carbon::now()->subMonths(2);

                $completedBotIds = Investment::where('status', 3)
                    ->where('is_settled', 1)
                    ->where('updated_at', '<=', $twoMonthsAgo)
                    ->pluck('id');

                if ($completedBotIds->isEmpty()) {
                    Log::info('No old completed bots found for sell cleanup');
                    return;
                }

                $deletedCount = 0;

                foreach ($completedBotIds as $botId) {
                    // Get the IDs of the last 100 sells to keep
                    $sellsToKeep = Sell::where('investment_id', $botId)
                        ->where('type', 'ai_sell')
                        ->orderBy('id', 'desc')
                        ->take(100)
                        ->pluck('id');

                    // Delete all sells except the last 100
                    $deleted = Sell::where('investment_id', $botId)
                        ->where('type', 'ai_sell')
                        ->when($sellsToKeep->isNotEmpty(), function ($query) use ($sellsToKeep) {
                            $query->whereNotIn('id', $sellsToKeep);
                        })
                        ->delete();

                    $deletedCount += $deleted;
                }

                Log::info("Cleanup: Deleted {$deletedCount} old AI sells for {$completedBotIds->count()} bots");
            });
        } catch (\Exception $e) {
            Log::error('Error in cleanupOldAiSells: ' . $e->getMessage());
        }
    }
}
