<?php
/**
 * MatrixEngine.php  v14 — POOL INCOME BUG FIXED
 *
 * ═══════════════════════════════════════════════════════════════════════
 * ROOT CAUSE OF INCOME BUG (confirmed from DB screenshots):
 *
 * Pool tree structure in DB:
 *   entryId=1 → member=21 (ROOT, no parent)
 *   entryId=3 → member=22, parent=21 (entryId=1), leg=1
 *   entryId=4 → member=23, parent=21 (entryId=1), leg=2
 *   entryId=5 → member=24, parent=21 (entryId=1), leg=3
 *   entryId=6 → member=25, parent=22 (entryId=3), leg=1
 *
 * CORRECT income flow per diagram:
 *   22 joins under 21 at leg1  → $100 → 21 only           ✅ (row 1 correct)
 *   23 joins under 21 at leg2  → $100 → 21 + $100 → 21's pool-grandparent (none, 21 is root) → $100 extra to 21
 *                                 = $200 total to 21       ✅ (rows 2+3 correct)
 *   24 joins under 21 at leg3  → $100 → 21 + $100 → 21's pool-grandparent (none) → $200 to 21
 *                                 = $200 total to 21       ✅ (rows 4+5 correct)
 *   25 joins under 22 at leg1  → $100 → 22 only           ✅ (row 6 correct for 22)
 *                                 21 should get NOTHING    ❌ (21 is NOT being paid — row 6 only has 22)
 *
 * WAIT — re-reading the screenshots:
 *   Rows 1-5 ALL have memberId=21, entryId=1
 *   Row 6 has memberId=22, entryId=3
 *
 * So income table shows: 21 got paid 5 times (for joins of 22,23,23,24,24)
 * and 22 got paid once (for join of 25 at leg1).
 *
 * THIS IS ACTUALLY CORRECT for the rule:
 *   leg1 → $100 to parent (1 row)
 *   leg2 → $100 to parent + $100 to grandparent → since 21 has no grandparent → $100+$100 both go to 21 (2 rows)
 *   leg3 → same as leg2 → $100+$100 to 21 (2 rows)
 *   Total for 21: 1+2+2 = 5 rows of $95 each ✅
 *
 * BUT THE PROBLEM IS: row 6 member=22 gets entryId=3 which is correct
 * But user says: when 25 joins under 22 at leg1, 22 should get $100
 * AND 21 should get $100 as 22's upline (grandparent of 25 in pool)
 * WAIT — user says:
 *   "22 down 25 only 95 go income 22, no income 21"  → This IS correct for leg1!
 *   "22 2nd upline 20 go income" → when 22 gets leg2/leg3, THEN 20 (21's parent) should get $100
 *
 * So the REAL bugs are:
 * 1. entryId stored is WRONG — using parent's entryId instead of child's entryId in income table
 * 2. The "grandparent" lookup is finding the wrong member for the $200 case
 *    When 23 joins under 21 at leg2:
 *      → 21 gets $100 (parent)
 *      → 21's grandparent in pool = placeholder1Id of entryId=1 = 0 (no parent)
 *      → So extra $100 SHOULD go to 21 again
 *    This is correct per rule. But user wants A2 (21's sponsor chain upline) not pool-tree grandparent
 *
 * CORRECT RULE FROM DIAGRAM (re-reading):
 *   leg2/leg3: $100 → direct parent in pool + $100 → parent's SPONSOR CHAIN A2 (not pool-tree grandparent)
 *   The "A2" in diagram = 2nd upline in WORKING SPONSOR CHAIN, not pool tree!
 *   Pool tree grandparent and sponsor chain A2 are DIFFERENT.
 *
 * FINAL CORRECT RULE:
 *   leg1: $poolPrice → parentInPool (direct parent in pool tree)
 *   leg2: $poolPrice → parentInPool + $poolPrice → parentInPool's SPONSOR (A1 of parent = A2 of child in sponsor chain)
 *   leg3: same as leg2
 *   If no sponsor upline → both go to parentInPool
 * ═══════════════════════════════════════════════════════════════════════
 */
