<?php
/**
 * سودانا — BillingEngine v2 (SAFE)
 * PATCH: atomic reserve/commit/rollback + idempotency + SELECT FOR UPDATE
 * PRINCIPLE: NO debit before execution confirmed
 *
 * Flow: reserveFunds() → execute → commitFunds() OR rollbackFunds()
 * States stored in billing_reserves table (auto-created)
 */
require_once __DIR__ . '/AccountingEngine.php';

class BillingEngine {

    // Billing tiers (SDG)
    const TIERS = [
        'free'       => 0,
        'micro'      => 10,
        'standard'   => 100,
        'premium'    => 300,
        'enterprise' => 1000,
    ];

    // ── PricingEngine integration ─────────────────────────────────
    // Document prices are now managed via admin panel (document_prices table)
    // Use PricingEngine::getDocumentPrice() instead of flat TIERS for docs

    /**
     * getDocumentCost(): سعر إصدار وثيقة من PricingEngine (مع خصم الخطة)
     */
    public static function getDocumentCost(int $userId, string $docType): array {
        // Get user plan
        $planKey = 'free';
        try {
            $planKey = db()->prepare("SELECT plan_key FROM users WHERE id=? LIMIT 1")
                          ->execute([$userId]) ? 'free' : 'free';
            $st = db()->prepare("SELECT plan_key FROM users WHERE id=? LIMIT 1");
            $st->execute([$userId]);
            $planKey = $st->fetchColumn() ?: 'free';
        } catch (Throwable $e) {}

        // Get price from document_prices table (via PricingEngine)
        $costSdg = 0;
        try {
            require_once __DIR__ . '/PricingEngine.php';
            $costSdg = PricingEngine::getDocumentPrice($docType, $planKey);
        } catch (Throwable $e) {
            // Fallback to old TIERS
            $costSdg = self::TIERS['premium'];
        }

        // Check subscription_credits first (Plus/Pro monthly credit)
        $creditResult = ['source' => 'wallet', 'enough' => false, 'credit_id' => null];
        if ($costSdg > 0 && $planKey !== 'free') {
            try {
                $cr = db()->prepare(
                    "SELECT id, remaining_sdg FROM subscription_credits
                     WHERE user_id=? AND period_month=MONTH(NOW()) AND period_year=YEAR(NOW())
                     AND remaining_sdg >= ? LIMIT 1"
                );
                $cr->execute([$userId, $costSdg]);
                $creditRow = $cr->fetch();
                if ($creditRow) {
                    $creditResult = [
                        'source'    => 'credit',
                        'credit_id' => $creditRow['id'],
                        'enough'    => true,
                    ];
                }
            } catch (Throwable $e) {}
        }

        // If not from credit, check wallet
        if (!$creditResult['enough']) {
            try {
                $walBal = db()->prepare("SELECT balance FROM wallet WHERE user_id=? LIMIT 1");
                $walBal->execute([$userId]);
                $balance = (float)($walBal->fetchColumn() ?? 0);
                $creditResult['enough']  = $balance >= $costSdg;
                $creditResult['source']  = 'wallet';
                $creditResult['balance'] = $balance;
            } catch (Throwable $e) {}
        }

        return [
            'cost_sdg'  => $costSdg,
            'plan_key'  => $planKey,
            'doc_type'  => $docType,
            'can_afford'=> $creditResult['enough'],
            'source'    => $creditResult['source'],
            'credit_id' => $creditResult['credit_id'] ?? null,
        ];
    }

