<?php
namespace App\Nexus\Engine;

use App\Models\Admin;
use App\Models\Bid;
use App\Models\Deposit;
use App\Models\Ownership;
use App\Models\Product;
use App\Models\Sell;
use App\Models\User;
use App\Nexus\Models\NexusUserMetric;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;

/**
 * NEXUS ENGINE - TradeExecutor
 *
 * Executes the financial side of every trade result.
 * Every write happens inside a DB transaction. If any part fails,
 * everything rolls back. No money moves without full consistency.
 *
 * CRITICAL: This class moves real money on a live platform.
 * The commission logic is copied from the existing Kernel.php
 * fundAgentAndReferrers() and fundReferrersBulk() methods.
 * Any deviation means incorrect payouts.
 *
 * Existing logic reference (from app/Console/Kernel.php):
 *   - Agent = Admin where role_id = 4
 *   - Agent commission = (profit / 2) * 0.15 = 7.5% of profit
 *   - Agent commission goes to admins.balance
 *   - Referrer chain: 3 levels (user.referrer -> referrer² -> referrer³)
 *   - Each referrer gets their rebate % of profit
 *   - Referrer commissions go to users.rebates (NOT users.balance)
 *
 * Cap Overhaul (March 20, 2026):
 *   - updateUserMetrics 'win' case now also increments daily_profit,
 *     weekly_profit, and total_earned so the 3-ceiling pacing system
 *     in SellEvaluator Step 4 reflects actual approved trades.
 *
 * @package App\Nexus\Engine
 */
class TradeExecutor
{
    /**
     * Cached agent record (Admin with role_id = 4).
     * Matches the Kernel.php caching pattern.
     */
    private ?Admin $agentCache = null;

    /**
     * Execute a winning sell -- the full financial settlement.
     * Atomic: if anything fails, everything rolls back.
     *
     * Flow (matches existing processManualSell in Kernel.php):
     *   1. Delete the ownership (product is sold)
     *   2. Calculate total received and profit
     *   3. Credit user balance
     *   4. Pay agent commission
     *   5. Pay referrer rebates (3 levels)
     *   6. Update NEXUS user metrics
     *   7. Update sell record (status, fill_price)
     *   8. Log to audit
     *
     * @param Sell  $sell      The sell being settled
     * @param float $fillPrice The NEXUS-determined fill price per unit
     * @return void
     * @throws \Throwable Rethrown after logging if transaction fails
     */
    public function executeWin(Sell $sell, float $fillPrice): void
    {
        try {
            DB::transaction(function () use ($sell, $fillPrice) {
                $user = User::find($sell->user_id);
                if (! $user) {
                    Log::error('[TradeExecutor] executeWin: User not found', ['sell_id' => $sell->id, 'user_id' => $sell->user_id]);
                    return;
                }

                // 0. Resolve wallet source BEFORE ownership delete.
                //    Traces sell -> originating bid to determine if ICTP funds were used.
                //    NULL or 'main' = main wallet (all pre-ICTP bids have NULL).
                //    'ictp' = house capital, must route back to ictp_balance.
                $walletSource = $this->resolveWalletSource($sell);

                // 1. Delete ownership (product is sold off)
                if ($sell->ownership) {
                    $sell->ownership->delete();
                }

                // 2. Calculate financials
                $totalReceived = $fillPrice * $sell->quantity;
                $capital       = $sell->initial_price * $sell->quantity;
                $profit        = ($fillPrice - $sell->initial_price) * $sell->quantity;

                // 3. Credit user -- route based on wallet source
                //    ICTP: ALL proceeds (capital + profit) return to ictp_balance.
                //    User grows their ICTP wallet through trading, then withdraws
                //    profits via the 60/40 split when they choose to.
                //    Main/NULL: everything to balance (existing behavior)
                if ($walletSource === 'ictp') {
                    $user->increment('ictp_balance', $totalReceived);
                } else {
                    $user->increment('balance', $totalReceived);
                }

                // 4+5. Pay agent commission and referrer rebates
                // ICTP trades do NOT generate referral commissions.
                // Only main wallet trades earn commissions for the referral chain.
                if ($profit > 0 && $walletSource !== 'ictp') {
                    $this->fundAgentAndReferrers($profit, $user);
                }

                // 6. Update NEXUS user metrics
                $this->updateUserMetrics($sell->user_id, 'win', $profit);

                // 7. Update the sell record
                $sell->update([
                    'status'           => 1,
                    'fill_price'       => $fillPrice,
                    'nexus_session_id' => $sell->nexus_session_id,
                ]);

                // 8. Audit log
                NexusAuditLogger::logAlgorithmDecision(
                    $sell->nexus_session_id,
                    $sell->user_id,
                    'sell_settled',
                    'sell',
                    $sell->id,
                    'approved',
                    'Sell won and settled',
                    [
                        'fill_price'     => $fillPrice,
                        'initial_price'  => (float) $sell->initial_price,
                        'quantity'       => $sell->quantity,
                        'total_received' => $totalReceived,
                        'capital'        => $capital,
                        'profit'         => $profit,
                        'wallet_source'  => $walletSource,
                    ]
                );
            });
        } catch (\Throwable $e) {
            // Log to Laravel log (NOT audit log -- that write may have also failed)
            Log::error('[TradeExecutor] executeWin FAILED -- transaction rolled back', [
                'sell_id' => $sell->id,
                'error'   => $e->getMessage(),
                'trace'   => $e->getTraceAsString(),
            ]);
            throw $e;
        }
    }