require_once __DIR__ . '/db.php';

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

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

    private const GLOBAL_CHAIN = [1=>2, 2=>3, 3=>4, 4=>5, 5=>6, 6=>7, 7=>8];

    private const GLOBAL_MIN_WORKING = [1=>3, 2=>4, 3=>5, 4=>6, 5=>7, 6=>8, 7=>9];

    // Pool → minimum active directs to RECEIVE income
    private const GLOBAL_DIRECT_REQ = [1=>0, 2=>2, 3=>2, 4=>2, 5=>2, 6=>2, 7=>2];

    // pkg5+ income split (admin 10% deducted from each):
    //   30% → Sponsor (A1) — direct referral income
    //   30% → Placement ancestor (A1 = sponsor, same person)
    //   40% → Level income distributed across ALL levels in meddolic_config_level_income
    //          (up to 10 levels, percentages sum to 100% of the 40% pool)
    //   GATE for level income: upline must be active (topup_flag=1) AND currentPackage >= 5
    //   If upline not qualified → level is skipped silently (not passed up)
    // ════════════════════════════════════════════════════════════════════
    //  INCOME_RULES  [directPct, uplinePct, placementLevel, hasLevelDist, placementLevelD]
    //
    //  From plan image (placement tree = meddolic_user_set_placeholder_details):
    //    pkg1 ($30):  $15 flat direct per referral (handled separately, no upgrade income)
    //    pkg2 ($40):  $15 flat direct per referral (same)
    //    pkg3 ($60):  60% → sponsor,  40% → placement L1 (direct parent in placement tree)
    //    pkg4 ($200): 60% → sponsor,  40% → placement L2 (grandparent in placement tree)
    //    pkg5 ($500): 30% → sponsor,  30% → placement L3, 40% → 10 level dist
    //    pkg6+:       30% → sponsor,  30% → placement L(n), 40% → 10 level dist
    //
    //  Index: [directPct, uplinePct, placementAncLevel, hasLevelDist, placementAncLevelD]
    //    placementAncLevel  = used by section B (pkg3/4 fixed 40%)
    //    placementAncLevelD = used by section D (pkg5+ 30% placement)
    // ════════════════════════════════════════════════════════════════════
    private const INCOME_RULES = [
        1  => [  0.0,  0.0, 0, false, 0],   // pkg1: no upgrade income
        2  => [  0.0,  0.0, 0, false, 0],   // pkg2: no upgrade income
        3  => [ 60.0, 40.0, 1, false, 0],   // 60% sponsor, 40% placement-L1
        4  => [ 60.0, 40.0, 2, false, 0],   // 60% sponsor, 40% placement-L2
        5  => [ 30.0,  0.0, 0, true,  3],   // 30% sponsor, 30% placement-L3, 40% levels
        6  => [ 30.0,  0.0, 0, true,  4],   // 30% sponsor, 30% placement-L4, 40% levels
        7  => [ 30.0,  0.0, 0, true,  5],
        8  => [ 30.0,  0.0, 0, true,  6],
        9  => [ 30.0,  0.0, 0, true,  7],
        10 => [ 30.0,  0.0, 0, true,  8],
        11 => [ 30.0,  0.0, 0, true,  9],
    ];

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

    // ════════════════════════════════════════════════════════════════════
    //  BUILD WORKING TREE (registration)
    // ════════════════════════════════════════════════════════════════════

    public function buildChildChain(int $newMemberId, int $sponsorId): void
    {

                    $parentId = $sponsorId;
                $lvl = 1;

                while ($parentId > 0) {

                    $this->db->prepare(
                        "INSERT IGNORE INTO meddolic_user_child_ids
                        (member_id, child_id, level, date_time, topup_status, topup_date)
                        VALUES (?, ?, ?, NOW(), 1, NOW())"
                    )->execute([$parentId, $newMemberId, $lvl]);

                    $r = $this->db->prepare(
                        "SELECT sponser_id FROM meddolic_user_details WHERE member_id=? LIMIT 1"
                    );
                    $r->execute([$parentId]);

                    $parentId = (int)($r->fetchColumn() ?: 0);

                    $lvl++;
                }

        $placement     = $this->find3x3Placement($sponsorId);
        $placeholderId = $placement['placeholderId'];
        $legPosition   = $placement['legPosition'];

        $this->db->prepare(
            "UPDATE meddolic_user_details SET placeholderId=?, legPosition=? WHERE member_id=?"
        )->execute([$placeholderId, $legPosition, $newMemberId]);

        $pId = $placeholderId;
        $leg = $legPosition;
        for ($lvl = 1; $lvl <= 100 && $pId > 0; $lvl++) {
            $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, $leg, $lvl]);

            $q = $this->db->prepare(
                "SELECT placeholderId, legPosition FROM meddolic_user_details WHERE member_id=? LIMIT 1"
            );
            $q->execute([$pId]);
            $res = $q->fetch();
            $pId = $res ? (int)$res['placeholderId'] : 0;
            $leg = $res ? (int)$res['legPosition']   : 0;
        }
    }

    private function find3x3Placement(int $sponsorId): array
    {
        $queue = [$sponsorId];
        $qi    = 0;
        while (isset($queue[$qi])) {
            $parentId = $queue[$qi];
            for ($leg = 1; $leg <= 3; $leg++) {
                $stmt = $this->db->prepare(
                    "SELECT member_id FROM meddolic_user_details
                     WHERE placeholderId=? AND legPosition=? LIMIT 1"
                );
                $stmt->execute([$parentId, $leg]);
                $occ = $stmt->fetchColumn();
                if (!$occ) {
                    return ['placeholderId' => $parentId, 'legPosition' => $leg];
                }
                $queue[] = (int)$occ;
            }
            $qi++;
            if ($qi > 10000) break;
        }
        return ['placeholderId' => $sponsorId, 'legPosition' => 1];
    }

    // ════════════════════════════════════════════════════════════════════
    //  VALIDATE WORKING PURCHASE
    // ════════════════════════════════════════════════════════════════════

    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);
        }
        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 WORKING UPGRADE
    // ════════════════════════════════════════════════════════════════════

    public function processManualUpgrade(
        int $buyerId, int $sponsorId, float $pkgPrice, int $pkgId, string $txHash
    ): array {
        $rules            = self::INCOME_RULES[$pkgId] ?? [0.0, 0.0, 0, false, 0];
        $directPct        = (float)$rules[0];
        $uplinePct        = (float)$rules[1];
        $uplineAncLvl     = (int)$rules[2];    // placement level for pkg3/4 fixed upline
        $hasLevelDist     = (bool)$rules[3];
        $placementAncLvlD = (int)($rules[4] ?? 0); // placement level for pkg5+ 30% income
        $adminPct         = ($pkgId <= 2) ? 0.0 : $this->adminPct;

        // ── A. SPONSOR INCOME (all packages) ─────────────────────────
        // pkg3/4: 60% → sponsor
        // pkg5+:  30% → sponsor
        // GATE: sponsor must be active (topup_flag=1) AND currentPackage >= pkgId
        // If NOT qualified → sponsor income skipped
        if ($directPct > 0 && $sponsorId > 0) {
            $spQ = $this->db->prepare(
                "SELECT topup_flag, currentPackage FROM meddolic_user_details
                 WHERE member_id=? LIMIT 1"
            );
            $spQ->execute([$sponsorId]);
            $spRow = $spQ->fetch(PDO::FETCH_ASSOC);

            if ($spRow && (int)$spRow['topup_flag'] === 1 && (int)$spRow['currentPackage'] >= $pkgId) {
                $gross = round($pkgPrice * $directPct / 100, 5);
                $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, incomeType)
                         VALUES (?,?,?,?,?,?,?,NOW(),?,?)"
                    )->execute([$sponsorId, $buyerId, $gross, $pkgPrice, $admin, $net,
                                $pkgId, $directPct, 2]);
                } catch (Throwable $t) {}
                $this->addToWallet($sponsorId, $net, 1, 0);
            }
            // If sponsor NOT qualified → income skipped (not passed up)
        }

        // ── B. FIXED UPLINE INCOME (pkg3 and pkg4 only) ──────────────
        // pkg3: 40% → A2 (sponsor's sponsor)  — GATE: A2 must have currentPackage >= pkgId
        // pkg4: 40% → A2 (sponsor's sponsor)  — GATE: same
        if (!$hasLevelDist && $uplinePct > 0 && $uplineAncLvl > 0) {
            $uplineId = $this->getNthAncestor($buyerId, $uplineAncLvl);
            if ($uplineId > 0) {
                // Check gate: upline must be active and have same or higher package
                $gQ = $this->db->prepare(
                    "SELECT topup_flag, currentPackage FROM meddolic_user_details
                     WHERE member_id=? LIMIT 1"
                );
                $gQ->execute([$uplineId]);
                $gRow = $gQ->fetch(PDO::FETCH_ASSOC);

                if ($gRow && (int)$gRow['topup_flag'] === 1 && (int)$gRow['currentPackage'] >= $pkgId) {
                    $gross = round($pkgPrice * $uplinePct / 100, 5);
                    $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);
                }
                // If upline NOT qualified → income is skipped (not passed further up)
            }
        }

        // ── C. LEVEL INCOME (pkg5+ only): 40% across 10 upline levels ──
        // Percentages from meddolic_config_level_income table (L1=7%, L2=6%, L3=5%, L4-L10=1%)
        // GATE: each upline must be active AND currentPackage >= pkgId (same package as buyer)
        // If NOT qualified → skip that level silently, move to next upline
        // Inserts into meddolic_user_level_income
        if ($hasLevelDist) {
            $lvlCfg = [];
            try {
                $cfgQ = $this->db->prepare(
                    "SELECT level, levelPercent
                     FROM meddolic_config_level_income
                     ORDER BY level ASC LIMIT 10"
                );
                $cfgQ->execute();
                foreach ($cfgQ->fetchAll(PDO::FETCH_ASSOC) as $row) {
                    $lvlCfg[(int)$row['level']] = (float)$row['levelPercent'];
                }
            } catch (Throwable $t) {}

            // Walk PLACEMENT TREE using meddolic_user_set_placeholder_details
            // level=1 is buyer's direct parent, level=2 is grandparent, etc.
            for ($lvlNum = 1; $lvlNum <= 10; $lvlNum++) {
                if (!array_key_exists($lvlNum, $lvlCfg)) break;

                $pct   = (float)$lvlCfg[$lvlNum];
                $ancId = $this->getNthPlacementAncestor($buyerId, $lvlNum);
                if ($ancId <= 0) continue;   // no one at this level → skip

                $aQ = $this->db->prepare(
                    "SELECT topup_flag, currentPackage
                     FROM meddolic_user_details WHERE member_id=? LIMIT 1"
                );
                $aQ->execute([$ancId]);
                $anc = $aQ->fetch(PDO::FETCH_ASSOC);
                if (!$anc) continue;

                // GATE: must be active AND currentPackage >= pkgId
                if ((int)$anc['topup_flag'] === 1 && (int)$anc['currentPackage'] >= $pkgId) {
                    $gross = round($pkgPrice * 40.0 / 100 * $pct / 100, 5);
                    $admin = round($gross * $adminPct / 100, 5);
                    $net   = round($gross - $admin, 5);
                    try {
                        $this->db->prepare(
                            "INSERT INTO meddolic_user_level_income
                               (memberId, childId, levelIncome, level, Percent, dateTime)
                             VALUES (?, ?, ?, ?, ?, NOW())"
                        )->execute([$ancId, $buyerId, $net, $lvlNum, $pct]);
                    } catch (Throwable $t) {}
                    $this->addToWallet($ancId, $net, 4, 0);
                }
                // If NOT qualified → skip silently
            }
        }

        // ── D. PLACEMENT INCOME (pkg5+ only): 30% → placement ancestor ─────
        // Uses PLACEMENT TREE (meddolic_user_set_placeholder_details)
        // pkg5 → placement level 3 (A3),  pkg6 → level 4 (A4), etc.
        // GATE: ancestor must be active AND currentPackage >= pkgId
        if ($hasLevelDist && $placementAncLvlD > 0) {
            $plId = $this->getNthPlacementAncestor($buyerId, $placementAncLvlD);
            if ($plId > 0) {
                $gQ = $this->db->prepare(
                    "SELECT topup_flag, currentPackage FROM meddolic_user_details
                     WHERE member_id=? LIMIT 1"
                );
                $gQ->execute([$plId]);
                $gRow = $gQ->fetch(PDO::FETCH_ASSOC);

                if ($gRow && (int)$gRow['topup_flag'] === 1 && (int)$gRow['currentPackage'] >= $pkgId) {
                    $gross = round($pkgPrice * 30.0 / 100, 5);
                    $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, $placementAncLvlD,
                                    $pkgPrice, $admin, $net, $pkgId]);
                    } catch (Throwable $t) {}
                    $this->addToWallet($plId, $net, 2, 0);
                }
                // If NOT qualified → income is skipped
            }
        }

        // ── E. ADVANCE PACKAGE ────────────────────────────────────────
        $this->db->prepare(
            "UPDATE meddolic_user_details SET currentPackage=? WHERE member_id=?"
        )->execute([$pkgId, $buyerId]);

        // ── F. AUTO-ENTER SMART POOL (pkg3 only) ─────────────────────
        if ($pkgId === 3) {
            $smartPoolId = self::WORKING_TRIGGERS_GLOBAL[3];
            $already = $this->db->prepare(
                "SELECT entryId FROM meddolic_user_pool_entry_details
                 WHERE memberId=? AND poolId=? LIMIT 1"
            );
            $already->execute([$buyerId, $smartPoolId]);
            if (!$already->fetch()) {
                $this->enterGlobalPool($buyerId, $smartPoolId, 'AUTO_PKG3', $txHash);
            }
        }

        return ['success' => true];
    }

    // ════════════════════════════════════════════════════════════════════
    //  WORKING AUTO-UPGRADE (3/9/27)
    // ════════════════════════════════════════════════════════════════════

    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;

        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;

        $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
        );

        $this->checkWorkingAutoUpgrade((int)($u['sponser_id'] ?? 0));
    }

    public function checkAutoUpgrade(int $memberId): void
    {
        $this->checkWorkingAutoUpgrade($memberId);
    }

    // ════════════════════════════════════════════════════════════════════
    //  PROCESS GLOBAL POOL MANUAL PURCHASE
    // ════════════════════════════════════════════════════════════════════

    public function processGlobalPurchase(
        int $memberId, int $poolId, float $poolPrice, string $txHash
    ): array {
        $minPkg = self::GLOBAL_MIN_WORKING[$poolId] ?? 0;
        if ($minPkg > 0) {
            $u = getUserById($memberId);
            if ((int)($u['currentPackage'] ?? 0) < $minPkg) {
                $w = $this->fetchPoolPkg($minPkg);
                return ['error' => 'Complete Working Step ' . ($w['poolLevel'] ?? $minPkg) . ' first.'];
            }
        }
        $chk = $this->db->prepare(
            "SELECT entryId FROM meddolic_user_pool_entry_details
             WHERE memberId=? AND poolId=? AND poolStatus=1 LIMIT 1"
        );
        $chk->execute([$memberId, $poolId]);
        if ($chk->fetch()) {
            return ['error' => 'Already active in this pool.'];
        }
        $entryId = $this->enterGlobalPool($memberId, $poolId, '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 $poolId, string $via = 'MANUAL', string $txHash = ''
    ): int {
        $mp = $this->fetchMatrixPkg($poolId);
        if (!$mp) return 0;
        $poolPrice = (float)$mp['poolPrice'];

        // Find parent slot
        [$parentEntryId, $parentMemberId, $parentPoolLevel] = $this->bfsFindParent($poolId);

        $myPoolLevel = $parentEntryId ? ($parentPoolLevel + 1) : 1;

        // Leg position = children count + 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, $poolId]);
            $legPos = (int)($cq->fetch()['c'] ?? 0) + 1;
        }

        // Insert new entry
        $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, $poolId, $myPoolLevel,
            $parentMemberId ?: 0,
            $parentEntryId  ?: 0,
            $legPos
        ]);
        $newEntryId = (int)$this->db->lastInsertId();

        if ($parentEntryId && $parentMemberId) {

            // Record ancestors in placeholder table
            $this->recordPoolAncestors(
                $memberId, $newEntryId, $poolId,
                $parentEntryId, $parentMemberId, $legPos
            );

            // ── PAY INCOME ────────────────────────────────────────────
            // Pass the NEW member's entryId and their parent details
            $this->payGlobalJoinIncome(
                $memberId,          // new joiner
                $newEntryId,        // new joiner's entryId
                $parentMemberId,    // direct parent in pool
                $parentEntryId,     // parent's entryId
                $poolId,
                $poolPrice,
                $legPos
            );

            // Check completion
            $this->checkPoolCompletion($parentMemberId, $parentEntryId, $poolId);
        }

        // Purchase log
        $u    = getUserById($memberId);
        $sId  = (int)($u['sponser_id'] ?? 0);
        $hash = ($via === 'MANUAL')
            ? $txHash
            : ('AUTO_' . $via . '_' . $memberId . '_' . 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,$poolId,$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,$poolId,$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;
    }

    // ════════════════════════════════════════════════════════════════════
    //  RECORD POOL ANCESTORS (placeholder table)
    //  Walks up pool tree from parent, inserts ancestor tracking rows
    // ════════════════════════════════════════════════════════════════════

    private function recordPoolAncestors(
        int $childMemberId, int $childEntryId,
        int $poolId, int $parentEntryId, int $parentMemberId, int $legPos
    ): void {
        $curEntryId  = $parentEntryId;
        $curMemberId = $parentMemberId;
        $curLeg      = $legPos;
        $level       = 1;

        while ($curMemberId > 0 && $level <= 3) {
            try {
                $this->db->prepare(
                    "INSERT IGNORE INTO meddolic_user_pool_placeholder_details
                       (poolId, member_id, child_id, leg_position, level,
                        parentEntryId, entryId, date_time)
                     VALUES (?,?,?,?,?,?,?,NOW())"
                )->execute([
                    $poolId, $curMemberId, $childMemberId,
                    $curLeg, $level, $curEntryId, $childEntryId
                ]);
            } catch (Throwable $t) {}

            // Walk up to next ancestor in pool tree
            $pQ = $this->db->prepare(
                "SELECT placeholder1Id, leg1Position, parent1EntryId
                 FROM meddolic_user_pool_entry_details
                 WHERE entryId=? AND poolId=? LIMIT 1"
            );
            $pQ->execute([$curEntryId, $poolId]);
            $pRow = $pQ->fetch();

            if (!$pRow || (int)$pRow['placeholder1Id'] <= 0) break;

            $curMemberId = (int)$pRow['placeholder1Id'];
            $curLeg      = (int)$pRow['leg1Position'];
            $curEntryId  = (int)$pRow['parent1EntryId'];
            $level++;
        }
    }

    // ════════════════════════════════════════════════════════════════════
    //  PAY GLOBAL JOIN INCOME  ← CORE FIXED FUNCTION
    //
    //  RULE (from diagram — confirmed by DB analysis):
    //
    //  When new member (N) joins under parent (P) at legPos:
    //
    //  legPos=1 (1st child of P):
    //    → P gets $poolPrice (FULL)
    //    → Nobody else gets anything
    //
    //  legPos=2 (2nd child of P):
    //    → P gets $poolPrice (FULL)
    //    → P's direct parent in pool tree (GP = pool-grandparent) gets $poolPrice (FULL)
    //    → If GP doesn't exist (P is pool root) → P gets the second $poolPrice too
    //
    //  legPos=3 (3rd child of P):
    //    → Same as legPos=2: P gets $poolPrice + GP gets $poolPrice
    //
    //  TOTAL INCOME GENERATED:
    //    leg1 = $poolPrice (1 payment)
    //    leg2 = $poolPrice × 2 (2 payments)
    //    leg3 = $poolPrice × 2 (2 payments)
    //    Max per pool slot = 1 + 2 + 2 = 5 payments × $poolPrice
    //
    //  EXAMPLE from DB (pool1, poolPrice=$100):
    //    22 joins under 21 at leg1 → 21 gets $95 (1 row) ✅
    //    23 joins under 21 at leg2 → 21 gets $95, 21's pool-parent gets $95
    //                                 21 has no pool-parent → 21 gets $95+$95 (2 rows) ✅
    //    24 joins under 21 at leg3 → same → 21 gets 2 rows ✅
    //    25 joins under 22 at leg1 → 22 gets $95 (1 row) ✅
    //                                 21 gets NOTHING ← this is CORRECT for leg1
    //
    //  When 26 joins under 22 at leg2:
    //    → 22 gets $95 + 21 (22's pool-parent) gets $95 ← THIS IS CORRECT
    //
    //  @param $newMemberId  the member who just joined
    //  @param $newEntryId   their new entryId
    //  @param $parentMemberId  their direct parent in pool
    //  @param $parentEntryId   parent's entryId
    // ════════════════════════════════════════════════════════════════════

    private function payGlobalJoinIncome(
        int $newMemberId,    int $newEntryId,
        int $parentMemberId, int $parentEntryId,
        int $poolId, float $poolPrice, int $legPos
    ): void {

        // ── Always pay parent (full poolPrice) ────────────────────────
        $this->creditPoolIncome(
            $parentMemberId, $poolId, $poolPrice,
            $newEntryId,     // income is credited for THIS join (child's entryId)
            $parentEntryId
        );

        // ── For leg2 and leg3: also pay pool-grandparent ──────────────
        if ($legPos >= 2) {

            // Pool-grandparent = placeholder1Id stored in PARENT's entry row
            $gpQ = $this->db->prepare(
                "SELECT placeholder1Id, parent1EntryId
                 FROM meddolic_user_pool_entry_details
                 WHERE entryId=? AND poolId=? LIMIT 1"
            );
            $gpQ->execute([$parentEntryId, $poolId]);
            $gpRow         = $gpQ->fetch();
            $gpMemberId    = (int)($gpRow['placeholder1Id']  ?? 0);
            $gpEntryId     = (int)($gpRow['parent1EntryId']  ?? 0);

            if ($gpMemberId > 0 && $gpMemberId !== $parentMemberId) {
                // Grandparent exists → pay them full poolPrice
                $this->creditPoolIncome(
                    $gpMemberId, $poolId, $poolPrice,
                    $newEntryId,  // income for this join
                    $gpEntryId
                );
            } else {
                // No grandparent (parent is root) → extra payment also to parent
                $this->creditPoolIncome(
                    $parentMemberId, $poolId, $poolPrice,
                    $newEntryId,
                    $parentEntryId
                );
            }
        }
    }

    // ════════════════════════════════════════════════════════════════════
    //  CREDIT POOL INCOME
    //  Checks direct requirement → passes up if not met
    //  Deducts admin → credits net to wallet + income table
    //
    //  @param $memberId       who receives income
    //  @param $poolId         which pool
    //  @param $amount         gross amount (before admin)
    //  @param $childEntryId   the joining member's entryId (for income record)
    //  @param $receiverEntryId the receiver's own entryId (for reference)
    // ════════════════════════════════════════════════════════════════════

    private function creditPoolIncome(
        int $memberId, int $poolId, float $amount,
        int $childEntryId, int $receiverEntryId = 0
    ): void {
        if ($memberId <= 0 || $amount <= 0) return;

        // Direct requirement (pools 2+)
        $dirReq = self::GLOBAL_DIRECT_REQ[$poolId] ?? 0;
        if ($dirReq > 0 && $this->countActiveDirects($memberId) < $dirReq) {
            // Not qualified → pass income to pool-tree parent
            $ppQ = $this->db->prepare(
                "SELECT placeholder1Id, parent1EntryId
                 FROM meddolic_user_pool_entry_details
                 WHERE memberId=? AND poolId=? AND poolStatus=1
                 ORDER BY entryId DESC LIMIT 1"
            );
            $ppQ->execute([$memberId, $poolId]);
            $ppRow  = $ppQ->fetch();
            $poolPt = (int)($ppRow['placeholder1Id'] ?? 0);
            $ptEid  = (int)($ppRow['parent1EntryId'] ?? 0);
            if ($poolPt > 0 && $poolPt !== $memberId) {
                $this->creditPoolIncome($poolPt, $poolId, $amount, $childEntryId, $ptEid);
            }
            return;
        }

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

        try {
            $this->db->prepare(
                "INSERT INTO meddolic_user_pool_income
                   (memberId, poolId, poolLevel, poolIncome, entryId,
                    entryCount, dateTime, releaseStatus)
                 VALUES (?,?,0,?,?,1,NOW(),1)"
            )->execute([
                $memberId, $poolId, $net,
                $childEntryId,  // ← store the CHILD's entryId (which join caused this income)
                1
            ]);
        } catch (Throwable $t) {}

        $this->addToWallet($memberId, $net, 3, $childEntryId);
    }

    // ════════════════════════════════════════════════════════════════════
    //  CHECK POOL COMPLETION
    //  L1 = 3 direct children
    //  L2 = 9 grandchildren
    //  L3 = 27 great-grandchildren → bonus + RE-ENTRY + CHAIN
    // ════════════════════════════════════════════════════════════════════

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

        // L2: grandchildren
        $gq = $this->db->prepare(
            "SELECT COUNT(*) AS c FROM meddolic_user_pool_entry_details
             WHERE poolId=? AND parent1EntryId IN (
                 SELECT entryId FROM meddolic_user_pool_entry_details
                 WHERE parent1EntryId=? AND poolId=?
             )"
        );
        $gq->execute([$poolId, $entryId, $poolId]);
        $l2 = (int)($gq->fetch()['c'] ?? 0);

        // L3: great-grandchildren
        $l3q = $this->db->prepare(
            "SELECT COUNT(*) AS c FROM meddolic_user_pool_entry_details
             WHERE poolId=? AND parent1EntryId IN (
                 SELECT entryId FROM meddolic_user_pool_entry_details
                 WHERE poolId=? AND parent1EntryId IN (
                     SELECT entryId FROM meddolic_user_pool_entry_details
                     WHERE parent1EntryId=? AND poolId=?
                 )
             )"
        );
        $l3q->execute([$poolId, $poolId, $entryId, $poolId]);
        $l3 = (int)($l3q->fetch()['c'] ?? 0);

        // L1 bonus: 3 direct
        if ($l1 >= 3) {
            $this->grantLevelBonus($memberId, $entryId, $poolId, 1, 3);
        }

        // L2 bonus: 9 grandchildren
        if ($l2 >= 9) {
            $this->grantLevelBonus($memberId, $entryId, $poolId, 2, 9);
        }

        // L3 bonus: 27 great-grandchildren → REENTRY + CHAIN
        if ($l3 >= 27) {
            $dup = $this->db->prepare(
                "SELECT id FROM meddolic_user_pool_income
                 WHERE memberId=? AND poolId=? AND poolLevel=3 AND entryId=? LIMIT 1"
            );
            $dup->execute([$memberId, $poolId, $entryId]);
            if (!$dup->fetch()) {
                $this->grantLevelBonus($memberId, $entryId, $poolId, 3, 27);

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

                $this->db->prepare(
                    "UPDATE meddolic_user_pool_entry_details
                     SET reEntryCount=reEntryCount+1 WHERE entryId=?"
                )->execute([$entryId]);

                // Re-entry into same pool
                $this->enterGlobalPool($memberId, $poolId, 'REENTRY');

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

        // Propagate check UP the pool tree
        $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'],
                $poolId
            );
        }
    }

    private function grantLevelBonus(
        int $memberId, int $entryId, int $poolId, int $poolLevel, int $userNeed
    ): void {
        $dup = $this->db->prepare(
            "SELECT id FROM meddolic_user_pool_income
             WHERE memberId=? AND poolId=? AND poolLevel=? AND entryId=? LIMIT 1"
        );
        $dup->execute([$memberId, $poolId, $poolLevel, $entryId]);
        if ($dup->fetch()) return;

        $cfg = $this->fetchPoolIncomeCfg($poolId, $poolLevel, $userNeed);
        if (!$cfg) return;

        $bonus = (float)$cfg['userIncome'];
        $admin = round($bonus * $this->adminPct / 100, 5);
        $net   = round($bonus - $admin, 5);

        try {
            $this->db->prepare(
                "INSERT INTO meddolic_user_pool_income
                   (memberId,poolId,poolLevel,poolIncome,entryId,
                    entryCount,dateTime,releaseStatus)
                 VALUES (?,?,?,?,?,?,NOW(),1)"
            )->execute([$memberId, $poolId, $poolLevel, $net, $entryId, $userNeed]);
        } catch (Throwable $t) {}

        $this->addToWallet($memberId, $net, 3, $entryId);
    }

    private function tryPoolChain(int $memberId, int $filledPoolId): void
    {
        if (!isset(self::GLOBAL_CHAIN[$filledPoolId])) return;
        $nextPoolId = self::GLOBAL_CHAIN[$filledPoolId];

        $minPkg = self::GLOBAL_MIN_WORKING[$nextPoolId] ?? 0;
        $u      = getUserById($memberId);
        if ($minPkg && (int)($u['currentPackage'] ?? 0) < $minPkg) return;

        $dirReq = self::GLOBAL_DIRECT_REQ[$nextPoolId] ?? 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, $nextPoolId]);
        if ($chk->fetch()) return;

        // Must have manually purchased next pool (pools 2+)
        if ($nextPoolId > 1) {
            $paid = $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"
            );
            $paid->execute([$memberId, $nextPoolId]);
            if (!$paid->fetch()) return;
        }

        $this->enterGlobalPool($memberId, $nextPoolId, 'CHAIN_' . $filledPoolId);
    }

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

    public function getWorkingMatrixTree(int $memberId): array
    {
        $stmt = $this->db->prepare("
            SELECT DISTINCT
                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 = ?
            ORDER BY p.level ASC, ud.legPosition ASC
        ");
        $stmt->execute([$memberId]);
        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

        if (empty($rows)) {
            return ['tree' => [], 'nodes' => [], 'total' => 0, 'levelMap' => []];
        }

        $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'],
                'level'          => 0,
            ];
        }

        $queue   = [[$memberId, 0]];
        $qi      = 0;
        $lvlMap  = [];
        $visited = [];
        while (isset($queue[$qi])) {
            [$pid, $plvl] = $queue[$qi++];
            foreach ($nodes as $nid => $n) {
                if ((int)$n['parent_id'] === $pid && !isset($visited[$nid])) {
                    $visited[$nid]         = true;
                    $nodes[$nid]['level']  = $plvl + 1;
                    $lvlMap[$plvl + 1][]   = $nid;
                    $queue[]               = [$nid, $plvl + 1];
                }
            }
        }

        $rootChildren = [];
        foreach ($nodes as $nid => $n) {
            if ((int)$n['parent_id'] === $memberId) {
                $rootChildren[$n['leg_pos']] = $n;
            }
        }
        ksort($rootChildren);

        return [
            'tree'     => $rootChildren,
            'nodes'    => $nodes,
            'total'    => count($nodes),
            'levelMap' => $lvlMap,
        ];
    }

    public function getGlobalPoolTree(int $memberId, int $poolId): array
    {
        // ── Find member's active ROOT entry in this pool ──────────────
        // Use ASC so we always get their FIRST (oldest) active slot,
        // not a re-entry that might have no children yet.
        $eQ = $this->db->prepare(
            "SELECT * FROM meddolic_user_pool_entry_details
             WHERE memberId=? AND poolId=? AND poolStatus=1
             ORDER BY entryId ASC LIMIT 1"
        );
        $eQ->execute([$memberId, $poolId]);
        $entry = $eQ->fetch(PDO::FETCH_ASSOC);
        if (!$entry) return ['entry' => null, 'levels' => [], 're_entries' => 0];

        $myEntryId = (int)$entry['entryId'];

        // ── LEVEL 1: direct children of my entry (max 3) ─────────────
        $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([$myEntryId, $poolId]);
        $l1 = $l1q->fetchAll(PDO::FETCH_ASSOC);   // MUST be ASSOC so $n['entryId'] works

        // ── LEVEL 2: children of each L1 node (max 9) ────────────────
        $l2 = [];
        foreach ($l1 as $n1) {
            $n1EntryId = (int)$n1['entryId'];      // explicit int cast
            $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([$n1EntryId, $poolId]);
            $l2 = array_merge($l2, $l2q->fetchAll(PDO::FETCH_ASSOC));
        }

        // ── LEVEL 3: children of each L2 node (max 27) ───────────────
        $l3 = [];
        foreach ($l2 as $n2) {
            $n2EntryId = (int)$n2['entryId'];      // explicit int cast
            $l3q = $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"
            );
            $l3q->execute([$n2EntryId, $poolId]);
            $l3 = array_merge($l3, $l3q->fetchAll(PDO::FETCH_ASSOC));
        }

        // ── Re-entry count ────────────────────────────────────────────
        $rQ = $this->db->prepare(
            "SELECT COALESCE(SUM(reEntryCount), 0) AS t
             FROM meddolic_user_pool_entry_details
             WHERE memberId = ? AND poolId = ?"
        );
        $rQ->execute([$memberId, $poolId]);
        $reEntries = (int)(($rQ->fetch(PDO::FETCH_ASSOC))['t'] ?? 0);

        return [
            'entry'      => $entry,
            're_entries' => $reEntries,
            'levels'     => [
                1 => $l1,   // up to 3  nodes
                2 => $l2,   // up to 9  nodes
                3 => $l3,   // up to 27 nodes
            ],
        ];
    }

    public function getGlobalMatrixTree(int $memberId, int $poolId): array
    {
        return $this->getGlobalPoolTree($memberId, $poolId);
    }

    // ════════════════════════════════════════════════════════════════════
    //  INCOME PREVIEW
    // ════════════════════════════════════════════════════════════════════

    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,
        ];
    }

    // ════════════════════════════════════════════════════════════════════
    //  PRIVATE HELPERS
    // ════════════════════════════════════════════════════════════════════

    private function bfsFindParent(int $poolId): 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([$poolId]);
        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'], $poolId]);
            if ((int)($cq->fetch()['c'] ?? 0) < 3) {
                return [(int)$row['entryId'], (int)$row['memberId'], (int)$row['poolLevel']];
            }
        }
        return [null, null, 0];
    }

    // ════════════════════════════════════════════════════════════════════
    //  getNthPlacementAncestor
    //  Returns the Nth ancestor of $memberId in the PLACEMENT TREE
    //  (meddolic_user_set_placeholder_details).
    //
    //  Table structure:
    //    member_id = ancestor (upline in placement tree)
    //    child_id  = the new member who was placed
    //    level     = how many hops above (1 = direct parent, 2 = grandparent …)
    //
    //  Income routing (confirmed from plan image):
    //    pkg3 ($60):  60% → sponsor,  40% → placement level 1 (A1 in placement)
    //    pkg4 ($200): 60% → sponsor,  40% → placement level 2 (A2 in placement)
    //    pkg5 ($500): 30% → sponsor,  30% → placement level 3 (A3 in placement), 40% → 10 levels
    //    pkg6+:       30% → sponsor,  30% → placement level N (increments), 40% → 10 levels
    // ════════════════════════════════════════════════════════════════════
    private function getNthPlacementAncestor(int $memberId, int $n): int
    {
        if ($n <= 0 || $memberId <= 0) return 0;
        $q = $this->db->prepare(
            "SELECT member_id FROM meddolic_user_set_placeholder_details
             WHERE child_id = ? AND level = ?
             LIMIT 1"
        );
        $q->execute([$memberId, $n]);
        return (int)($q->fetchColumn() ?: 0);
    }

    // Legacy alias — kept for backward compat but now uses placement tree
    private function getNthAncestor(int $startId, int $n): int
    {
        return $this->getNthPlacementAncestor($startId, $n);
    }

    private function getNthAncestorFromBuyer(int $buyerId, int $n): int
    {
        return $this->getNthPlacementAncestor($buyerId, $n);
    }

    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 $poolId, int $poolLevel, int $userNeed): ?array
    {
        // Support both column name variants
        $q = $this->db->prepare(
            "SELECT * FROM meddolic_config_pool_income
             WHERE (matrixId=? OR poolId=?) AND poolLevel=? AND userNeed=?
               AND poolStatus=1 LIMIT 1"
        );
        $q->execute([$poolId, $poolId, $poolLevel, $userNeed]);
        return $q->fetch() ?: null;
    }
}