    /**
     * chargeDocument(): خصم تكلفة الوثيقة (من credit أو wallet)
     */
    public static function chargeDocument(int $userId, string $docType, string $executionId): array {
        $costInfo = self::getDocumentCost($userId, $docType);
        if (!$costInfo['can_afford']) {
            return ['ok' => false, 'error' => 'insufficient_balance', 'cost_sdg' => $costInfo['cost_sdg']];
        }
        if ($costInfo['cost_sdg'] === 0) {
            return ['ok' => true, 'cost_sdg' => 0, 'source' => 'free'];
        }

        $cost = $costInfo['cost_sdg'];

        try {
            db()->beginTransaction();
            if ($costInfo['source'] === 'credit' && $costInfo['credit_id']) {
                db()->prepare(
                    "UPDATE subscription_credits SET used_sdg=used_sdg+?, remaining_sdg=remaining_sdg-?
                     WHERE id=? AND remaining_sdg>=?"
                )->execute([$cost, $cost, $costInfo['credit_id'], $cost]);
            } else {
                $upd = db()->prepare(
                    "UPDATE wallet SET balance=balance-? WHERE user_id=? AND balance>=?"
                );
                $upd->execute([$cost, $userId, $cost]);
                if ($upd->rowCount() === 0) {
                    db()->rollBack();
                    return ['ok' => false, 'error' => 'wallet_insufficient'];
                }
                // Log transaction
                $newBal = db()->prepare("SELECT balance FROM wallet WHERE user_id=?")->execute([$userId])
                    ? (float)(function($uid){ $s=db()->prepare("SELECT balance FROM wallet WHERE user_id=? LIMIT 1"); $s->execute([$uid]); return $s->fetchColumn()??0; })($userId)
                    : 0;
                db()->prepare(
                    "INSERT INTO wallet_transactions
                     (wallet_id,user_id,type,amount_sdg,balance_after,ref_type,description)
                     SELECT id,user_id,'debit',?,?,'doc_issuance',?
                     FROM wallet WHERE user_id=?"
                )->execute([$cost, $newBal, "إصدار وثيقة: $docType", $userId]);
            }
            db()->commit();
            return ['ok' => true, 'cost_sdg' => $cost, 'source' => $costInfo['source']];
        } catch (Throwable $e) {
            db()->rollBack();
            return ['ok' => false, 'error' => $e->getMessage()];
        }
    }

        // Per-sector billing class
    const SECTOR_TIER = [
        'academic'    => 'free',  'community'   => 'free',
        'government'  => 'free',  'platform'    => 'free',
        'general'     => 'free',  'developer'   => 'free',
        'mental'      => 'micro', 'career'      => 'micro',
        'productivity'=> 'micro', 'emergency'   => 'micro',
        'agriculture' => 'standard', 'market'   => 'standard',
        'tech'        => 'standard', 'finance'  => 'standard',
        'pharmacy'    => 'standard',
        'bde'         => 'premium',  'medical'  => 'premium',
        'enterprise'  => 'enterprise', 'family'   => 'micro',
        'community'   => 'free',       'market'   => 'free',
        'legal'       => 'premium',  'business' => 'premium',
        'engineering' => 'premium',
    ];

