<?php
/**
 * MatrixEngine.php  v11 — ALL INCOME RULES CORRECT
 *
 * ═══════════════════════════════════════════════════════════════════════
 *  WORKING PLAN INCOME (from Hindi text + Excel images):
 *
 *  Reg $30+$40=$70 → Sponsor gets $15 flat (paid by contract, logged only)
 *
 *  Pkg3 $60   → Sponsor(A1) 60% = $36,  A2(sponsor's sponsor) 40% = $24,  admin 10%
 *               After: AUTO-ENTER buyer into SMART $100 (free slot)
 *               SMART $100 unlocked for manual purchase
 *               Pkg4 LOCKED until buyer manually pays SMART $100
 *
 *  Pkg4 $200  → Sponsor(A1) 60% = $120, A3(3rd upline) 40% = $80,  admin 10%
 *               After: pkg5 unlocked
 *
 *  Pkg5 $500  → Sponsor(A1) 30% = $150, A4(4th upline) 30% = $150,
 *               40% distributed across 10 upline levels (4% each = $20/level), admin 10%
 *               After: SILVER $500 unlocked (need 2 active directs)
 *
 *  Pkg6+ same structure as pkg5 (A1 30%, A(N+1) 30%, 40% × 10 levels)
 *
 *  NOTE: Working auto-upgrade (3/9/27) is triggered from buyNormalPackage.php
 *        after building child chain — NOT from within processManualUpgrade itself.
 *        processManualUpgrade only distributes income + advances package.
 *
 * ═══════════════════════════════════════════════════════════════════════
 *  GLOBAL MATRIX INCOME (from PDF diagram + user description):
 *
 *  Pool is 3×3 = 9 members. YOU are at top.
 *  When B joins under you (1st child, legPos=1):
 *    → 100% of pool price → YOU ($100 for SMART)
 *  When C joins (2nd child, legPos=2):
 *    → 50% → YOU + 50% → YOUR parent in pool tree (A2 = 2nd upline)
 *    → If YOU have no parent in pool → 100% → YOU
 *  When D joins (3rd child, legPos=3):
 *    → 50% → YOU + 50% → YOUR parent in pool tree
 *
 *  L2 members (4-9): same rule applies to their direct parent in pool
 *
 *  L1 COMPLETE BONUS (3 direct filled):  from config_pool_income poolLevel=1
 *  L2 COMPLETE BONUS (9 total filled):   from config_pool_income poolLevel=2
 *    → Mark done → RE-ENTRY into same pool → try chain to next pool
 *
 *  DIRECT REQ for pools 2+ (SILVER onwards): need 2 active directs
 *  If receiver doesn't have 2 directs → income goes to THEIR upline in pool tree
 *
 *  NOTE: Working pkg purchase (30,40,60,200) does NOT auto-enter buyer
 *        into global pool. Only pkg3($60) triggers AUTO-ENTER SMART.
 *        pkg5,6,7 only UNLOCK the corresponding global pools.
 * ═══════════════════════════════════════════════════════════════════════
 */
require_once __DIR__ . '/db.php';

class MatrixEngine
{
    private PDO   $db;
    private float $adminPct;

    // ── Working pkg → which global pool it UNLOCKS (for manual buy) ──────────
    // pkg3→SMART(1), pkg5→SILVER(2), pkg6→GOLD(3), pkg7→ROYAL(4)

       // Working pkg → triggers global pool unlock + auto-entry
  public const WORKING_TRIGGERS_GLOBAL = [
        3 => 1,  // pkg3 ($60)    -> UNLOCK SMART ($100)
        4 => 2,  // pkg4 ($200)   -> UNLOCK SILVER ($500)
        5 => 3,  // pkg5 ($500)   -> UNLOCK GOLD ($2000)
        6 => 4,  // pkg6 ($4000)  -> UNLOCK ROYAL ($8000)
        7 => 5,  // pkg7 ($16000) -> UNLOCK DIAMOND ($40k)
        8 => 6,  // pkg8 ($100k)  -> UNLOCK CROWN ($200k)
        9 => 7,  // pkg9 ($... )  -> UNLOCK 2L ($800k)
    ];

    // When pool fills → try chain to next pool
    private const GLOBAL_CHAIN = [1=>2, 2=>3, 3=>4, 4=>5, 5=>6, 6=>7, 7=>8];

    // Global pool → min working pkg required
    private const GLOBAL_MIN_WORKING = [1=>3, 2=>4, 3=>5, 4=>6, 5=>7 , 6=>8, 7=>9];

    // Global pool → min active directs required to RECEIVE income
    private const GLOBAL_DIRECT_REQ = [1=>0, 2=>2, 3=>2, 4=>2, 5=>2, 6=>2];

    // ── Working plan income rules ─────────────────────────────────────────────
    // [directPct, uplinePct, uplineAncestorLevel, hasLevelDist, placementAncestorLevel]
    // Lvl 3: 60% A1(sponsor), 40% A2
    // Lvl 4: 60% A1,          40% A3
    // Lvl 5+: 30% A1,         30% A(n+1 where n=pkg-1), 40% across 10 upline levels
    private const INCOME_RULES = [
        1 => [100.0,  0.0, 1, false, 0],  // reg $30: 100% sponsor, no admin
        2 => [ 50.0, 50.0, 1, false, 0],  // reg $40: 50/50, no admin
        3 => [ 60.0, 40.0, 2, false, 0],  // $60: 60% A1, 40% A2
        4 => [ 60.0, 40.0, 3, false, 0],  // $200: 60% A1, 40% A3
        5 => [ 30.0, 40.0, 0, true,  4],  // $500: 30% A1, 40% 10-lvl, 30% A4
        6 => [ 30.0, 40.0, 0, true,  5],  // $4k: 30% A1, 40% 10-lvl, 30% A5
        7 => [ 30.0, 40.0, 0, true,  6],  // $16k: 30% A1, 40% 10-lvl, 30% A6
        8 => [ 30.0, 40.0, 0, true,  7],  // $100k: 30% A1, 40% 10-lvl, 30% A7
    ];