    /**
     * Execute a losing sell -- update status and metrics.
     * User keeps the ownership. No money moves.
     *
     * @param Sell   $sell   The sell being rejected
     * @param string $reason Why NEXUS rejected this sell
     * @return void
     */
    public function executeLoss(Sell $sell, string $reason): void
    {
        try {
            DB::transaction(function () use ($sell, $reason) {
                // Update the sell record
                $sell->update([
                    'status'           => 2,
                    'rejection_reason' => $reason,
                    'nexus_session_id' => $sell->nexus_session_id,
                ]);

                // Update NEXUS user metrics
                $this->updateUserMetrics($sell->user_id, 'loss');

                // Audit log
                NexusAuditLogger::logAlgorithmDecision(
                    $sell->nexus_session_id,
                    $sell->user_id,
                    'sell_settled',
                    'sell',
                    $sell->id,
                    'rejected',
                    $reason,
                    [
                        'initial_price' => (float) $sell->initial_price,
                        'amount'        => (float) $sell->amount,
                        'quantity'      => $sell->quantity,
                    ]
                );
            });
        } catch (\Throwable $e) {
            Log::error('[TradeExecutor] executeLoss FAILED', [
                'sell_id' => $sell->id,
                'error'   => $e->getMessage(),
            ]);
        }
    }

    /**
     * Execute a winning bid -- create ownership, update product.
     * Matches existing processManualBid in Kernel.php.
     *
     * Flow:
     *   1. Mark bid as won (status 1)
     *   2. Create Ownership record
     *   3. Deduct product quantity
     *   4. Pay product owner (5% profit on product value)
     *   5. Update NEXUS metrics
     *
     * Note: Balance was already deducted when the user placed the bid
     * (in ArbitrageController.placeBid). No balance deduction here.
     *
     * @param Bid $bid The bid that won
     * @return void
     */
    public function executeBidWin(Bid $bid): void
    {
        try {
            DB::transaction(function () use ($bid) {
                $user = User::find($bid->user_id);
                if (! $user) {
                    return;
                }

                // 1. Mark bid as won
                $bid->update(['status' => 1]);

                // 2. Create ownership
                Ownership::create([
                    'slug'          => Str::random(9),
                    'user_id'       => $user->id,
                    'product_id'    => $bid->product_id,
                    'value'         => $bid->amount,
                    'quantity'      => $bid->quantity,
                    'type'          => 'manual',
                    'wallet_source' => $bid->wallet_source,
                ]);

                // 3+4. Update product quantity and pay product owner
                // This matches Kernel.php updateProductAndOwnerBalance()
                $this->updateProductAndOwnerBalance($bid);

                // 5. Update NEXUS metrics
                $this->updateUserMetrics($bid->user_id, 'bid_win');

                // Audit log
                NexusAuditLogger::logAlgorithmDecision(
                    $bid->nexus_session_id,
                    $bid->user_id,
                    'bid_settled',
                    'bid',
                    $bid->id,
                    'approved',
                    'Bid won',
                    [
                        'amount'   => (float) $bid->amount,
                        'quantity' => $bid->quantity,
                    ]
                );
            });
        } catch (\Throwable $e) {
            Log::error('[TradeExecutor] executeBidWin FAILED', [
                'bid_id' => $bid->id,
                'error'  => $e->getMessage(),
            ]);
        }
    }