    // ── STEP 1: Reserve funds (do NOT debit yet) ───────────────────────
    public static function reserveFunds(int $userId, string $sector,
            float $amount, string $executionId): array {
        $traceId = uniqid('bil_', true);
        $tier    = self::SECTOR_TIER[$sector] ?? 'free';
        $cost    = $amount > 0 ? $amount : self::TIERS[$tier];

        if ($cost === 0.0) {
            return ['decision'=>'allow','reserved'=>true,'cost'=>0,
                    'execution_id'=>$executionId,'tier'=>$tier,'trace_id'=>$traceId];
        }

        if (!$userId) {
            return ['decision'=>'deny','reason'=>'login_required','cost'=>$cost,'trace_id'=>$traceId];
        }

        // Idempotency: if already reserved for this execution_id, return existing
        try {
            $existing = db()->prepare(
                "SELECT * FROM billing_reserves WHERE execution_id=? AND status IN ('reserved','committed')"
            );
            $existing->execute([$executionId]);
            $row = $existing->fetch(PDO::FETCH_ASSOC);
            if ($row) {
                return ['decision'=>'allow','reserved'=>true,'cost'=>(float)$row['amount'],
                        'execution_id'=>$executionId,'idempotent'=>true,'trace_id'=>$traceId];
            }
        } catch (Throwable $e) {
            // billing_reserves table may not exist yet — create it
            self::ensureTable();
        }

        try {
            db()->beginTransaction();

            // SELECT FOR UPDATE — prevents concurrent double-reserve
            // READ both balance_sdg AND hold_balance
            $walQ = db()->prepare(
                "SELECT balance_sdg, hold_balance FROM wallet WHERE user_id=? FOR UPDATE"
            );
            $walQ->execute([$userId]);
            $row     = $walQ->fetch(PDO::FETCH_ASSOC);
            $balance = (float)($row['balance_sdg'] ?? 0);
            $held    = (float)($row['hold_balance'] ?? 0);
            // Available = balance minus already-held funds
            $available = $balance - $held;

            if ($available < $cost) {
                db()->rollBack();
                return ['decision'=>'deny','reason'=>'insufficient_balance',
                        'balance'=>$balance,'held'=>$held,'available'=>$available,
                        'shortfall'=>round($cost-$available,2),
                        'top_up_url'=>(defined('SITE_URL')?SITE_URL:'').'/wallet.php',
                        'cost'=>$cost,'tier'=>$tier,'trace_id'=>$traceId];
            }

            // HOLD ONLY — DO NOT touch balance_sdg until commitFunds()
            db()->prepare(
                "UPDATE wallet SET hold_balance = hold_balance + ? WHERE user_id=?"
            )->execute([$cost, $userId]);

            db()->prepare(
                "INSERT INTO billing_reserves
                 (execution_id, user_id, sector, amount, status, created_at)
                 VALUES (?,?,?,?,'reserved',NOW())"
            )->execute([$executionId, $userId, $sector, $cost]);

            // NO wallet_transactions INSERT here — only on commit (actual debit)

            db()->commit();

            return ['decision'=>'allow','reserved'=>true,'cost'=>$cost,
                    'balance_sdg'=>$balance,          // unchanged
                    'hold_balance'=>round($held+$cost,2), // updated
                    'available_after'=>round($available-$cost,2),
                    'execution_id'=>$executionId,'tier'=>$tier,'trace_id'=>$traceId];

        } catch (Throwable $e) {
            if (db()->inTransaction()) db()->rollBack();
            error_log("[BillingEngine::reserve] " . $e->getMessage());
            // Fail-open for free tier only
            if ($tier === 'free') {
                return ['decision'=>'allow','reserved'=>true,'cost'=>0,'trace_id'=>$traceId,'billing_error'=>true];
            }
            return ['decision'=>'deny','reason'=>'billing_error','error'=>$e->getMessage(),'trace_id'=>$traceId];
        }
    }

    // ── STEP 2a: Commit — execution succeeded ─────────────────────────
    public static function commitFunds(string $executionId): bool {
        try {
            db()->beginTransaction();

            // Lock the reserve row
            $q = db()->prepare(
                "SELECT user_id, amount FROM billing_reserves
                 WHERE execution_id=? AND status='reserved'
                 FOR UPDATE"
            );
            $q->execute([$executionId]);
            $row = $q->fetch(PDO::FETCH_ASSOC);

            if (!$row) { db()->rollBack(); return true; } // Already committed

            $userId = (int)$row['user_id'];
            $amount = (float)$row['amount'];

            // NOW deduct balance AND release hold (atomic)
            db()->prepare(
                "UPDATE wallet
                 SET balance_sdg  = balance_sdg  - ?,
                     hold_balance = hold_balance - ?
                 WHERE user_id=?"
            )->execute([$amount, $amount, $userId]);

            // Mark committed
            db()->prepare(
                "UPDATE billing_reserves SET status='committed', committed_at=NOW()
                 WHERE execution_id=?"
            )->execute([$executionId]);

            // NOW log the actual debit (execution confirmed)
            db()->prepare(
                "INSERT INTO wallet_transactions
                 (user_id,type,amount_sdg,description,ref_type,ref_id,created_at)
                 VALUES (?,'debit',?,'تنفيذ مكتمل','billing_commit',?,NOW())"
            )->execute([$userId, $amount, $executionId]);

            db()->commit();
            return true;

        } catch (Throwable $e) {
            if (db()->inTransaction()) db()->rollBack();
            error_log("[BillingEngine::commit] " . $e->getMessage());
            return false;
        }
    }