    public function __construct()
    {
        $this->db       = getDB();
        $this->adminPct = (float)(getMiscSetting()['adminCharge'] ?? 10.0);
    }

    // ════════════════════════════════════════════════════════════════════
    //  BUILD ANCESTOR CHAIN at registration
    // ════════════════════════════════════════════════════════════════════

public function buildChildChain(int $newMemberId, int $sponsorId): void
{
    // 1. Find the exact spot (B-Leg 2, etc.)
    $placement = $this->find3x3Placement($sponsorId);
    $placeholderId = $placement['placeholderId'];
    $legPosition = $placement['legPosition'];

    // 2. Update the new member's record
    $upd = $this->db->prepare("UPDATE meddolic_user_details SET placeholderId = ?, legPosition = ? WHERE member_id = ?");
    $upd->execute([$placeholderId, $legPosition, $newMemberId]);

    // 3. Loop up the tree to record history for all ancestors
    $pId = $placeholderId;
    $currentLeg = $legPosition;

    for ($lvl = 1; $lvl <= 100 && $pId > 0; $lvl++) {
        // Record: Child is X levels below this Parent in this Leg
        $this->db->prepare("INSERT IGNORE INTO meddolic_user_set_placeholder_details 
            (member_id, child_id, legPosition, level, date_time, topup_flag)
            VALUES (?, ?, ?, ?, NOW(), 1)")
        ->execute([$pId, $newMemberId, $currentLeg, $lvl]);

        // Move to the next parent up the matrix
        $q = $this->db->prepare("SELECT placeholderId, legPosition FROM meddolic_user_details WHERE member_id=? LIMIT 1");
        $q->execute([$pId]);
        $res = $q->fetch();
        
        if ($res) {
            $pId = (int)$res['placeholderId'];
            $currentLeg = (int)$res['legPosition'];
        } else {
            $pId = 0;
        }
    }
}
/**
 * Helper function to find the first empty spot in 3x3 tree
 */
private function find3x3Placement(int $sponsorId): array
{
    // The queue starts with the sponsor (Member A)
    $queue = [$sponsorId];
    $processed = 0;

    while (isset($queue[$processed])) {
        $currentParentId = $queue[$processed];

        // We check every leg (1, 2, 3) for the current parent
        for ($leg = 1; $leg <= 3; $leg++) {
            // LIVE CHECK: Is there anyone already in this specific leg?
            $stmt = $this->db->prepare("
                SELECT member_id FROM meddolic_user_details 
                WHERE placeholderId = ? AND legPosition = ? 
                LIMIT 1
            ");
            $stmt->execute([$currentParentId, $leg]);
            $occupant = $stmt->fetchColumn();

            if (!$occupant) {
                // SUCCESS: The spot is empty! 
                // If B's Leg 1 is taken by A's spillover, this will find Leg 2.
                return [
                    'placeholderId' => $currentParentId,
                    'legPosition'   => $leg
                ];
            } else {
                // Spot is full, add the occupant (e.g., Member B) to the queue 
                // so we can check their downline legs later.
                $queue[] = (int)$occupant;
            }
        }
        $processed++;
        // Limit search to prevent server crash on massive trees
        if ($processed > 10000) break;
    }
    return ['placeholderId' => $sponsorId, 'legPosition' => 1];
}
    // ════════════════════════════════════════════════════════════════════
    //  VALIDATE WORKING PURCHASE (sequential + gate check)
    // ════════════════════════════════════════════════════════════════════

    public function validateWorkingPurchase(int $memberId, int $pkgId): ?string
    {
        $user     = getUserById($memberId);
        $curPkgId = (int)($user['currentPackage'] ?? 0);
        if ($pkgId !== $curPkgId + 1) {
            return 'Sequential order required. Next step: ' . ($curPkgId + 1);
        }
        // Gate: if current step triggered a global pool, user must have manually PAID it first
        if (isset(self::WORKING_TRIGGERS_GLOBAL[$curPkgId])) {
            $gId = self::WORKING_TRIGGERS_GLOBAL[$curPkgId];
            $chk = $this->db->prepare(
                "SELECT id FROM gm_package_purchases
                 WHERE member_id=? AND package_id=? AND buy_type=3
                   AND trx_hash NOT LIKE 'AUTO_%' AND status=1 LIMIT 1"
            );
            $chk->execute([$memberId, $gId]);
            if (!$chk->fetch()) {
                $g = $this->fetchMatrixPkg($gId);
                return 'Buy ' . ($g['poolName'] ?? 'Matrix Pool') .
                       ' ($' . number_format((float)($g['poolPrice'] ?? 0), 0) .
                       ') in Matrix Packages before this step.';
            }
        }
        return null;
    }
    // ════════════════════════════════════════════════════════════════════
    //  PROCESS MANUAL WORKING UPGRADE
    //  → Pays income to sponsor/upline
    //  → Advances buyer's package
    //  → AUTO-ENTERS global pool only for pkg3 (SMART)
    //  → Does NOT call checkWorkingAutoUpgrade (caller does that)
    // ════════════════════════════════════════════════════════════════════

    public function processManualUpgrade(
        int $buyerId, int $sponsorId, float $pkgPrice, int $pkgId, string $txHash
    ): array {
        $rules        = self::INCOME_RULES[$pkgId] ?? [50.0, 50.0, 1, false, 0];
        $directPct    = $rules[0];
        $uplinePct    = $rules[1];
        $uplineAncLvl = $rules[2];
        $hasLevelDist = $rules[3];
        $placementAnc = $rules[4];   // which ancestor gets 30% placement (pkg5+)
        $adminPct     = ($pkgId <= 2) ? 0.0 : $this->adminPct;

        // ── 1. Sponsor / Direct income ────────────────────────────────
        if ($directPct > 0 && $sponsorId > 0) {
            $gross = $pkgPrice * $directPct / 100;
            $admin = round($gross * $adminPct / 100, 5);
            $net   = round($gross - $admin, 5);
            try {
                $this->db->prepare(
                    "INSERT INTO meddolic_user_referral_income
                       (memberId, childId, referralIncome, packagePrice, adminCharge,
                        netIncome, packageId, dateTime, referralPercent)
                     VALUES (?, ?, ?, ?, ?, ?, ?, NOW(), ?)"
                )->execute([$sponsorId, $buyerId, $gross, $pkgPrice, $admin, $net, $pkgId, $directPct]);
            } catch (Throwable $t) {}
            $this->addToWallet($sponsorId, $net, 1, 0);
        }

        // ── 2a. Fixed upline income (pkg3: A2, pkg4: A3) ──────────────
        if (!$hasLevelDist && $uplinePct > 0 && $uplineAncLvl > 0) {
            // A2 = 2nd upline from buyer = sponsor's sponsor
            // A3 = 3rd upline from buyer
            $uplineId = $this->getNthAncestorFromBuyer($buyerId, $uplineAncLvl);
            if ($uplineId > 0) {
                $gross = $pkgPrice * $uplinePct / 100;
                $admin = round($gross * $adminPct / 100, 5);
                $net   = round($gross - $admin, 5);
                try {
                    $this->db->prepare(
                        "INSERT INTO meddolic_user_upgrade_income
                           (memberId, childId, upgradeIncome, level, packagePrice, adminCharge,
                            netIncome, packageId, upgradePercent, dateTime)
                         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())"
                    )->execute([$uplineId, $buyerId, $gross, $uplineAncLvl,
                                $pkgPrice, $admin, $net, $pkgId, $uplinePct]);
                } catch (Throwable $t) {}
                $this->addToWallet($uplineId, $net, 2, 0);
            }
        }

        // ── 2b. Level distribution (pkg5+): 40% across 10 upline levels ──
        if ($hasLevelDist && $uplinePct > 0) {
            $perLvlGross = $pkgPrice * $uplinePct / 100 / 10;  // 4% per level
            $perLvlAdmin = round($perLvlGross * $adminPct / 100, 5);
            $perLvlNet   = round($perLvlGross - $perLvlAdmin, 5);

            $ancId = $sponsorId;
            for ($lvl = 1; $lvl <= 10 && $ancId > 0; $lvl++) {
                $aQ = $this->db->prepare(
                    "SELECT sponser_id, topup_flag, currentPackage FROM meddolic_user_details WHERE member_id=? LIMIT 1"
                );
                $aQ->execute([$ancId]);
                $anc = $aQ->fetch();
                // Pay only if ancestor is active (topup_flag=1) and at same or higher package
                if ($anc && (int)$anc['topup_flag'] && (int)$anc['currentPackage'] >= $pkgId) {
                    try {
                        $this->db->prepare(
                            "INSERT INTO meddolic_user_upgrade_income
                               (memberId, childId, upgradeIncome, level, packagePrice, adminCharge,
                                netIncome, packageId, upgradePercent, dateTime)
                             VALUES (?, ?, ?, ?, ?, ?, ?, ?, 4.00, NOW())"
                        )->execute([$ancId, $buyerId, $perLvlGross, $lvl,
                                    $pkgPrice, $perLvlAdmin, $perLvlNet, $pkgId]);
                    } catch (Throwable $t) {}
                    $this->addToWallet($ancId, $perLvlNet, 2, 0);
                }
                $ancId = (int)($anc['sponser_id'] ?? 0);
            }
        }

        // ── 2c. Placement income (pkg5+): 30% to specific ancestor ───────
        if ($hasLevelDist && $placementAnc > 0) {
            $plId = $this->getNthAncestorFromBuyer($buyerId, $placementAnc);
            if ($plId > 0) {
                $gross = $pkgPrice * 30.0 / 100;
                $admin = round($gross * $adminPct / 100, 5);
                $net   = round($gross - $admin, 5);
                try {
                    $this->db->prepare(
                        "INSERT INTO meddolic_user_upgrade_income
                           (memberId, childId, upgradeIncome, level, packagePrice, adminCharge,
                            netIncome, packageId, upgradePercent, dateTime)
                         VALUES (?, ?, ?, ?, ?, ?, ?, ?, 30.00, NOW())"
                    )->execute([$plId, $buyerId, $gross, $placementAnc,
                                $pkgPrice, $admin, $net, $pkgId]);
                } catch (Throwable $t) {}
                $this->addToWallet($plId, $net, 2, 0);
            }
        }

        // ── 3. Advance buyer's working package ───────────────────────────
        $this->db->prepare(
            "UPDATE meddolic_user_details SET currentPackage=? WHERE member_id=?"
        )->execute([$pkgId, $buyerId]);

        // ── 4. AUTO-ENTER global pool for pkg3 only (SMART $100 free slot) ─
        //       pkg5,6,7 only UNLOCK the corresponding global pool (no auto-entry)
        // $autoGlobal = null;
        // if ($pkgId === 3) {
        //     // Only pkg3 ($60) auto-enters SMART matrix as a free slot
        //     $matrixId = self::WORKING_TRIGGERS_GLOBAL[3]; // = 1
        //     $chk = $this->db->prepare(
        //         "SELECT entryId FROM meddolic_user_pool_entry_details
        //          WHERE memberId=? AND poolId=? LIMIT 1"
        //     );
        //     $chk->execute([$buyerId, $matrixId]);
        //     if (!$chk->fetch()) {
        //         $this->enterGlobalPool($buyerId, $matrixId, 'AUTO_WORKING', $txHash);
        //         $autoGlobal = $this->fetchMatrixPkg($matrixId);
        //     }
        // }

        return [
            'success'         => true,
         
        ];
    }

    // ════════════════════════════════════════════════════════════════════
    //  WORKING AUTO-UPGRADE (3/9/27 team trigger)
    //  Called externally after child chain is built (buyNormalPackage.php)
    // ════════════════════════════════════════════════════════════════════

    public function checkWorkingAutoUpgrade(int $memberId): void
    {
        if ($memberId <= 0) return;
        $u = getUserById($memberId);
        if (!$u || !(int)($u['topup_flag'] ?? 0)) return;
        $curId = (int)($u['currentPackage'] ?? 0);
        if ($curId <= 0) return;
        $nextId  = $curId + 1;
        $nextPkg = $this->fetchPoolPkg($nextId);
        if (!$nextPkg) return;

        // Gate check: global pool must be manually paid if required
        if (isset(self::WORKING_TRIGGERS_GLOBAL[$curId])) {
            $gId = self::WORKING_TRIGGERS_GLOBAL[$curId];
            $chk = $this->db->prepare(
                "SELECT id FROM gm_package_purchases
                 WHERE member_id=? AND package_id=? AND buy_type=3
                   AND trx_hash NOT LIKE 'AUTO_%' LIMIT 1"
            );
            $chk->execute([$memberId, $gId]);
            if (!$chk->fetch()) return;
        }

        $l1  = $this->countTeamRange($memberId, 1, 1);
        $l12 = $this->countTeamRange($memberId, 1, 2);
        $l13 = $this->countTeamRange($memberId, 1, 3);
        $trigger = null;
        if ($l13 >= 27)    $trigger = '27-team';
        elseif ($l12 >= 9) $trigger = '9-team';
        elseif ($l1 >= 3)  $trigger = '3-direct';
        if (!$trigger) return;

        // Duplicate guard
        $dup = $this->db->prepare(
            "SELECT id FROM gm_auto_upgrades WHERE member_id=? AND from_pkg_id=? AND trigger_reason=? LIMIT 1"
        );
        $dup->execute([$memberId, $curId, $trigger]);
        if ($dup->fetch()) return;

        $this->db->prepare(
            "INSERT INTO gm_auto_upgrades (member_id, from_pkg_id, to_pkg_id, trigger_reason, created_at)
             VALUES (?, ?, ?, ?, NOW())"
        )->execute([$memberId, $curId, $nextId, $trigger]);

        $this->processManualUpgrade(
            $memberId,
            (int)($u['sponser_id'] ?? 0),
            (float)$nextPkg['upgradePrice'],
            $nextId,
            'AUTO_' . $trigger
        );

        // Cascade up to sponsor
        $this->checkWorkingAutoUpgrade((int)($u['sponser_id'] ?? 0));
    }

    // Legacy alias for processRegister.php which calls checkAutoUpgrade
    public function checkAutoUpgrade(int $memberId): void
    {
        $this->checkWorkingAutoUpgrade($memberId);
    }

    // ════════════════════════════════════════════════════════════════════
    //  PROCESS GLOBAL POOL PURCHASE (manual buy from MatrixPackages)
    // ════════════════════════════════════════════════════════════════════

    public function processGlobalPurchase(int $memberId, int $matrixId, float $poolPrice, string $txHash): array
    {
        $minPkg = self::GLOBAL_MIN_WORKING[$matrixId] ?? 0;
        if ($minPkg > 0) {
            $u = getUserById($memberId);
            if ((int)($u['currentPackage'] ?? 0) < $minPkg) {
                $w = $this->fetchPoolPkg($minPkg);
                return ['error' => 'Buy Step ' . ((int)($w['poolLevel'] ?? $minPkg)) . ' first.'];
            }
        }
        $entryId = $this->enterGlobalPool($memberId, $matrixId, 'MANUAL', $txHash);
        return $entryId ? ['success' => true, 'entry_id' => $entryId] : ['error' => 'Pool entry failed'];
    }

    // ════════════════════════════════════════════════════════════════════
    //  ENTER GLOBAL POOL — BFS LEFT→RIGHT TOP→BOTTOM
    // ════════════════════════════════════════════════════════════════════

    public function enterGlobalPool(int $memberId, int $matrixId, string $via = 'MANUAL', string $txHash = ''): int
    {
        $mp = $this->fetchMatrixPkg($matrixId);
        if (!$mp) return 0;
        $poolPrice = (float)$mp['poolPrice'];

        [$parentEntryId, $parentMemberId, $parentLevel] = $this->bfsFindParent($matrixId);

        $myLevel = $parentEntryId ? ($parentLevel + 1) : 1;
        $legPos  = 1;
        if ($parentEntryId) {
            $cq = $this->db->prepare(
                "SELECT COUNT(*) AS c FROM meddolic_user_pool_entry_details WHERE parent1EntryId=? AND poolId=?"
            );
            $cq->execute([$parentEntryId, $matrixId]);
            $legPos = (int)($cq->fetch()['c'] ?? 0) + 1;
        }

        $this->db->prepare(
            "INSERT INTO meddolic_user_pool_entry_details
               (memberId, poolId, poolLevel, reEntryCount, entryDate, poolStatus,
                placeholder1Id, parent1EntryId, leg1Position)
             VALUES (?, ?, ?, 0, NOW(), 1, ?, ?, ?)"
        )->execute([$memberId, $matrixId, $myLevel,
                    $parentMemberId ?: 0, $parentEntryId ?: 0, $legPos]);
        $newEntryId = (int)$this->db->lastInsertId();

        if ($parentEntryId && $parentMemberId) {
            try {
                $this->db->prepare(
                    "INSERT INTO meddolic_user_pool_placeholder_details
                       (poolId, member_id, child_id, leg_position, level, parentEntryId, entryId, date_time)
                     VALUES (?, ?, ?, ?, ?, ?, ?, NOW())"
                )->execute([$matrixId, $parentMemberId, $memberId,
                            $legPos, $myLevel, $parentEntryId, $newEntryId]);
            } catch (Throwable $t) {}

            // Pay per-join income to parent (and grandparent if legPos > 1)
            $this->payGlobalJoinIncome($parentMemberId, $parentEntryId, $matrixId, $poolPrice, $legPos);

            // Check completion (L1 + L2 bonuses)
            $this->checkPoolCompletion($parentMemberId, $parentEntryId, $matrixId);
        }

        // Purchase log
        $u    = getUserById($memberId);
        $sId  = (int)($u['sponser_id'] ?? 0);
        $hash = ($via === 'MANUAL') ? $txHash : ('AUTO_' . $via . '_' . time());

        try {
            $this->db->prepare(
                "INSERT INTO gm_package_purchases
                   (member_id, sponser_id, package_id, package_price, trx_amount, trx_hash, wallet_address, buy_type, purchase_date, status)
                 VALUES (?, ?, ?, ?, 0, ?, ?, 3, NOW(), 1)"
            )->execute([$memberId, $sId, $matrixId, $poolPrice, $hash, $u['walletAddress'] ?? '']);
        } catch (Throwable $t) {}

        try {
            $this->db->prepare(
                "INSERT INTO meddolic_user_activation_details
                   (member_id, sponser_id, packageId, investPrice, transHash, walletAddress, date_time, status, activate_by, buyType)
                 VALUES (?, ?, ?, ?, ?, ?, NOW(), 1, ?, 3)"
            )->execute([$memberId, $sId, $matrixId, $poolPrice, $hash, $u['walletAddress'] ?? '', $memberId]);
        } catch (Throwable $t) {}

        try {
            $this->db->prepare(
                "UPDATE meddolic_user_details SET poolEntry=COALESCE(poolEntry,0)+1 WHERE member_id=?"
            )->execute([$memberId]);
        } catch (Throwable $t) {}

        return $newEntryId;
    }

    // ════════════════════════════════════════════════════════════════════
    //  GLOBAL POOL PER-JOIN INCOME
    //
    //  From PDF diagram + user description:
    //  A is at top of pool. B,C,D join under A.
    //
    //  B (1st child, legPos=1): 100% of pool price → A
    //  C (2nd child, legPos=2): 50% → A,  50% → A's upline in pool tree
    //  D (3rd child, legPos=3): 50% → A,  50% → A's upline in pool tree
    //
    //  If A has no upline in pool (A is root) → 100% goes to A even for C,D
    //
    //  IMPORTANT: each join pays the FULL pool price (not poolPrice/9)
    //  The $100 SMART pool pays $100 per member who joins under you.
    //  B pays → $100 to A
    //  C pays → $50 to A + $50 to A2 (A's parent in pool tree)
    //  This is confirmed by image3: $10.56 + 4×$5.28 = 1st join full + 4 half-joins
    // ════════════════════════════════════════════════════════════════════

    private function payGlobalJoinIncome(
        int $parentMemberId, int $parentEntryId, int $matrixId, float $poolPrice, int $legPos
    ): void {
        // Get A's parent in pool tree (grandparent of new member)
        $gpQ = $this->db->prepare(
            "SELECT placeholder1Id FROM meddolic_user_pool_entry_details WHERE entryId=? LIMIT 1"
        );
        $gpQ->execute([$parentEntryId]);
        $gpMemberId = (int)(($gpQ->fetch())['placeholder1Id'] ?? 0);

        if ($legPos === 1) {
            // 1st child → 100% to parent
            $this->creditPoolJoinIncome($parentMemberId, $matrixId, $poolPrice, $parentEntryId);
        } else {
            // 2nd/3rd child → 50% parent + 50% grandparent
            $half = round($poolPrice / 2, 5);
            $this->creditPoolJoinIncome($parentMemberId, $matrixId, $half, $parentEntryId);
            if ($gpMemberId > 0) {
                $this->creditPoolJoinIncome($gpMemberId, $matrixId, $half, $parentEntryId);
            } else {
                // No grandparent in pool (parent is root) → second half also to parent
                $this->creditPoolJoinIncome($parentMemberId, $matrixId, $half, $parentEntryId);
            }
        }
    }

    private function creditPoolJoinIncome(
        int $memberId, int $matrixId, float $amount, int $refEntryId
    ): void {
        if ($memberId <= 0 || $amount <= 0) return;

        // Check direct requirement for pools 2+ (SILVER, GOLD etc.)
        $dirReq = self::GLOBAL_DIRECT_REQ[$matrixId] ?? 0;
        if ($dirReq > 0 && $this->countActiveDirects($memberId) < $dirReq) {
            // Doesn't meet direct requirement → pass income to their pool parent
            $ppQ = $this->db->prepare(
                "SELECT placeholder1Id FROM meddolic_user_pool_entry_details
                 WHERE memberId=? AND poolId=? AND poolStatus=1
                 ORDER BY entryId DESC LIMIT 1"
            );
            $ppQ->execute([$memberId, $matrixId]);
            $ppRow  = $ppQ->fetch();
            $poolPt = (int)($ppRow['placeholder1Id'] ?? 0);
            if ($poolPt > 0 && $poolPt !== $memberId) {
                $this->creditPoolJoinIncome($poolPt, $matrixId, $amount, $refEntryId);
            }
            return;
        }

        $admin = round($amount * $this->adminPct / 100, 5);
        $net   = round($amount - $admin, 5);

        $this->db->prepare(
            "INSERT INTO meddolic_user_pool_income
               (memberId, poolId, poolLevel, poolIncome, entryId, entryCount, dateTime, releaseStatus)
             VALUES (?, ?, 0, ?, ?, 1, NOW(), 1)"
        )->execute([$memberId, $matrixId, $net, $refEntryId]);
        $this->addToWallet($memberId, $net, 3, $refEntryId);
    }

    // ════════════════════════════════════════════════════════════════════
    //  POOL COMPLETION: L1 BONUS (3 direct) + L2 BONUS (9 total)
    //  → Re-entry on L2 complete
    //  → Try chain to next pool
    // ════════════════════════════════════════════════════════════════════

    private function checkPoolCompletion(int $memberId, int $entryId, int $matrixId): void
    {
        $dq = $this->db->prepare(
            "SELECT COUNT(*) AS c FROM meddolic_user_pool_entry_details WHERE parent1EntryId=? AND poolId=?"
        );
        $dq->execute([$entryId, $matrixId]);
        $directCount = (int)($dq->fetch()['c'] ?? 0);

        $gq = $this->db->prepare(
            "SELECT COUNT(*) AS c FROM meddolic_user_pool_entry_details
             WHERE parent1EntryId IN (
                 SELECT entryId FROM meddolic_user_pool_entry_details WHERE parent1EntryId=? AND poolId=?
             ) AND poolId=?"
        );
        $gq->execute([$entryId, $matrixId, $matrixId]);
        $grandCount = (int)($gq->fetch()['c'] ?? 0);
        $total = $directCount + $grandCount;

        // L1 bonus when 3 direct fill
        if ($directCount >= 3) {
            $cfg = $this->fetchPoolIncomeCfg($matrixId, 1, 3);
            if ($cfg) {
                $dup = $this->db->prepare(
                    "SELECT id FROM meddolic_user_pool_income WHERE memberId=? AND poolId=? AND poolLevel=1 AND entryId=?"
                );
                $dup->execute([$memberId, $matrixId, $entryId]);
                if (!$dup->fetch()) {
                    $bonus = (float)$cfg['userIncome'];
                    $admin = round($bonus * $this->adminPct / 100, 5);
                    $net   = round($bonus - $admin, 5);
                    $this->db->prepare(
                        "INSERT INTO meddolic_user_pool_income
                           (memberId, poolId, poolLevel, poolIncome, entryId, entryCount, dateTime, releaseStatus)
                         VALUES (?, ?, 1, ?, ?, 3, NOW(), 1)"
                    )->execute([$memberId, $matrixId, $net, $entryId]);
                    $this->addToWallet($memberId, $net, 3, $entryId);
                }
            }
        }

        // L2 bonus when all 9 fill → re-entry + chain
        if ($total >= 9) {
            $cfg = $this->fetchPoolIncomeCfg($matrixId, 2, 9);
            if ($cfg) {
                $dup = $this->db->prepare(
                    "SELECT id FROM meddolic_user_pool_income WHERE memberId=? AND poolId=? AND poolLevel=2 AND entryId=?"
                );
                $dup->execute([$memberId, $matrixId, $entryId]);
                if (!$dup->fetch()) {
                    $bonus = (float)$cfg['userIncome'];
                    $admin = round($bonus * $this->adminPct / 100, 5);
                    $net   = round($bonus - $admin, 5);
                    $this->db->prepare(
                        "INSERT INTO meddolic_user_pool_income
                           (memberId, poolId, poolLevel, poolIncome, entryId, entryCount, dateTime, releaseStatus)
                         VALUES (?, ?, 2, ?, ?, 9, NOW(), 1)"
                    )->execute([$memberId, $matrixId, $net, $entryId]);
                    $this->addToWallet($memberId, $net, 3, $entryId);

                    // Mark entry complete
                    $this->db->prepare(
                        "UPDATE meddolic_user_pool_entry_details SET poolStatus=0 WHERE entryId=?"
                    )->execute([$entryId]);

                    // Increment re-entry count on the completed slot
                    $this->db->prepare(
                        "UPDATE meddolic_user_pool_entry_details
                         SET reEntryCount=reEntryCount+1
                         WHERE memberId=? AND poolId=? AND poolStatus=0
                         ORDER BY entryId DESC LIMIT 1"
                    )->execute([$memberId, $matrixId]);

                    // Re-entry: go back into same pool from BFS
                    $this->enterGlobalPool($memberId, $matrixId, 'REENTRY');

                    // Try chain to next pool
                    $this->tryPoolChain($memberId, $matrixId);
                }
            }
        }

        // Propagate check up the tree to parent
        $pQ = $this->db->prepare(
            "SELECT parent1EntryId, placeholder1Id FROM meddolic_user_pool_entry_details WHERE entryId=? LIMIT 1"
        );
        $pQ->execute([$entryId]);
        $pRow = $pQ->fetch();
        if ($pRow && (int)$pRow['parent1EntryId'] > 0 && (int)$pRow['placeholder1Id'] > 0) {
            $this->checkPoolCompletion(
                (int)$pRow['placeholder1Id'],
                (int)$pRow['parent1EntryId'],
                $matrixId
            );
        }
    }

    private function tryPoolChain(int $memberId, int $filledMatrixId): void
    {
        if (!isset(self::GLOBAL_CHAIN[$filledMatrixId])) return;
        $nextId = self::GLOBAL_CHAIN[$filledMatrixId];
        $minPkg = self::GLOBAL_MIN_WORKING[$nextId] ?? 0;
        $u = getUserById($memberId);
        if ($minPkg && (int)($u['currentPackage'] ?? 0) < $minPkg) return;
        $dirReq = self::GLOBAL_DIRECT_REQ[$nextId] ?? 0;
        if ($dirReq && $this->countActiveDirects($memberId) < $dirReq) return;
        $chk = $this->db->prepare(
            "SELECT entryId FROM meddolic_user_pool_entry_details WHERE memberId=? AND poolId=? AND poolStatus=1 LIMIT 1"
        );
        $chk->execute([$memberId, $nextId]);
        if (!$chk->fetch()) {
            $this->enterGlobalPool($memberId, $nextId, 'AUTOUPGRADE');
        }
    }

    // ════════════════════════════════════════════════════════════════════
    //  INCOME PREVIEW (for UI display in MatrixTree + NormalPackages)
    // ════════════════════════════════════════════════════════════════════

    public function calculateIncomePreview(int $pkgId, float $pkgPrice): array
    {
        $rules   = self::INCOME_RULES[$pkgId] ?? [50.0, 50.0, 1, false, 0];
        $admin   = ($pkgId <= 2) ? 0.0 : $this->adminPct;
        $dGross  = $pkgPrice * $rules[0] / 100;
        $uGross  = $pkgPrice * $rules[1] / 100;
        $gId     = self::WORKING_TRIGGERS_GLOBAL[$pkgId] ?? null;
        return [
            'direct_pct'      => $rules[0],
            'upline_pct'      => $rules[1],
            'has_level_dist'  => $rules[3],
            'admin_pct'       => $admin,
            'direct_net'      => round($dGross * (1 - $admin / 100), 2),
            'upline_net'      => round($uGross * (1 - $admin / 100), 2),
            'total_admin'     => round(($dGross + $uGross) * $admin / 100, 2),
            'triggers_pool'   => $gId,
            'triggers_matrix' => $gId ? $this->fetchMatrixPkg($gId) : null,
        ];
    }

    // ════════════════════════════════════════════════════════════════════
    //  TREE GETTERS
    // ════════════════════════════════════════════════════════════════════

    public function getGlobalPoolTree(int $memberId, int $matrixId): array
    {
        $eQ = $this->db->prepare(
            "SELECT * FROM meddolic_user_pool_entry_details
             WHERE memberId=? AND poolId=? AND poolStatus=1
             ORDER BY entryId DESC LIMIT 1"
        );
        $eQ->execute([$memberId, $matrixId]);
        $entry = $eQ->fetch();
        if (!$entry) return ['entry'=>null, 'levels'=>[], 're_entries'=>0];
        $eid = (int)$entry['entryId'];

        $l1q = $this->db->prepare(
            "SELECT ped.memberId, ped.entryId, ped.leg1Position AS legPos, ud.user_id
             FROM meddolic_user_pool_entry_details ped
             LEFT JOIN meddolic_user_details ud ON ud.member_id=ped.memberId
             WHERE ped.parent1EntryId=? AND ped.poolId=?
             ORDER BY ped.leg1Position ASC"
        );
        $l1q->execute([$eid, $matrixId]);
        $l1 = $l1q->fetchAll();

        $l2 = [];
        foreach ($l1 as $n) {
            $l2q = $this->db->prepare(
                "SELECT ped.memberId, ped.entryId, ped.leg1Position AS legPos, ud.user_id
                 FROM meddolic_user_pool_entry_details ped
                 LEFT JOIN meddolic_user_details ud ON ud.member_id=ped.memberId
                 WHERE ped.parent1EntryId=? AND ped.poolId=?
                 ORDER BY ped.leg1Position ASC"
            );
            $l2q->execute([$n['entryId'], $matrixId]);
            $l2 = array_merge($l2, $l2q->fetchAll());
        }

        $rQ = $this->db->prepare(
            "SELECT COALESCE(SUM(reEntryCount),0) AS t FROM meddolic_user_pool_entry_details WHERE memberId=? AND poolId=?"
        );
        $rQ->execute([$memberId, $matrixId]);
        return [
            'entry'      => $entry,
            're_entries' => (int)($rQ->fetch()['t'] ?? 0),
            'levels'     => [1=>$l1, 2=>$l2],
        ];
    }

    // Alias used by MatrixTree.php
    public function getGlobalMatrixTree(int $memberId, int $matrixId): array
    {
        return $this->getGlobalPoolTree($memberId, $matrixId);
    }

//  public function getWorkingMatrixTree(int $memberId): array
// {
//     $levels = [1 => [], 2 => [], 3 => []];

//     try {
//         // We fetch children for levels 1, 2, and 3 in one go using the placeholder table
//         $stmt = $this->db->prepare("
//             SELECT 
//                 p.child_id, 
//                 p.level, 
//                 p.leg_position, 
//                 ud.user_id, 
//                 ud.topup_flag, 
//                 ud.currentPackage
//             FROM meddolic_user_set_placeholder_details p
//             JOIN meddolic_user_details ud ON p.child_id = ud.member_id
//             WHERE p.member_id = ? AND p.level BETWEEN 1 AND 3
//             ORDER BY p.level ASC, p.legPosition ASC
//         ");
//         $stmt->execute([$memberId]);
//         $results = $stmt->fetchAll();

//         foreach ($results as $row) {
//             $lvl = (int)$row['level'];
//             // Normalize column names to match what your MatrixTree.php expects
//             $levels[$lvl][] = [
//                 'child_id'       => $row['child_id'],
//                 'user_id'        => $row['user_id'],
//                 'topup_flag'     => $row['topup_flag'],
//                 'currentPackage' => $row['currentPackage'],
//                 'legPosition'    => $row['leg_position']
//             ];
//         }
//     } catch (Throwable $e) {
//         error_log("Error in getWorkingMatrixTree: " . $e->getMessage());
//     }

//     return ['levels' => $levels];
// }
    // ════════════════════════════════════════════════════════════════════
    //  PRIVATE HELPERS
    // ════════════════════════════════════════════════════════════════════


 
public function getWorkingMatrixTree(int $memberId): array
{
    // Load ALL descendants using the placeholder history table
    // We get every descendant and their direct parent linkage
    $stmt = $this->db->prepare("
        SELECT 
            ud.member_id    AS child_id,
            ud.user_id,
            ud.topup_flag,
            ud.currentPackage,
            ud.placeholderId AS parent_id,
            ud.legPosition   AS leg_pos
        FROM meddolic_user_set_placeholder_details p
        JOIN meddolic_user_details ud ON p.child_id = ud.member_id
        WHERE p.member_id = ?
        GROUP BY ud.member_id
        ORDER BY p.level ASC, ud.legPosition ASC
    ");
    $stmt->execute([$memberId]);
    $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
 
    if (empty($rows)) {
        return ['tree' => null, 'total' => 0, 'levels' => []];
    }
 
    // Build node map: member_id => data
    $nodes = [];
    foreach ($rows as $row) {
        $cid = (int)$row['child_id'];
        $nodes[$cid] = [
            'member_id'      => $cid,
            'user_id'        => $row['user_id'],
            'topup_flag'     => (int)$row['topup_flag'],
            'currentPackage' => (int)$row['currentPackage'],
            'parent_id'      => (int)$row['parent_id'],
            'leg_pos'        => (int)$row['leg_pos'],
            'children'       => [],  // [leg1=>node, leg2=>node, leg3=>node]
            'level'          => 0,
        ];
    }
 
    // Build parent→children relationships
    foreach ($nodes as $cid => &$node) {
        $pid = $node['parent_id'];
        if ($pid > 0 && isset($nodes[$pid])) {
            $leg = $node['leg_pos'] ?: (count($nodes[$pid]['children']) + 1);
            $nodes[$pid]['children'][$leg] = &$node;
        }
    }
    unset($node);
 
    // Assign levels via BFS from memberId
    $queue = [[$memberId, 1]]; // [node_id, level]
    $qi = 0;
    $levelMap = []; // level => [member_id, ...]
 
    while (isset($queue[$qi])) {
        [$nid, $lvl] = $queue[$qi++];
        if (!isset($nodes[$nid])) continue;
        $nodes[$nid]['level'] = $lvl;
        $levelMap[$lvl][] = $nid;
        foreach ($nodes[$nid]['children'] as $child) {
            $queue[] = [$child['member_id'], $lvl + 1];
        }
    }
 
    // Get direct children of memberId (root) sorted by leg
    $rootChildren = [];
    foreach ($nodes as $nid => $node) {
        if ((int)$node['parent_id'] === $memberId) {
            $rootChildren[$node['leg_pos']] = $nodes[$nid];
        }
    }
    ksort($rootChildren);
 
    return [
        'tree'     => $rootChildren,   // Used for recursive rendering
        'nodes'    => $nodes,          // Full flat map  
        'total'    => count($nodes),
        'levelMap' => $levelMap,
    ];
}
    /** BFS: find first active pool entry with < 3 children (left→right, top→bottom) */
    private function bfsFindParent(int $matrixId): array
    {
        $allQ = $this->db->prepare(
            "SELECT entryId, memberId, poolLevel
             FROM meddolic_user_pool_entry_details
             WHERE poolId=? AND poolStatus=1
             ORDER BY poolLevel ASC, entryId ASC"
        );
        $allQ->execute([$matrixId]);
        foreach ($allQ->fetchAll() as $row) {
            $cq = $this->db->prepare(
                "SELECT COUNT(*) AS c FROM meddolic_user_pool_entry_details WHERE parent1EntryId=? AND poolId=?"
            );
            $cq->execute([$row['entryId'], $matrixId]);
            if ((int)($cq->fetch()['c'] ?? 0) < 3) {
                return [(int)$row['entryId'], (int)$row['memberId'], (int)$row['poolLevel']];
            }
        }
        return [null, null, 0];
    }

    /**
     * Get Nth ancestor from BUYER (not from sponsor).
     * N=1 → buyer's direct sponsor (A1)
     * N=2 → A2 (sponsor of sponsor)
     * N=3 → A3
     */
    private function getNthAncestorFromBuyer(int $buyerId, int $n): int
    {
        $id = $buyerId;
        for ($i = 0; $i < $n && $id > 0; $i++) {
            $q = $this->db->prepare(
                "SELECT sponser_id FROM meddolic_user_details WHERE member_id=? LIMIT 1"
            );
            $q->execute([$id]);
            $id = (int)($q->fetchColumn() ?: 0);
        }
        return $id;
    }

    private function countActiveDirects(int $memberId): int
    {
        $q = $this->db->prepare(
            "SELECT COUNT(*) AS c FROM meddolic_user_child_ids ci
             JOIN meddolic_user_details ud ON ud.member_id=ci.child_id
             WHERE ci.member_id=? AND ci.level=1 AND ud.topup_flag=1"
        );
        $q->execute([$memberId]);
        return (int)($q->fetch()['c'] ?? 0);
    }

    private function countTeamRange(int $memberId, int $min, int $max): int
    {
        $q = $this->db->prepare(
            "SELECT COUNT(*) AS c FROM meddolic_user_child_ids
             WHERE member_id=? AND level BETWEEN ? AND ? AND topup_status=1"
        );
        $q->execute([$memberId, $min, $max]);
        return (int)($q->fetch()['c'] ?? 0);
    }

    private function addToWallet(int $memberId, float $amount, int $type, int $trnId): void
    {
        if ($memberId <= 0 || $amount <= 0) return;
        $this->db->prepare(
            "UPDATE meddolic_user_details SET wallet=COALESCE(wallet,0)+? WHERE member_id=?"
        )->execute([$amount, $memberId]);
        try {
            $this->db->prepare(
                "INSERT INTO meddolic_user_wallet_statement
                   (member_id, wallet_statement_id, deb_cr, amount, date_time, trn_id)
                 VALUES (?, ?, 2, ?, NOW(), ?)"
            )->execute([$memberId, $type, $amount, $trnId]);
        } catch (Throwable $t) {}
    }

    private function fetchPoolPkg(int $id): ?array
    {
        static $c = [];
        if (!isset($c[$id])) {
            $q = $this->db->prepare("SELECT * FROM meddolic_config_pool_list WHERE id=? LIMIT 1");
            $q->execute([$id]);
            $c[$id] = $q->fetch() ?: null;
        }
        return $c[$id];
    }

    private function fetchMatrixPkg(int $id): ?array
    {
        static $c = [];
        if (!isset($c[$id])) {
            $q = $this->db->prepare("SELECT * FROM meddolic_config_matrix_package WHERE matrixId=? LIMIT 1");
            $q->execute([$id]);
            $c[$id] = $q->fetch() ?: null;
        }
        return $c[$id];
    }

    private function fetchPoolIncomeCfg(int $matrixId, int $poolLevel, int $userNeed): ?array
    {
        $q = $this->db->prepare(
            "SELECT * FROM meddolic_config_pool_income
             WHERE matrixId=? AND poolLevel=? AND userNeed=? AND poolStatus=1 LIMIT 1"
        );
        $q->execute([$matrixId, $poolLevel, $userNeed]);
        return $q->fetch() ?: null;
    }




}