    /**
     * Execute a losing bid -- refund balance.
     * Matches existing processManualBid loss path in Kernel.php.
     *
     * @param Bid $bid The bid that lost
     * @return void
     */
    public function executeBidLoss(Bid $bid): void
    {
        try {
            DB::transaction(function () use ($bid) {
                $user = User::find($bid->user_id);
                if (! $user) {
                    return;
                }

                // Refund the bid amount to the wallet it came from.
                // 'ictp' -> ictp_balance. NULL or 'main' -> balance.
                $total       = $bid->amount * $bid->quantity;
                $refundField = ($bid->wallet_source === 'ictp') ? 'ictp_balance' : 'balance';
                $user->increment($refundField, $total);

                // Mark bid as lost
                $bid->update(['status' => 2]);

                // Audit log
                NexusAuditLogger::logAlgorithmDecision(
                    $bid->nexus_session_id,
                    $bid->user_id,
                    'bid_settled',
                    'bid',
                    $bid->id,
                    'rejected',
                    'Bid lost',
                    [
                        'amount'        => (float) $bid->amount,
                        'quantity'      => $bid->quantity,
                        'refunded'      => $total,
                        'wallet_source' => $bid->wallet_source,
                        'refund_to'     => $refundField,
                    ]
                );
            });
        } catch (\Throwable $e) {
            Log::error('[TradeExecutor] executeBidLoss FAILED', [
                'bid_id' => $bid->id,
                'error'  => $e->getMessage(),
            ]);
        }
    }

    /**
     * Resolve which wallet funded the bid that created this sell's ownership.
     *
     * Trace: sell -> originating bid (same user, product, price, won).
     * Returns the wallet_source value from the bid record.
     *
     * NULL or 'main' = main wallet (all 1.69M pre-ICTP bids have NULL).
     * 'ictp' = house capital from ICTP program.
     *
     * If no matching bid is found (edge case: very old data, manual
     * ownership creation), defaults to null which routes to main wallet.
     *
     * @param Sell $sell The sell being evaluated
     * @return string|null 'main', 'ictp', or null
     */
    private function resolveWalletSource(Sell $sell): ?string
    {
        $originBid = Bid::where('user_id', $sell->user_id)
            ->where('product_id', $sell->product_id)
            ->where('amount', $sell->initial_price)
            ->where('status', 1)
            ->orderByDesc('id')
            ->first();

        return $originBid?->wallet_source;
    }

    /**
     * Fund agent and referrers after a successful trade.
     *
     * EXACT MATCH of Kernel.php fundAgentAndReferrers() + fundReferrersBulk().
     *
     * Agent commission:
     *   agentProfit = (profit / 2) * 0.15 = 7.5% of profit
     *   Goes to admins.balance where role_id = 4
     *
     * Referrer rebates (3 levels):
     *   Level 1: user.referrer          gets first_rebate%  of profit -> users.rebates
     *   Level 2: user.referrer.referrer gets second_rebate% of profit -> users.rebates
     *   Level 3: referrer³              gets third_rebate%  of profit -> users.rebates
     *
     * @param float $profit The profit amount (total, not per-unit)
     * @param User  $user   The user who made the profit
     * @return void
     */
    private function fundAgentAndReferrers(float $profit, User $user): void
    {
        // Agent commission (DISABLED -- matching existing Kernel.php behavior).
        // To enable: uncomment the 4 lines below. Agent gets 7.5% of profit.
        // $agentProfit = ($profit / 2) * 0.15;
        // $agent = $this->getAgent();
        // if ($agent) {
        //     Admin::where('id', $agent->id)->increment('balance', $agentProfit);
        // }

        // Fund referrers (3-level chain)
        $this->fundReferrersBulk($user, $profit);
    }