    // ── STEP 2b: Rollback — execution failed → release hold, balance unchanged ──
    public static function rollbackFunds(string $executionId): bool {
        try {
            db()->beginTransaction();

            $q = db()->prepare(
                "SELECT user_id, amount FROM billing_reserves
                 WHERE execution_id=? AND status='reserved'
                 FOR UPDATE"
            );
            $q->execute([$executionId]);
            $row = $q->fetch(PDO::FETCH_ASSOC);

            if (!$row) { db()->rollBack(); return true; } // Already rolled back

            // Release hold ONLY — balance_sdg was NEVER touched during reserve
            db()->prepare(
                "UPDATE wallet SET hold_balance = hold_balance - ? WHERE user_id=?"
            )->execute([(float)$row['amount'], (int)$row['user_id']]);

            // Mark as rolled back
            db()->prepare(
                "UPDATE billing_reserves SET status='rolled_back', rolled_back_at=NOW()
                 WHERE execution_id=?"
            )->execute([$executionId]);

            // Audit log for rollback (no wallet_transactions credit needed — balance unchanged)
            error_log("[BillingEngine::rollback] execution_id=$executionId amount={$row['amount']} hold_released=true balance_unchanged=true");

            db()->commit();
            return true;

        } catch (Throwable $e) {
            if (db()->inTransaction()) db()->rollBack();
            error_log("[BillingEngine::rollback] " . $e->getMessage());
            return false;
        }
    }

    // ── Legacy evaluate() — wraps reserve for backward compatibility ──
    public static function evaluate(int $userId, string $sector, array $executionIntent = []): array {
        $traceId     = uniqid('bil_', true);
        $tier        = self::SECTOR_TIER[$sector] ?? 'free';
        $cost        = self::TIERS[$tier];
        $executionId = $executionIntent['execution_id'] ?? $traceId;
        $flowType    = $executionIntent['flow_type'] ?? 'ai_answer';

        if ($cost === 0) {
            return ['decision'=>'allow','tier'=>$tier,'cost'=>0,'trace_id'=>$traceId];
        }

        if (!$userId) {
            return ['decision'=>'deny','reason'=>'login_required','tier'=>$tier,'cost'=>$cost,'trace_id'=>$traceId];
        }

        // Provider booking → create invoice (offline payment)
        if ($flowType === 'provider_booking') {
            require_once __DIR__ . '/InvoiceEngine.php';
            $invoice = InvoiceEngine::create($userId, $sector, $cost, [
                'payment_method' => 'offline',
                'execution_id'   => $executionId,
            ]);
            if ($invoice['success'] ?? false) {
                return [
                    'decision'    => 'create_invoice',
                    'invoice_id'  => $invoice['invoice_id'],
                    'reference'   => $invoice['reference_code'],
                    'payment_url' => (defined('SITE_URL') ? SITE_URL : '') . '/payment.php?invoice_id=' . $invoice['invoice_id'],
                    'tier'        => $tier, 'cost' => $cost, 'trace_id' => $traceId,
                ];
            }
            return ['decision'=>'deny','reason'=>'invoice_error','trace_id'=>$traceId];
        }

        // AI tier → use reserve/commit pattern
        $reserved = self::reserveFunds($userId, $sector, $cost, $executionId);
        return $reserved;
    }

    public static function canAfford(int $userId, string $sector): bool {
        $cost = self::TIERS[self::SECTOR_TIER[$sector] ?? 'free'];
        if ($cost === 0) return true;
        if (!$userId) return false;
        try {
            $w = db()->prepare("SELECT balance_sdg FROM wallet WHERE user_id=?");
            $w->execute([$userId]);
            return (float)($w->fetchColumn() ?: 0) >= $cost;
        } catch (Throwable $e) { return true; }
    }

    public static function getBillingClass(string $sector): array {
        $tier = self::SECTOR_TIER[$sector] ?? 'free';
        return ['tier' => $tier, 'cost_sdg' => self::TIERS[$tier]];
    }