    /**
     * Pay referrer rebates through the 3-level chain.
     *
     * MATCHES Kernel.php fundReferrersBulk() including deposit eligibility.
     * Only referrers who have at least one verified deposit (status=1) receive rebates.
     * Rebates go to users.rebates column, NOT users.balance.
     *
     * @param User  $user   The trading user
     * @param float $amount The profit amount to calculate rebates from
     * @return void
     */
    private function fundReferrersBulk(User $user, float $amount): void
    {
        $referrers = collect([
            $user->referrer,
            $user->referrer?->referrer,
            $user->referrer?->referrer?->referrer,
        ])->filter(fn($r) => $r instanceof User);

        if ($referrers->isEmpty()) {
            return;
        }

        // Only referrers with at least one verified deposit get rebates.
        // Matches Kernel.php deposit eligibility gate.
        $eligibleIds = 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,
        ];

        $updates = [];
        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);
        }
    }

    /**
     * Update product quantity and pay the product owner.
     *
     * EXACT MATCH of Kernel.php updateProductAndOwnerBalance().
     * Product owner gets product.value + 5% profit per unit obtained.
     *
     * @param Bid $bid The winning bid
     * @return void
     */
    private function updateProductAndOwnerBalance(Bid $bid): void
    {
        $product = $bid->product;
        if (! $product) {
            return;
        }

        $quantityToUpdate = 0;
        $quantityObtained = 0;

        if (! is_null($product->quantity) && $product->quantity >= $bid->quantity) {
            $quantityToUpdate = $product->quantity - $bid->quantity;
            $quantityObtained = $bid->quantity;
        } else {
            $quantityToUpdate = 0;
            $quantityObtained = $product->quantity ?? 0;
        }

        $product->update(['quantity' => $quantityToUpdate]);

        $profit      = (5 / 100) * $product->value;
        $totalAmount = ($product->value + $profit) * $quantityObtained;
        $owner       = $product->user;

        if ($owner) {
            $owner->increment('balance', $totalAmount);
        }
    }

    /**
     * Update NEXUS user metrics for the current month.
     *
     * Cap Overhaul: 'win' case now also increments daily_profit, weekly_profit,
     * and total_earned. These are the ACTUAL values that SellEvaluator Step 4
     * reads for budget enforcement. Only real winners update these counters.
     *
     * @param int    $userId  The user ID
     * @param string $outcome 'win', 'loss', or 'bid_win'
     * @param float  $profit  Profit amount (only for wins)
     * @return void
     */
    private function updateUserMetrics(int $userId, string $outcome, float $profit = 0): void
    {
        try {
            $monthYear = Carbon::now()->format('Y-m');
            $metric    = NexusUserMetric::getOrCreate($userId, $monthYear);

            switch ($outcome) {
                case 'win':
                    $metric->increment('total_wins');
                    $metric->update([
                        'monthly_profit'     => (float) $metric->monthly_profit + $profit,
                        'daily_profit'       => (float) $metric->daily_profit + $profit,
                        'weekly_profit'      => (float) $metric->weekly_profit + $profit,
                        'total_earned'       => (float) $metric->total_earned + $profit,
                        'consecutive_losses' => 0,
                    ]);
                    break;

                case 'loss':
                    $metric->increment('total_losses');
                    $metric->increment('consecutive_losses');
                    break;

                case 'bid_win':
                    $metric->increment('total_bids');
                    break;
            }
        } catch (\Throwable $e) {
            // Metrics update failure should not crash the trade execution
            Log::error('[TradeExecutor] updateUserMetrics failed', [
                'user_id' => $userId,
                'outcome' => $outcome,
                'error'   => $e->getMessage(),
            ]);
        }
    }

    /**
     * Get the agent admin record (role_id = 4).
     * Cached per instance to avoid repeated queries.
     * Matches Kernel.php getAgent() pattern.
     *
     * @return Admin|null
     */
    private function getAgent(): ?Admin
    {
        if (is_null($this->agentCache)) {
            $this->agentCache = Admin::where('role_id', 4)->first();
        }
        return $this->agentCache;
    }
}