    // ── Ensure billing_reserves table exists ──────────────────────────
    private static function ensureTable(): void {
        try {
            db()->exec("CREATE TABLE IF NOT EXISTS billing_reserves (
                id           BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
                execution_id VARCHAR(60)     NOT NULL UNIQUE,
                user_id      INT UNSIGNED    NOT NULL,
                sector       VARCHAR(50)     NOT NULL,
                amount       DECIMAL(10,2)   NOT NULL,
                status       ENUM('reserved','committed','rolled_back') DEFAULT 'reserved',
                created_at   DATETIME DEFAULT CURRENT_TIMESTAMP,
                committed_at DATETIME NULL,
                rolled_back_at DATETIME NULL,
                INDEX idx_user (user_id),
                INDEX idx_status (status)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
        } catch (Throwable $e) {
            error_log("[BillingEngine::ensureTable] " . $e->getMessage());
        }
    }

    // ── Dynamic document pricing from PricingEngine ──────────────
    private static function getDocCost(string $docType, string $planKey): int {
        try {
            if (!class_exists('PricingEngine')) {
                require_once __DIR__ . '/PricingEngine.php';
            }
            return PricingEngine::getDocumentPrice($docType, $planKey);
        } catch (Throwable $e) {
            // Fallback to legacy TIERS
            return self::TIERS['premium'];
        }
    }

    // ── Check subscription_credits before wallet ──────────────────
    public static function deductDocumentCost(int $userId, string $docType): array {
        $u = db()->prepare("SELECT plan_key FROM users WHERE id=? LIMIT 1");
        $u->execute([$userId]);
        $planKey = $u->fetchColumn() ?: 'free';

        try {
            if (!class_exists('PricingEngine')) {
                require_once __DIR__ . '/PricingEngine.php';
            }
            $costSdg = PricingEngine::getDocumentPrice($docType, $planKey);
        } catch (Throwable $e) {
            $costSdg = self::TIERS['premium'];
        }

        if ($costSdg === 0) {
            return ['ok' => true, 'source' => 'free', 'cost' => 0];
        }

        // 1. Try subscription_credits first
        try {
            $cr = db()->prepare(
                "SELECT id, remaining_sdg FROM subscription_credits
                 WHERE user_id=? AND period_month=MONTH(NOW())
                 AND period_year=YEAR(NOW()) AND remaining_sdg >= ?
                 LIMIT 1 FOR UPDATE"
            );
            $cr->execute([$userId, $costSdg]);
            $credit = $cr->fetch();

            if ($credit) {
                db()->prepare(
                    "UPDATE subscription_credits
                     SET used_sdg=used_sdg+?, remaining_sdg=remaining_sdg-?
                     WHERE id=?"
                )->execute([$costSdg, $costSdg, $credit['id']]);

                // Log wallet transaction
                db()->prepare(
                    "INSERT INTO wallet_transactions
                     (wallet_id,user_id,type,amount_sdg,balance_after,ref_type,description)
                     SELECT w.id,?,?,?,w.balance,'credit_deduct',?
                     FROM wallet w WHERE w.user_id=? LIMIT 1"
                )->execute([$userId,'debit',$costSdg,
                    "خصم رصيد وثيقة: {$docType}",
                    $userId]);

                return ['ok'=>true,'source'=>'credit','cost'=>$costSdg,'plan'=>$planKey];
            }
        } catch (Throwable $e) {
            error_log("[BE::deductDoc] credit check: " . $e->getMessage());
        }

        // 2. Fallback to wallet
        try {
            $w = db()->prepare(
                "SELECT id, balance FROM wallet WHERE user_id=? AND is_active=1 LIMIT 1 FOR UPDATE"
            );
            $w->execute([$userId]);
            $wallet = $w->fetch();

            if (!$wallet || $wallet['balance'] < $costSdg) {
                return ['ok'=>false,'source'=>'none','cost'=>$costSdg,
                        'message'=>'رصيد غير كافٍ لإصدار هذه الوثيقة'];
            }

            $newBal = $wallet['balance'] - $costSdg;
            db()->prepare("UPDATE wallet SET balance=? WHERE id=?")->execute([$newBal,$wallet['id']]);
            db()->prepare(
                "INSERT INTO wallet_transactions
                 (wallet_id,user_id,type,amount_sdg,balance_after,ref_type,description)
                 VALUES (?,?,?,?,?,?,?)"
            )->execute([$wallet['id'],$userId,'debit',$costSdg,$newBal,'doc_issuance',
                        "إصدار وثيقة: {$docType}"]);

            return ['ok'=>true,'source'=>'wallet','cost'=>$costSdg,'balance_after'=>$newBal];
        } catch (Throwable $e) {
            error_log("[BE::deductDoc] wallet: " . $e->getMessage());
            return ['ok'=>false,'error'=>$e->getMessage()];
        }
    }

    // ── Check AI daily limit from platform_settings ───────────────
    public static function checkAILimit(int $userId, string $planKey='free'): array {
        try {
            // Read from PricingEngine (admin-controlled via platform_settings)
            if (!class_exists('PricingEngine')) require_once __DIR__.'/PricingEngine.php';
            $dailyLimit = PricingEngine::getAILimit($planKey);

            // Count today's requests
            $count = db()->prepare(
                "SELECT COUNT(*) FROM execution_log
                 WHERE user_id=? AND DATE(created_at)=CURDATE()"
            );
            $count->execute([$userId]);
            $used = (int)$count->fetchColumn();

            if ($used >= $dailyLimit) {
                // Check wallet for pay-per-request
                if (!class_exists('PricingEngine')) require_once __DIR__.'/PricingEngine.php';
                $pricing = PricingEngine::getAll();
                $costPerReq = $pricing['documents']['ai_chat'][$planKey] ?? 500;

                return [
                    'ok'       => false,
                    'limit'    => $dailyLimit,
                    'used'     => $used,
                    'exceeded' => true,
                    'cost_sdg' => $costPerReq,
                    'message'  => "تجاوزت حد {$dailyLimit} سؤال/يوم",
                ];
            }

            return ['ok'=>true,'limit'=>$dailyLimit,'used'=>$used,'remaining'=>$dailyLimit-$used];
        } catch (Throwable $e) {
            return ['ok'=>true,'limit'=>999,'used'=>0,'remaining'=>999]; // fail open
        }
    }


    /**
     * holdFunds(): حجز مبلغ في المحفظة
     */
    public static function holdFunds(int $userId, float $amount, string $service): array {
        try {
            $w = db()->prepare("SELECT id, balance FROM wallet WHERE user_id=? FOR UPDATE");
            $w->execute([$userId]);
            $wallet = $w->fetch();
            if (!$wallet || $wallet['balance'] < $amount) {
                return ['ok'=>false, 'balance'=>(float)($wallet['balance']??0)];
            }
            db()->prepare(
                "UPDATE wallet SET hold_balance=COALESCE(hold_balance,0)+? WHERE user_id=?"
            )->execute([$amount, $userId]);
            return ['ok'=>true];
        } catch (Throwable $e) {
            return ['ok'=>false, 'error'=>$e->getMessage()];
        }
    }


    // ── deductCredits(): خصم رصيد AI ─────────────────────────
    public static function deductCredits(int $userId, int $credits = 1, string $reason = 'ai_query'): bool {
        try {
            $r = db()->prepare("SELECT ai_credits_remaining FROM users WHERE id=? LIMIT 1");
            $r->execute([$userId]);
            $remaining = (int)($r->fetchColumn() ?? 0);
            if ($remaining < $credits) return false;
            db()->prepare("UPDATE users SET ai_credits_remaining=ai_credits_remaining-? WHERE id=?")->execute([$credits,$userId]);
            db()->prepare("INSERT INTO ai_usage (user_id,event_type,tokens_used,cost_usd,service,created_at) VALUES (?, ?, 0, 0, ?, NOW())")->execute([$userId, 'credit_debit', $reason]);
            return true;
        } catch (\Throwable $e) { return true; } // fail open
    }


    /**
     * Check user's daily booking limit (Bridge method)
     */
    public static function checkBookingLimit(int $userId, string $planKey = 'free'): array {
        if (!class_exists('PricingEngine')) require_once __DIR__.'/PricingEngine.php';
        $limit = PricingEngine::getBookingLimit($planKey);
        
        try {
            $st = db()->prepare(
                "SELECT COUNT(*) FROM bookings
                 WHERE user_id=? AND DATE(created_at)=CURDATE()"
            );
            $st->execute([$userId]);
            $used = (int)$st->fetchColumn();
        } catch (\Throwable $e) { $used = 0; }
        
        return [
            'ok'       => $used < $limit,
            'exceeded' => $used >= $limit,
            'used'     => $used,
            'limit'    => $limit,
            'plan'     => $planKey,
        ];
    }

}