<?php

namespace App\Controllers;

use CodeIgniter\API\ResponseTrait;
use CodeIgniter\RESTful\ResourceController;
use App\Models\BuilderTicketModel;
use App\Models\TransactionModel;
use App\Models\TipsUnlockModel;
use App\Models\TipModel;
use App\Controllers\TransactionChecker;

class Payments extends ResourceController
{
    use ResponseTrait;

    protected $format = 'json';

    /**
     * Process payment transaction
     * 
     * @return \CodeIgniter\HTTP\Response JSON response with transaction result
     */
    public function process()
    {
        $this->logRequest('payment_process_started');

        try {
            $data = $this->request->getJSON(true);

            // Validate required fields for both tips and subscriptions
            if (empty($data['msisdn']) || empty($data['amount']) || empty($data['transactionId']) || empty($data['naration'])) {
                throw new \InvalidArgumentException('Missing required fields', 422);
            }

            // If this is a subscription, plan_id is required
            if (!empty($data['is_subscription']) && empty($data['plan_id'])) {
                throw new \InvalidArgumentException('plan_id is required for subscriptions', 422);
            }

            $txnModel = new TransactionModel();
            $db = \Config\Database::connect();

            // Prepare transaction data
            $transactionData = [
                'msisdn' => $data['msisdn'],
                'amount' => $data['amount'],
                'transaction_id' => $data['transactionId'],
                'naration' => $data['naration'],
                'payment_method' => 'cash',
                'status' => 'pending',
                'tip_id' => $data['tip_id'] ?? null,
                'plan_id' => $data['plan_id'] ?? null,
                'builder_ticket_id' => $data['builder_ticket_id'] ?? null,
                'tip_unlocked' => 0
            ];

            $transactionId = $txnModel->insert($transactionData);

            // Prepare data for mobile money API (payload unchanged; only URL via config)
            $msisdn = $data['msisdn'];
            if (!str_starts_with($msisdn, '+')) {
                $msisdn = '+' . $msisdn;
            }

            $apiData = [
                'msisdn' => $msisdn,
                'amount' => (float) $data['amount'],
            ];

            $payUrl = config('MobileMoney')->getPayUrl();
            log_message('info', "[PAYMENT] Calling MM API: {$payUrl}");

            $client = \Config\Services::curlrequest();
            $response = $client->post($payUrl, [
                'headers' => [
                    'Content-Type' => 'application/json',
                    'Accept' => 'application/json',
                ],
                'body' => json_encode($apiData),
                'timeout' => 30,
                'http_errors' => false,
                'verify' => false,
            ]);

            $httpCode = $response->getStatusCode();
            $responseData = json_decode($response->getBody(), true);

            log_message('info', "[PAYMENT] MM API HTTP {$httpCode} | body: " . json_encode($responseData));

            // Require 2xx from mobile money API
            if ($httpCode < 200 || $httpCode >= 300) {
                $msg = is_array($responseData) ? ($responseData['message'] ?? $responseData['error'] ?? $response->getReasonPhrase()) : $response->getReasonPhrase();
                log_message('error', "[PAYMENT] MM API error: HTTP {$httpCode} – {$msg}");
                return $this->respond([
                    'success' => false,
                    'message' => 'Mobile money request failed: ' . $msg,
                    'data' => ['local_id' => $transactionId],
                ], $httpCode >= 500 ? 502 : 400);
            }

            // If API returns success: false in body, treat as failure
            if (isset($responseData['success']) && $responseData['success'] === false) {
                $msg = $responseData['message'] ?? $responseData['error'] ?? 'Mobile money rejected the request';
                log_message('error', "[PAYMENT] MM API success=false: {$msg}");
                return $this->respond([
                    'success' => false,
                    'message' => $msg,
                    'data' => ['local_id' => $transactionId],
                ], 400);
            }

            if (isset($responseData['reference'])) {
                log_message('info', "[PAYMENT] Updating transaction ID {$transactionId} with reference: " . $responseData['reference']);
                $txnModel->update($transactionId, ['transaction_id' => $responseData['reference']]);
            } else {
                log_message('warning', "[PAYMENT] No reference in MM response for transaction ID {$transactionId}");
            }

            return $this->respond([
                'success' => true,
                'message' => 'Transaction initiated',
                'data' => [
                    'local_id' => $transactionId,
                    'external_response' => $responseData,
                ],
            ]);
        } catch (\Exception $e) {
            $this->logError('payment_processing_error', $e);
            return $this->handleException($e);
        }
    }

    /**
     * GET: Return status for a transaction by local id (used by app to poll).
     * POST: Handle payment status callback from mobile money provider.
     *
     * @param string|null $id For GET: local DB id (primary key) of the transaction
     * @return \CodeIgniter\HTTP\Response
     */
    public function status($id = null)
    {
        log_message('debug', 'Payment status: method=' . $this->request->getMethod() . ' id=' . ($id ?? 'null'));

        // GET with id = app polling by local_id (DB primary key)
        if ($this->request->getMethod() === 'get' && $id !== null && $id !== '') {
            return $this->respond($this->getStatusByLocalId((string) $id));
        }

        if ($this->request->getMethod() !== 'post') {
            return $this->response->setStatusCode(405)
                ->setHeader('Allow', 'GET, POST')
                ->setJSON([
                    'success' => false,
                    'error' => 'Method Not Allowed. Use GET with /status/{local_id} to poll, or POST for callback.',
                    'method' => $this->request->getMethod(),
                ]);
        }

        $this->logRequest('status_callback_received');

        try {
            // Get raw input and log it
            $rawInput = $this->request->getBody();
            $this->logRequest('status_callback_raw_input', ['raw_input' => $rawInput]);

            // Parse JSON input
            $callbackData = json_decode($rawInput, true);
            if (json_last_error() !== JSON_ERROR_NONE) {
                throw new \InvalidArgumentException('Invalid JSON in request body: ' . json_last_error_msg(), 400);
            }

            // Log the parsed data
            $this->logRequest('status_callback_parsed', ['parsed_data' => $callbackData]);

            // Validate callback data
            if (!isset($callbackData['transaction']) || !isset($callbackData['status'])) {
                throw new \InvalidArgumentException('Missing required callback parameters', 422);
            }

            if (!isset($callbackData['transaction']['transaction_id']) || !isset($callbackData['transaction']['msisdn'])) {
                throw new \InvalidArgumentException('Missing required transaction details', 422);
            }

            // Initialize models and database
            $txnModel = new TransactionModel();
            $db = \Config\Database::connect();

            // Start transaction
            $db->transStart();

            // Process the callback
            $result = $this->processCallback($txnModel, $callbackData);

            // Commit transaction
            $db->transComplete();

            if ($db->transStatus() === false) {
                throw new \RuntimeException('Database transaction failed');
            }

            $response = [
                'success' => true,
                'message' => 'Transaction status processed',
                'transaction_id' => $callbackData['transaction']['transaction_id'],
                'status' => $result['status'],
                'internal_status' => $result['internal_status'],
                'tip_unlocked' => $result['tip_unlocked']
            ];

            $this->logRequest('status_callback_response', $response);

            return $this->respond($response);

        } catch (\Exception $e) {
            $this->logError('status_callback_error', $e);
            return $this->handleException($e);
        }
    }

    /**
     * Get transactions by phone number
     * 
     * @return \CodeIgniter\HTTP\Response JSON response with transactions list
     */
    public function getByPhone()
    {
        $this->logRequest('get_transactions_by_phone');

        try {
            $phone = $this->normalizeCustomerMsisdn($this->validatePhoneNumber());
            $transactions = $this->fetchTransactions($phone);

            return $this->respond([
                'success' => true,
                'data' => $this->formatTransactionsResponse($transactions)
            ]);

        } catch (\Exception $e) {
            $this->logError('get_transactions_error', $e);
            return $this->handleException($e);
        }
    }

    /**
     * Record crypto payment and notify Discord.
     * POST api/payments/crypto
     * Body: { "tip_id": int, "amount_ugx": number, "usdt_amount": number, "tx_hash": string }
     */
    public function crypto()
    {
        $this->logRequest('crypto_payment_started');

        try {
            $data = $this->request->getJSON(true);
            if (empty($data['tip_id']) || !isset($data['amount_ugx']) || empty($data['tx_hash'])) {
                throw new \InvalidArgumentException('tip_id, amount_ugx and tx_hash are required', 422);
            }

            $tipId = (int) $data['tip_id'];
            $amountUgx = (float) ($data['amount_ugx'] ?? 0);
            $usdtAmount = isset($data['usdt_amount']) ? (float) $data['usdt_amount'] : ($amountUgx / $this->getUgxUsdtRate());
            $txHash = trim((string) $data['tx_hash']);
            if ($txHash === '') {
                throw new \InvalidArgumentException('tx_hash is required', 422);
            }

            $tipModel = new TipModel();
            $tip = $tipModel->find($tipId);
            if (!$tip) {
                throw new \RuntimeException('Tip not found', 404);
            }

            $txnModel = new TransactionModel();
            $txnId = $txnModel->insert([
                'msisdn' => 'crypto',
                'amount' => (int) round($amountUgx),
                'transaction_id' => $txHash,
                'naration' => 'Crypto USDT',
                'payment_method' => 'crypto',
                'status' => 0, // pending
                'tip_id' => $tipId,
                'tip_unlocked' => 0,
            ]);

            if (!$txnId) {
                throw new \RuntimeException('Failed to save crypto transaction', 500);
            }

            $this->sendCryptoDiscordNotification([
                'tip_id' => $tipId,
                'tip_title' => $tip['title'] ?? 'N/A',
                'amount_ugx' => $amountUgx,
                'usdt_amount' => $usdtAmount,
                'tx_hash' => $txHash,
                'trc20_address' => $this->getTrc20Address(),
            ]);

            return $this->respond([
                'success' => true,
                'message' => 'Crypto payment recorded and under review.',
                'data' => ['id' => $txnId, 'transaction_id' => $txHash],
            ]);
        } catch (\Exception $e) {
            $this->logError('crypto_payment_error', $e);
            return $this->handleException($e);
        }
    }

    /**
     * FinFlo hosted checkout: create session and return pay_url.
     * POST api/payments/finflo/create-session
     * Body: { "tip_id": int, "amount": number } OR { "builder_ticket_id": int, "amount": number }
     */
    public function finfloCreateSession()
    {
        $this->logRequest('finflo_create_session');

        try {
            $data = $this->request->getJSON(true);
            $tipId = !empty($data['tip_id']) ? (int) $data['tip_id'] : null;
            $builderTicketId = !empty($data['builder_ticket_id']) ? (int) $data['builder_ticket_id'] : null;

            if (($tipId === null && $builderTicketId === null) || ($tipId !== null && $builderTicketId !== null)) {
                throw new \InvalidArgumentException('Provide exactly one of tip_id or builder_ticket_id', 422);
            }

            $amount = 0;
            $naration = '';
            $ourRef = '';
            $txnInsert = [
                'msisdn' => 'finflo',
                'payment_method' => 'finflo',
                'status' => 'pending',
                'tip_unlocked' => 0,
            ];

            if ($builderTicketId !== null) {
                $ticketModel = new BuilderTicketModel();
                $ticket = $ticketModel->find($builderTicketId);
                if (!$ticket) {
                    throw new \RuntimeException('Bet Builder ticket not found', 404);
                }
                if (($ticket['status'] ?? '') !== 'pending') {
                    throw new \RuntimeException('Bet Builder ticket is not pending payment', 422);
                }
                $amount = (int) $ticket['amount'];
                $naration = 'FinFlo – Bet Builder ticket #' . $builderTicketId;
                $ourRef = 'FFB-' . $builderTicketId . '-' . time();
                $txnInsert['amount'] = $amount;
                $txnInsert['transaction_id'] = $ourRef;
                $txnInsert['naration'] = $naration;
                $txnInsert['builder_ticket_id'] = $builderTicketId;
                $txnInsert['tip_id'] = null;
                $customerPhone = $this->normalizeCustomerMsisdn((string) ($ticket['msisdn'] ?? ''));
                if ($customerPhone !== '') {
                    $txnInsert['customer_msisdn'] = $customerPhone;
                }
            } else {
                if (!isset($data['amount']) || (float) $data['amount'] <= 0) {
                    throw new \InvalidArgumentException('tip_id and positive amount are required', 422);
                }
                $amount = (int) round((float) $data['amount']);
                $tipModel = new TipModel();
                $tip = $tipModel->find($tipId);
                if (!$tip) {
                    throw new \RuntimeException('Tip not found', 404);
                }
                $naration = 'FinFlo – ' . ($tip['title'] ?? 'Tip ' . $tipId);
                $ourRef = 'FF-' . $tipId . '-' . time();
                $txnInsert['amount'] = $amount;
                $txnInsert['transaction_id'] = $ourRef;
                $txnInsert['naration'] = $naration;
                $txnInsert['tip_id'] = $tipId;
                if (!empty($data['msisdn'])) {
                    $customerPhone = $this->normalizeCustomerMsisdn((string) $data['msisdn']);
                    if ($customerPhone !== '') {
                        $txnInsert['customer_msisdn'] = $customerPhone;
                    }
                }
            }

            $config = config('FinFlo');
            $apiKey = $config->getApiKey();
            if ($apiKey === '') {
                throw new \RuntimeException('FinFlo API key not configured', 500);
            }

            $baseUrl = rtrim(config('App')->baseURL, '/');
            $callbackUrl = $baseUrl . '/api/payments/finflo/callback?ref=' . urlencode($ourRef);
            $cancelUrl = $baseUrl . '/api/payments/finflo/cancel';

            $txnModel = new TransactionModel();
            $txnId = $txnModel->insert($txnInsert);
            if (!$txnId) {
                throw new \RuntimeException('Failed to save FinFlo session transaction', 500);
            }

            $client = \Config\Services::curlrequest();
            $createUrl = $config->getCreateSessionUrl();
            $body = [
                'app_slug' => $config->appSlug,
                'amount' => $amount,
                'reference' => $ourRef,
                'callback_url' => $callbackUrl,
                'cancel_url' => $cancelUrl,
            ];

            log_message('info', '[FinFlo] Create session request: ' . json_encode($body));

            $response = $client->post($createUrl, [
                'headers' => [
                    'X-API-Key' => $apiKey,
                    'Content-Type' => 'application/json',
                    'Accept' => 'application/json',
                ],
                'body' => json_encode($body),
                'timeout' => 15,
                'http_errors' => false,
            ]);

            $httpCode = $response->getStatusCode();
            $responseData = json_decode($response->getBody(), true);
            log_message('info', '[FinFlo] Create session response ' . $httpCode . ': ' . $response->getBody());

            if ($httpCode < 200 || $httpCode >= 300) {
                $msg = is_array($responseData) ? ($responseData['message'] ?? $response->getReasonPhrase()) : $response->getReasonPhrase();
                throw new \RuntimeException('FinFlo create-session failed: ' . $msg, 502);
            }

            if (empty($responseData['status']) || $responseData['status'] !== 'success' || empty($responseData['data']['pay_url'])) {
                throw new \RuntimeException('Invalid FinFlo session response', 502);
            }

            return $this->respond([
                'success' => true,
                'message' => 'FinFlo session created',
                'data' => [
                    'pay_url' => $responseData['data']['pay_url'],
                    'reference' => $ourRef,
                    'session_id' => $responseData['data']['session_id'] ?? null,
                ],
            ]);
        } catch (\Exception $e) {
            $this->logError('finflo_create_session_error', $e);
            return $this->handleException($e);
        }
    }

    /**
     * FinFlo hosted checkout: redirect target after payment.
     * GET api/payments/finflo/callback?ref=...&tx_ref=...&status=...&amount=...
     */
    public function finfloCallback()
    {
        $this->logRequest('finflo_callback');

        $ref = $this->request->getGet('ref');
        $txRef = $this->request->getGet('tx_ref');
        $status = $this->request->getGet('status');
        $amount = $this->request->getGet('amount');

        if (empty($ref)) {
            return $this->redirectToFinfloSuccess(null, false, 'Missing reference');
        }

        $txnModel = new TransactionModel();
        $transaction = $txnModel->where('transaction_id', $ref)->where('payment_method', 'finflo')->first();
        if (!$transaction) {
            log_message('error', '[FinFlo] Callback: transaction not found for ref=' . $ref);
            return $this->redirectToFinfloSuccess($txRef, false, 'Transaction not found');
        }

        $normalized = strtoupper((string) $status);
        if ($normalized !== 'COMPLETED') {
            $txnModel->update($transaction['id'], ['status' => 'failed', 'updated_at' => date('Y-m-d H:i:s')]);
            return $this->redirectToFinfloSuccess($txRef, false, 'Payment was not completed');
        }

        $msisdn = 'finflo-' . ($txRef ?: $ref);
        $customerPhone = $this->resolveCustomerMsisdnForTransaction($transaction);

        $txnUpdate = [
            'status' => 'success',
            'tip_unlocked' => !empty($transaction['tip_id']) ? 1 : 0,
            'msisdn' => $msisdn,
            'updated_at' => date('Y-m-d H:i:s'),
        ];
        if ($customerPhone !== '' && empty($transaction['customer_msisdn'])) {
            $txnUpdate['customer_msisdn'] = $customerPhone;
        }
        $txnModel->update($transaction['id'], $txnUpdate);

        if (!empty($transaction['builder_ticket_id'])) {
            $ticketModel = new BuilderTicketModel();
            $ticketModel->markPaid(
                (int) $transaction['builder_ticket_id'],
                $txRef ?: $ref
            );
        }

        if (!empty($transaction['tip_id'])) {
            $this->ensureFinfloTipUnlock((int) $transaction['tip_id'], $msisdn, $txRef ?: $ref, $customerPhone);

            // Create tip_sales so author earnings are reflected (same as cash flow)
            $tipSalesModel = new \App\Models\TipSaleModel();
            $existingSale = $tipSalesModel->where([
                'msisdn' => $msisdn,
                'tip_id' => $transaction['tip_id'],
            ])->first();
            if (!$existingSale) {
                $tipModel = new TipModel();
                $tip = $tipModel->find($transaction['tip_id']);
                $author = $tip['author'] ?? null;
                if ($author) {
                    $tipSalesModel->insert([
                        'tip_id' => $transaction['tip_id'],
                        'author' => $author,
                        'msisdn' => $msisdn,
                        'amount' => $transaction['amount'],
                        'created_at' => date('Y-m-d H:i:s'),
                        'tag' => 'FinFlo',
                    ]);
                }
            }
        }

        return $this->redirectToFinfloSuccess($txRef ?: $ref, true);
    }

    /**
     * Redirect to FinFlo success/cancel page (HTML).
     */
    private function redirectToFinfloSuccess(?string $txRef, bool $success, string $message = ''): \CodeIgniter\HTTP\Response
    {
        $baseUrl = rtrim(config('App')->baseURL, '/');
        $url = $baseUrl . '/api/payments/finflo/success?tx_ref=' . urlencode((string) $txRef) . '&ok=' . ($success ? '1' : '0');
        if ($message !== '') {
            $url .= '&message=' . urlencode($message);
        }
        return redirect()->to($url);
    }

    /**
     * FinFlo success/cancel page (HTML) after redirect.
     * GET api/payments/finflo/success?tx_ref=...&ok=1|0&message=...
     */
    public function finfloSuccess()
    {
        $txRef = $this->request->getGet('tx_ref');
        $ok = (int) $this->request->getGet('ok');
        $message = (string) $this->request->getGet('message');
        $success = $ok === 1;

        $html = '<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>FinFlo Payment</title></head><body style="font-family:sans-serif;max-width:480px;margin:2rem auto;padding:1rem;text-align:center;">';
        if ($success) {
            $html .= '<h2 style="color:green;">Payment successful</h2><p>Your FinFlo reference: <strong>' . htmlspecialchars($txRef ?: '—') . '</strong></p><p>Return to the Bet Tips app and go to <strong>Track Tips</strong>. Enter your <strong>phone number</strong> (easiest) or this FinFlo reference to retrieve your tip.</p>';
        } else {
            $html .= '<h2 style="color:#b00;">Payment cancelled or failed</h2>';
            if ($message !== '') {
                $html .= '<p>' . htmlspecialchars($message) . '</p>';
            }
            $html .= '<p>You can try again from the app.</p>';
        }
        $html .= '</body></html>';

        return $this->response->setBody($html)->setHeader('Content-Type', 'text/html; charset=utf-8');
    }

    /**
     * FinFlo cancel redirect (optional).
     * GET api/payments/finflo/cancel
     */
    public function finfloCancel()
    {
        $baseUrl = rtrim(config('App')->baseURL, '/');
        return redirect()->to($baseUrl . '/api/payments/finflo/success?ok=0&message=Cancelled');
    }

    /**
     * Get payments by FinFlo reference (e.g. tx_ref from success page).
     * GET api/payments/by-ref?ref=TX-xxx
     */
    public function getByRef()
    {
        $this->logRequest('get_transactions_by_ref');

        try {
            $ref = $this->request->getGet('ref');
            if (empty($ref) || !is_string($ref)) {
                throw new \InvalidArgumentException('ref is required', 422);
            }
            $ref = trim($ref);
            $msisdn = str_starts_with($ref, 'finflo-') ? $ref : 'finflo-' . $ref;
            $transactions = $this->fetchTransactions($msisdn);

            return $this->respond([
                'success' => true,
                'data' => $this->formatTransactionsResponse($transactions),
            ]);
        } catch (\Exception $e) {
            $this->logError('get_transactions_by_ref_error', $e);
            return $this->handleException($e);
        }
    }

    /**
     * Get crypto payments by transaction hash.
     * GET api/payments/by-hash?hash=...
     */
    public function getByHash()
    {
        $this->logRequest('get_transactions_by_hash');

        try {
            $hash = $this->request->getGet('hash');
            if (empty($hash) || !is_string($hash)) {
                throw new \InvalidArgumentException('hash is required', 422);
            }
            $hash = trim($hash);

            $txnModel = new TransactionModel();
            $transactions = $txnModel->where('payment_method', 'crypto')
                ->where('transaction_id', $hash)
                ->orderBy('created_at', 'DESC')
                ->findAll();

            $tipModel = new TipModel();
            $unlockModel = new TipsUnlockModel();
            $list = [];
            foreach ($transactions as $txn) {
                $tip = $txn['tip_id'] ? $tipModel->find($txn['tip_id']) : null;
                $unlockRecord = $txn['tip_id'] ? $unlockModel->where('transaction_id', $hash)->where('tip_id', $txn['tip_id'])->first() : null;
                $unlocked = $unlockRecord && (int) ($unlockRecord['unlock_status'] ?? 0) === 1;

                $list[] = [
                    'id' => $txn['id'],
                    'transaction_id' => $txn['transaction_id'],
                    'amount' => $txn['amount'],
                    'status' => $this->normalizeStatus($txn['status']),
                    'created_at' => $txn['created_at'],
                    'tip_id' => $txn['tip_id'],
                    'tip' => $tip ? [
                        'id' => $tip['id'],
                        'title' => $tip['title'] ?? null,
                        'author' => $tip['author'] ?? null,
                    ] : null,
                    'unlocked' => $unlocked,
                ];
            }

            return $this->respond([
                'success' => true,
                'data' => [
                    'transactions' => $list,
                    'summary' => [
                        'total_count' => count($list),
                    ],
                ],
            ]);
        } catch (\Exception $e) {
            $this->logError('get_by_hash_error', $e);
            return $this->handleException($e);
        }
    }

    private function getUgxUsdtRate(): float
    {
        return config('Crypto')->getUgxUsdtRate();
    }

    private function getTrc20Address(): string
    {
        return config('Crypto')->getTrc20Address();
    }

    private function sendCryptoDiscordNotification(array $payload): void
    {
        $url = $this->getDiscordWebhookUrl();
        if ($url === '' || $url === '0') {
            log_message('warning', '[PAYMENT] Crypto Discord webhook URL not set, skipping notification.');
            return;
        }

        $title = $payload['tip_title'] ?? 'Crypto payment';
        $amountUgx = $payload['amount_ugx'] ?? 0;
        $usdt = $payload['usdt_amount'] ?? 0;
        $txHash = $payload['tx_hash'] ?? '';
        $addr = $payload['trc20_address'] ?? '';

        $content = "**Crypto payment recorded**\n"
            . "Tip: {$title} (ID: " . ($payload['tip_id'] ?? '') . ")\n"
            . "Amount: " . number_format($amountUgx) . " UGX / {$usdt} USDT\n"
            . "TX Hash: `{$txHash}`\n"
            . "TRC20 Address: `{$addr}`";

        $body = json_encode(['content' => $content]);
        if ($body === false) {
            log_message('error', '[PAYMENT] Discord webhook: JSON encode failed.');
            return;
        }

        $client = \Config\Services::curlrequest();
        try {
            $response = $client->post($url, [
                'headers' => ['Content-Type' => 'application/json'],
                'body' => $body,
                'timeout' => 15,
                'http_errors' => false,
                'allow_redirects' => false,
            ]);
            $statusCode = $response->getStatusCode();
            if ($statusCode < 200 || $statusCode >= 300) {
                $responseBody = (string) $response->getBody();
                log_message('error', '[PAYMENT] Discord webhook failed. HTTP ' . $statusCode . ' Body: ' . $responseBody);
            }
        } catch (\Throwable $e) {
            log_message('error', '[PAYMENT] Discord webhook exception: ' . $e->getMessage());
        }
    }

    private function getDiscordWebhookUrl(): string
    {
        try {
            $config = config('Crypto');
            if ($config === null) {
                return '';
            }
            return $config->getDiscordWebhookUrl();
        } catch (\Throwable $e) {
            log_message('error', '[PAYMENT] Crypto config load failed: ' . $e->getMessage());
            return '';
        }
    }

    /**
     * Check tip unlock status
     * 
     * @param string $msisdn User's phone number
     * @param int $tipId Tip ID
     * @return \CodeIgniter\HTTP\Response JSON response with unlock status
     */
    public function unlockStatus($msisdn, $tipId)
    {
        $this->logRequest('check_unlock_status', ['msisdn' => $msisdn, 'tip_id' => $tipId]);

        try {
            $unlocked = $this->isTipUnlockedForIdentifier($msisdn, (int) $tipId);

            return $this->respond([
                'unlocked' => $unlocked,
                'status' => $unlocked ? 1 : 0,
                'message' => $unlocked ? 'Unlock status retrieved' : 'No unlock record found',
            ]);

        } catch (\Exception $e) {
            $this->logError('unlock_status_error', $e);
            return $this->handleException($e);
        }
    }

    /**
     * Unlock a tip for a user
     * 
     * @param int $tipId Tip ID to unlock
     * @return \CodeIgniter\HTTP\Response JSON response with unlock result
     */
    public function unlockTip($tipId)
    {
        $this->logRequest('unlock_tip_request', ['tip_id' => $tipId]);

        try {
            $data = $this->validateUnlockRequest();
            $result = $this->processTipUnlock($tipId, $data);

            return $this->respond([
                'success' => true,
                'message' => $result['message'],
                'data' => ['tip_id' => $tipId]
            ]);

        } catch (\Exception $e) {
            $this->logError('unlock_tip_error', $e);
            return $this->handleException($e);
        }
    }

    /**
     * Helper Methods
     */

    /**
     * Get transaction status by local DB id (primary key). Used by app when polling.
     */
    private function getStatusByLocalId(string $localId): array
    {
        $txnModel = new TransactionModel();
        $txn = $txnModel->find((int) $localId);
        if (!$txn) {
            return [
                'success' => false,
                'status' => 'unknown',
                'tip_unlocked' => false,
                'message' => 'Transaction not found',
            ];
        }
        $status = $this->normalizeStatus($txn['status']);
        return [
            'success' => true,
            'status' => $status,
            'tip_unlocked' => (bool) ($txn['tip_unlocked'] ?? 0),
            'message' => $status === 'success' ? 'Payment completed' : ($status === 'failed' ? 'Payment failed' : 'Processing'),
        ];
    }

    private function validatePaymentRequest(): array
    {
        $data = $this->request->getJSON(true);

        $requiredFields = ['msisdn', 'amount', 'transactionId', 'naration'];
        $missingFields = array_diff($requiredFields, array_keys($data));

        if (!empty($missingFields)) {
            throw new \InvalidArgumentException('Missing required fields: ' . implode(', ', $missingFields), 422);
        }

        if (!is_numeric($data['amount']) || $data['amount'] <= 0) {
            throw new \InvalidArgumentException('Amount must be a positive number', 422);
        }

        return $data;
    }

    private function prepareTransactionData(array $data): array
    {
        return [
            'msisdn' => $data['msisdn'],
            'amount' => $data['amount'],
            'transaction_id' => $data['transactionId'],
            'naration' => $data['naration'],
            'status' => 'pending', // Using string status
            'tip_id' => $data['tip_id'] ?? null,
            'tip_unlocked' => 0
        ];
    }

    private function saveTransaction(TransactionModel $model, array $data): int
    {
        $transactionId = $model->insert($data);

        if (!$transactionId) {
            $errors = $model->errors();
            throw new \RuntimeException('Failed to save transaction: ' . json_encode($errors), 500);
        }

        return $transactionId;
    }

    private function processExternalPayment(array $data, int $transactionId): array
    {
        $msisdn = $data['msisdn'];
        if (!str_starts_with($msisdn, '+')) {
            $msisdn = '+' . $msisdn;
        }
        $apiData = [
            'msisdn' => $msisdn,
            'amount' => (float) $data['amount'],
        ];
        $jsonPayload = json_encode($apiData);
        log_message('debug', "[PAYMENT] Final API payload: " . $jsonPayload);

        $client = \Config\Services::curlrequest();
        $payUrl = config('MobileMoney')->getPayUrl();
        $response = $client->post($payUrl, [
            'headers' => [
                'Content-Type' => 'application/json',
                'Accept' => 'application/json',
            ],
            'body' => $jsonPayload,
            'timeout' => 30,
            'http_errors' => false,
            'verify' => false,
        ]);


        if ($response->getStatusCode() != 200) {
            $errorMessage = 'API returned status: ' . $response->getStatusCode();
            log_message('error', '[PAYMENT] ' . $errorMessage);
            throw new \RuntimeException($errorMessage, 502);
        }

        $responseData = json_decode($response->getBody(), true);

        if (!$responseData) {
            $errorMessage = 'Invalid JSON response from API';
            log_message('error', '[PAYMENT] ' . $errorMessage);
            throw new \RuntimeException($errorMessage, 502);
        }
        
        // Log API response for debugging
        log_message('info', "[PAYMENT] API Response: " . json_encode($responseData));
        
        // Update transaction with the reference from new API
        if (isset($responseData['reference'])) {
            log_message('info', "[PAYMENT] Updating transaction ID {$transactionId} with reference: " . $responseData['reference']);
            $txnModel = new TransactionModel();
            $txnModel->update($transactionId, ['transaction_id' => $responseData['reference']]);
        } else {
            log_message('warning', "[PAYMENT] No reference in API response for transaction ID {$transactionId}");
        }

        // Log successful API response
        $this->logRequest('payment_api_response', [
            'status_code' => $response->getStatusCode(),
            'has_response_data' => !empty($responseData)
        ]);

        return $responseData;
    }

    private function validateCallbackRequest(): array
    {
        $callbackData = $this->request->getJSON(true);

        if (!isset($callbackData['msisdn'], $callbackData['transaction_id'])) {
            throw new \InvalidArgumentException('Missing required fields', 422);
        }

        return $callbackData;
    }

    private function processCallback(TransactionModel $model, array $callbackData): array
    {
        $transactionId = $callbackData['transaction']['transaction_id'];
        $msisdn = $callbackData['transaction']['msisdn'];
        $externalStatus = strtoupper($callbackData['status']);

        $transaction = $model->where([
            'transaction_id' => $transactionId,
            'msisdn' => $msisdn
        ])->first();

        if (!$transaction) {
            throw new \RuntimeException("Transaction not found in DB: $transactionId for msisdn: $msisdn", 404);
        }

        $updateData = ['updated_at' => date('Y-m-d H:i:s')];
        $tipUnlocked = false;

        // Determine status based on external status
        if ($externalStatus === 'SUCCESS') {
            $updateData['status'] = 'success';

            if (!empty($transaction['tip_id'])) {
                $updateData['tip_unlocked'] = 1;
                $tipUnlocked = $this->createTipUnlockRecord($transaction, $transactionId);

                if (!$tipUnlocked) {
                    throw new \RuntimeException('Failed to create tip unlock record', 500);
                }

                // --- Create tip_sales record if not already exists ---
                $tipSalesModel = new \App\Models\TipSaleModel();
                $existingSale = $tipSalesModel->where([
                    'msisdn' => $transaction['msisdn'],
                    'tip_id' => $transaction['tip_id'],
                ])->first();
                if (!$existingSale) {
                    // Get author from tips
                    $tipModel = new \App\Models\TipModel();
                    $tip = $tipModel->find($transaction['tip_id']);
                    $author = $tip['author'] ?? null;
                    if ($author) {
                        $tipSalesModel->insert([
                            'tip_id' => $transaction['tip_id'],
                            'author' => $author,
                            'msisdn' => $transaction['msisdn'],
                            'amount' => $transaction['amount'],
                            'created_at' => date('Y-m-d H:i:s'),
                            'tag' => 'Used Cash',
                        ]);
                    }
                }
                // --- End tip_sales record creation ---
            }

            // --- Mark bet-builder ticket as paid on success ---
            if (!empty($transaction['builder_ticket_id'])) {
                $ticketModel = new \App\Models\BuilderTicketModel();
                $ticketModel->markPaid(
                    (int) $transaction['builder_ticket_id'],
                    $transactionId
                );
            }
            // --- End bet-builder fulfillment ---
        } elseif (in_array($externalStatus, ['FAILED', 'FAILE'])) { // MM API uses "FAILE" not "FAILED"
            $updateData['status'] = 'failed';
        } else {
            $updateData['status'] = 'pending';
        }

        if (!$model->update($transaction['id'], $updateData)) {
            throw new \RuntimeException('Failed to update transaction status', 500);
        }

        return [
            'status' => $externalStatus,
            'internal_status' => $updateData['status'],
            'tip_unlocked' => $tipUnlocked
        ];
    }

    private function createTipUnlockRecord(array $transaction, string $transactionId): bool
    {
        $model = new TipsUnlockModel();

        // Check if already unlocked
        $existing = $model->where([
            'msisdn' => $transaction['msisdn'],
            'tip_id' => $transaction['tip_id']
        ])->first();

        if ($existing) {
            return true;
        }

        $unlockData = [
            'msisdn' => $transaction['msisdn'],
            'tip_id' => $transaction['tip_id'],
            'transaction_id' => $transactionId,
            'unlock_status' => 1,
            'created_at' => date('Y-m-d H:i:s')
        ];

        return (bool) $model->insert($unlockData);
    }

    private function validatePhoneNumber(): string
    {
        $phone = $this->request->getGet('phone');

        if (empty($phone)) {
            throw new \InvalidArgumentException('Phone number (msisdn) is required', 422);
        }

        return $phone;
    }

    private function fetchTransactions(string $phone): array
    {
        return $this->fetchTransactionsForCustomer($phone);
    }

    /**
     * All transactions for a customer phone: cash/MM, PesaPal, FinFlo (via customer_msisdn),
     * and Bet Builder payments linked through builder_tickets.msisdn.
     */
    private function fetchTransactionsForCustomer(string $phone): array
    {
        $phone = $this->normalizeCustomerMsisdn($phone);
        if ($phone === '') {
            return [];
        }

        $model = new TransactionModel();
        $byMsisdn = $model->where('msisdn', $phone)->findAll();

        $byCustomer = [];
        if ($model->db->fieldExists('customer_msisdn', 'transactions')) {
            $byCustomer = $model->where('customer_msisdn', $phone)->findAll();
        }

        $byBuilder = [];
        $ticketModel = new BuilderTicketModel();
        $ticketIds = $ticketModel->where('msisdn', $phone)->findColumn('id');
        if (!empty($ticketIds)) {
            $byBuilder = $model->whereIn('builder_ticket_id', array_map('intval', $ticketIds))->findAll();
        }

        $merged = [];
        foreach (array_merge($byMsisdn, $byCustomer, $byBuilder) as $txn) {
            $merged[(int) $txn['id']] = $txn;
        }

        usort($merged, static function (array $a, array $b): int {
            return strcmp((string) ($b['created_at'] ?? ''), (string) ($a['created_at'] ?? ''));
        });

        return array_values($merged);
    }

    private function normalizeCustomerMsisdn(string $phone): string
    {
        $phone = trim($phone);
        if ($phone === '') {
            return '';
        }

        if (str_starts_with(strtolower($phone), 'finflo-')) {
            return $phone;
        }

        $digits = preg_replace('/\D+/', '', $phone) ?? '';
        if ($digits === '') {
            return $phone;
        }

        if (str_starts_with($digits, '256')) {
            return $digits;
        }
        if (str_starts_with($digits, '0')) {
            $digits = substr($digits, 1);
        }

        return strlen($digits) >= 9 ? '256' . substr($digits, -9) : $phone;
    }

    private function resolveCustomerMsisdnForTransaction(array $transaction): string
    {
        $existing = $this->normalizeCustomerMsisdn((string) ($transaction['customer_msisdn'] ?? ''));
        if ($existing !== '' && !str_starts_with(strtolower($existing), 'finflo-')) {
            return $existing;
        }

        if (!empty($transaction['builder_ticket_id'])) {
            $ticketModel = new BuilderTicketModel();
            $ticket = $ticketModel->find((int) $transaction['builder_ticket_id']);
            if ($ticket && !empty($ticket['msisdn'])) {
                return $this->normalizeCustomerMsisdn((string) $ticket['msisdn']);
            }
        }

        return '';
    }

    private function ensureFinfloTipUnlock(int $tipId, string $finfloMsisdn, string $transactionId, string $customerPhone = ''): void
    {
        $unlockModel = new TipsUnlockModel();
        $existing = $unlockModel->where('msisdn', $finfloMsisdn)->where('tip_id', $tipId)->first();
        if (!$existing) {
            $unlockModel->insert([
                'msisdn' => $finfloMsisdn,
                'tip_id' => $tipId,
                'transaction_id' => $transactionId,
                'unlock_status' => 1,
                'created_at' => date('Y-m-d H:i:s'),
            ]);
        }

        if ($customerPhone !== '' && !str_starts_with(strtolower($customerPhone), 'finflo-')) {
            $phoneUnlock = $unlockModel->where('msisdn', $customerPhone)->where('tip_id', $tipId)->first();
            if (!$phoneUnlock) {
                $unlockModel->insert([
                    'msisdn' => $customerPhone,
                    'tip_id' => $tipId,
                    'transaction_id' => $transactionId,
                    'unlock_status' => 1,
                    'created_at' => date('Y-m-d H:i:s'),
                ]);
            }
        }
    }

    private function isTipUnlockedForIdentifier(string $identifier, int $tipId): bool
    {
        $unlockModel = new TipsUnlockModel();
        $record = $unlockModel->where(['msisdn' => $identifier, 'tip_id' => $tipId])->first();
        if ($record && (int) ($record['unlock_status'] ?? 0) === 1) {
            return true;
        }

        if ($this->looksLikeFinfloReference($identifier)) {
            return false;
        }

        $phone = $this->normalizeCustomerMsisdn($identifier);
        if ($phone === '' || str_starts_with(strtolower($phone), 'finflo-')) {
            return false;
        }

        $phoneRecord = $unlockModel->where(['msisdn' => $phone, 'tip_id' => $tipId])->first();
        if ($phoneRecord && (int) ($phoneRecord['unlock_status'] ?? 0) === 1) {
            return true;
        }

        $txnModel = new TransactionModel();
        if (!$txnModel->db->fieldExists('customer_msisdn', 'transactions')) {
            return false;
        }

        $txn = $txnModel->where('customer_msisdn', $phone)
            ->where('tip_id', $tipId)
            ->where('payment_method', 'finflo')
            ->orderBy('created_at', 'DESC')
            ->first();

        if (!$txn) {
            return false;
        }

        $status = $this->normalizeStatus($txn['status'] ?? '');
        return $status === 'success' && (int) ($txn['tip_unlocked'] ?? 0) === 1;
    }

    private function looksLikeFinfloReference(string $value): bool
    {
        $t = trim($value);
        if ($t === '') {
            return false;
        }

        $lower = strtolower($t);
        return str_starts_with($lower, 'finflo-')
            || str_starts_with(strtoupper($t), 'TX-');
    }

    private function formatTransactionsResponse(array $transactions): array
    {
        $response = [
            'transactions' => [],
            'summary' => [
                'total_count' => count($transactions),
                'total_amount_successful' => 0,
                'successful_transactions' => 0,
                'pending_transactions' => 0,
                'failed_transactions' => 0
            ]
        ];

        foreach ($transactions as $txn) {
            $status = $this->normalizeStatus($txn['status']);

            // Update summary
            if ($status === 'success') {
                $response['summary']['total_amount_successful'] += $txn['amount'];
                $response['summary']['successful_transactions']++;
            } elseif ($status === 'pending') {
                $response['summary']['pending_transactions']++;
            } elseif ($status === 'failed') {
                $response['summary']['failed_transactions']++;
            }

            $response['transactions'][] = [
                'id' => $txn['id'],
                'transaction_id' => $txn['transaction_id'],
                'msisdn' => $txn['msisdn'],
                'amount' => $txn['amount'],
                'naration' => $txn['naration'],
                'status' => $status,
                'created_at' => $txn['created_at'],
                'updated_at' => $txn['updated_at'],
                'tip_id' => $txn['tip_id']
            ];
        }

        return $response;
    }

    private function normalizeStatus($status): string
    {
        if (is_numeric($status)) {
            switch ((int) $status) {
                case 0:
                    return 'pending';
                case 1:
                    return 'success';
                case 2:
                    return 'failed';
                default:
                    return 'unknown';
            }
        }

        $status = strtolower((string) $status);

        if (in_array($status, ['success', 'completed']))
            return 'success';
        if (in_array($status, ['fail', 'failed']))
            return 'failed';
        if (in_array($status, ['pending', 'processing']))
            return 'pending';

        return 'unknown';
    }

    private function validateUnlockRequest(): array
    {
        $data = $this->request->getJSON(true);

        if (!isset($data['msisdn'], $data['transaction_id'])) {
            throw new \InvalidArgumentException('Missing required fields', 422);
        }

        return $data;
    }

    private function processTipUnlock(int $tipId, array $data): array
    {
        $txnModel = new TransactionModel();
        $transaction = null;
        $tid = $data['transaction_id'] ?? '';

        // App sends local_id (DB primary key) as transaction_id; support lookup by id when numeric
        if ($tid !== '' && is_numeric($tid)) {
            $row = $txnModel->find((int) $tid);
            if ($row && (string) ($row['msisdn'] ?? '') === (string) ($data['msisdn'] ?? '') && $this->isSuccessStatus($row['status'] ?? null)) {
                $transaction = $row;
            }
        }

        // Fallback: lookup by transaction_id (MM reference) and msisdn; status can be 'success' or 1
        if (!$transaction) {
            $transaction = $txnModel->where([
                'transaction_id' => $data['transaction_id'],
                'msisdn' => $data['msisdn'],
            ])->whereIn('status', ['success', 1, '1'])->first();
        }

        if (!$transaction) {
            throw new \RuntimeException('No successful transaction found with provided details', 404);
        }

        $unlockModel = new TipsUnlockModel();
        $existing = $unlockModel->where([
            'msisdn' => $data['msisdn'],
            'tip_id' => $tipId,
        ])->first();

        if ($existing) {
            return ['message' => 'Tip already unlocked'];
        }

        // Use the transaction's transaction_id (MM reference) in tips_unlock, not the request's value (which may be local_id)
        $unlockModel->insert([
            'msisdn' => $data['msisdn'],
            'tip_id' => $tipId,
            'transaction_id' => $transaction['transaction_id'],
            'unlock_status' => 1,
            'created_at' => date('Y-m-d H:i:s'),
        ]);

        return ['message' => 'Tip unlocked successfully'];
    }

    /** Return true if status indicates success (string 'success' or numeric 1). */
    private function isSuccessStatus($status): bool
    {
        if ($status === 'success' || $status === 1) {
            return true;
        }
        if (is_numeric($status) && (int) $status === 1) {
            return true;
        }
        return false;
    }

    private function logRequest(string $action, array $context = []): void
    {
        $ip = ($this->request && method_exists($this->request, 'getIPAddress')) ? $this->request->getIPAddress() : 'N/A';
        $userAgent = ($this->request && method_exists($this->request, 'getUserAgent')) ? $this->request->getUserAgent()->getAgentString() : 'N/A';
        log_message('info', "[PAYMENT] $action - " . json_encode([
            'ip' => $ip,
            'user_agent' => $userAgent,
            'data' => $context
        ]));
    }

    private function logTransactionData(array $data): void
    {
        log_message('debug', "[PAYMENT] Transaction data - " . json_encode($data));
    }

    private function logError(string $context, \Exception $e): void
    {
        log_message('error', "[PAYMENT] $context - " . json_encode([
            'error' => $e->getMessage(),
            'code' => $e->getCode(),
            'file' => $e->getFile(),
            'line' => $e->getLine(),
            'trace' => $e->getTraceAsString()
        ]));
    }

    /**
     * Handle exceptions in a consistent way
     * 
     * @param \Exception $e The exception to handle
     * @return \CodeIgniter\HTTP\Response
     */
    protected function handleException(\Exception $e)
    {
        $statusCode = 500;

        // Determine appropriate status code based on exception type
        if ($e instanceof \InvalidArgumentException) {
            $statusCode = 400; // Bad Request
        } elseif ($e instanceof \RuntimeException) {
            $statusCode = 500; // Internal Server Error
        }

        // Log the error with full details
        log_message('error', '[Payment Error] ' . $e->getMessage() . "\n" . $e->getTraceAsString());

        // Return JSON response
        return $this->response->setStatusCode($statusCode)
            ->setJSON([
                'success' => false,
                'error' => $e->getMessage(),
                'status_code' => $statusCode
            ]);
    }

    /**
     * POST api/payments/pesapal/register-ipn
     * One-time setup: registers IPN URL with PesaPal via mm.finflo.net.
     * Call this once from Postman/curl — the returned ipn_id is stored on mm.finflo.net.
     */
    public function pesapalRegisterIpn()
    {
        try {
            $client   = \Config\Services::curlrequest();
            $response = $client->post('https://mm.finflo.net/public/api/pesapal/register-ipn', [
                'headers'     => ['Content-Type' => 'application/json', 'Accept' => 'application/json'],
                'body'        => json_encode(['url' => 'https://mm.finflo.net/public/pesapal-ipn']),
                'timeout'     => 15,
                'http_errors' => false,
                'verify'      => false,
            ]);

            $rawBody = $response->getBody();
            $res     = json_decode($rawBody, true);
            log_message('info', '[PesaPal] register-ipn response: ' . $rawBody);

            return $this->respond([
                'success'  => true,
                'message'  => 'IPN registration response from mm.finflo.net',
                'response' => $res,
            ]);
        } catch (\Exception $e) {
            return $this->handleException($e);
        }
    }

    /**
     * POST api/payments/pesapal/create-order
     * Body: { "tip_id": int, "amount": number, "msisdn": string }
     *    OR { "builder_ticket_id": int, "amount": number, "msisdn": string }
     *
     * Calls mm.finflo.net/public/api/pesapal/order, saves pending transaction
     * to bettips DB with the returned order_tracking_id, returns redirect_url.
     */
    public function pesapalCreateOrder()
    {
        $this->logRequest('pesapal_create_order');

        try {
            $data = $this->request->getJSON(true);
            if (empty($data['msisdn'])) {
                throw new \InvalidArgumentException('msisdn is required', 422);
            }

            $tipId = !empty($data['tip_id']) ? (int) $data['tip_id'] : null;
            $builderTicketId = !empty($data['builder_ticket_id']) ? (int) $data['builder_ticket_id'] : null;

            if (($tipId === null && $builderTicketId === null) || ($tipId !== null && $builderTicketId !== null)) {
                throw new \InvalidArgumentException('Provide exactly one of tip_id or builder_ticket_id', 422);
            }

            $msisdn = $data['msisdn'];
            $amount = 0;
            $orderId = '';
            $description = '';
            $naration = '';
            $txnInsert = [
                'msisdn'         => $msisdn,
                'payment_method' => 'pesapal',
                'status'         => 'pending',
                'tip_unlocked'   => 0,
            ];

            if ($builderTicketId !== null) {
                $ticketModel = new BuilderTicketModel();
                $ticket = $ticketModel->find($builderTicketId);
                if (!$ticket) {
                    throw new \RuntimeException('Bet Builder ticket not found', 404);
                }
                if (($ticket['status'] ?? '') !== 'pending') {
                    throw new \RuntimeException('Bet Builder ticket is not pending payment', 422);
                }
                $amount = (int) $ticket['amount'];
                $orderId = 'Bet-Builder-' . $builderTicketId . '-' . time();
                $description = 'Bet Builder ticket #' . $builderTicketId;
                $naration = 'PesaPal – Bet Builder ticket #' . $builderTicketId;
                $txnInsert['amount'] = $amount;
                $txnInsert['naration'] = $naration;
                $txnInsert['builder_ticket_id'] = $builderTicketId;
                $txnInsert['tip_id'] = null;
            } else {
                if (!isset($data['amount']) || (float) $data['amount'] <= 0) {
                    throw new \InvalidArgumentException('tip_id, amount and msisdn are required', 422);
                }
                $amount = (int) round((float) $data['amount']);
                $tipModel = new TipModel();
                $tip = $tipModel->find($tipId);
                if (!$tip) {
                    throw new \RuntimeException('Tip not found', 404);
                }
                $orderId = 'Bet-Tip-' . $tipId . '-' . time();
                $description = 'Payment for ' . ($tip['title'] ?? 'Tip ' . $tipId) . ' #' . $tipId;
                $naration = 'PesaPal – ' . ($tip['title'] ?? 'Tip ' . $tipId);
                $txnInsert['amount'] = $amount;
                $txnInsert['naration'] = $naration;
                $txnInsert['tip_id'] = $tipId;
            }

            $orderUrl = config('PesaPal')->getOrderUrl();

            // Payload exactly matching the confirmed-working Postman format.
            // notification_id is handled internally by mm.finflo.net — do NOT include it.
            $payload = [
                'id'              => $orderId,
                'amount'          => $amount,
                'description'     => $description,
                'billing_address' => [
                    'email_address' => 'tips@bettips.online',
                    'phone_number'  => $msisdn,
                    'country_code'  => 'UG',
                    'first_name'    => 'Bet',
                    'last_name'     => 'Tips',
                ],
            ];

            log_message('info', '[PesaPal] Submitting order to ' . $orderUrl . ': ' . json_encode($payload));

            $client   = \Config\Services::curlrequest();
            $response = $client->post($orderUrl, [
                'headers'     => ['Content-Type' => 'application/json', 'Accept' => 'application/json'],
                'body'        => json_encode($payload),
                'timeout'     => 15,
                'http_errors' => false,
                'verify'      => false,
            ]);

            $httpCode = $response->getStatusCode();
            $rawBody  = $response->getBody();
            $res      = json_decode($rawBody, true);

            log_message('info', '[PesaPal] Response HTTP ' . $httpCode . ': ' . $rawBody);

            if ($httpCode < 200 || $httpCode >= 300 || empty($res['success'])) {
                $msg = is_array($res) ? ($res['message'] ?? $res['error'] ?? $response->getReasonPhrase()) : $response->getReasonPhrase();
                log_message('error', '[PesaPal] Order failed HTTP ' . $httpCode . ': ' . $rawBody);
                throw new \RuntimeException('PesaPal order failed: ' . $msg, 502);
            }

            // Support both known response contracts:
            // 1) top-level fields, 2) nested data fields.
            $redirectUrl = $res['redirect_url']
                ?? ($res['data']['redirect_url'] ?? null);
            $trackingId = $res['order_tracking_id']
                ?? ($res['data']['order_tracking_id'] ?? null);

            if (empty($redirectUrl) || empty($trackingId)) {
                $keys = is_array($res) ? implode(', ', array_keys($res)) : 'non-json-response';
                log_message('error', '[PesaPal] Incomplete response. Keys: ' . $keys . ' Body: ' . $rawBody);
                throw new \RuntimeException('PesaPal returned incomplete response: ' . $rawBody, 502);
            }

            // Save to bettips DB — transaction_id = order_tracking_id so status
            // checks and tip-unlock queries can find it by tracking ID or msisdn
            $txnModel = new TransactionModel();
            $txnInsert['transaction_id'] = $trackingId;
            $txnModel->insert($txnInsert);

            return $this->respond([
                'success' => true,
                'message' => 'PesaPal order created',
                'data'    => [
                    'redirect_url'      => $redirectUrl,
                    'order_tracking_id' => $trackingId,
                ],
            ]);
        } catch (\Exception $e) {
            $this->logError('pesapal_create_order_error', $e);
            return $this->handleException($e);
        }
    }

    /**
     * GET api/payments/pesapal/status/{trackingId}
     *
     * Calls mm.finflo.net/public/api/pesapal/transaction/{id} to get status.
     * If status == SUCCESS, marks the local transaction success and tip_unlocked=1
     * so the user can retrieve their tip via Track Tips using their phone number.
     */
    public function pesapalStatus(string $trackingId)
    {
        $this->logRequest('pesapal_status');

        try {
            $client   = \Config\Services::curlrequest();
            $response = $client->get(config('PesaPal')->getTransactionUrl($trackingId), [
                'headers'     => ['Accept' => 'application/json'],
                'timeout'     => 10,
                'http_errors' => false,
                'verify'      => false,
            ]);

            $httpCode = $response->getStatusCode();
            $rawBody  = $response->getBody();
            $res      = json_decode($rawBody, true);

            log_message('info', '[PesaPal] Transaction status HTTP ' . $httpCode . ': ' . $rawBody);

            if ($httpCode < 200 || $httpCode >= 300 || empty($res['success'])) {
                return $this->respond(['success' => false, 'message' => 'Status check failed'], 400);
            }

            $txnData = is_array($res['data'] ?? null) ? $res['data'] : [];
            $status  = strtoupper((string) ($txnData['status'] ?? $res['status'] ?? ''));
            $gatewayReference = (string) ($txnData['reference'] ?? $txnData['gateway_reference'] ?? $txnData['order_tracking_id'] ?? '');
            $gatewayMsisdn = (string) ($txnData['msisdn'] ?? '');
            $unlockEligible = false;
            $unlockReason = null;

            // On SUCCESS: update bettips DB and unlock the tip
            if ($status === 'SUCCESS') {
                $txnModel = new TransactionModel();
                $localTxn = $txnModel->where('transaction_id', $trackingId)
                                     ->where('payment_method', 'pesapal')
                                     ->first();

                if ($localTxn) {
                    if (!empty($localTxn['builder_ticket_id'])) {
                        $ticketModel = new BuilderTicketModel();
                        $ticketModel->markPaid(
                            (int) $localTxn['builder_ticket_id'],
                            $trackingId
                        );
                        $txnModel->update($localTxn['id'], [
                            'status'       => 'success',
                            'tip_unlocked' => 0,
                        ]);
                        $unlockEligible = true;
                    } else {
                        $referenceMatches = !empty($gatewayReference) && (string) $localTxn['transaction_id'] === $gatewayReference;
                        $msisdnMatches = !empty($gatewayMsisdn) && (string) $localTxn['msisdn'] === $gatewayMsisdn;

                        if ($referenceMatches && $msisdnMatches) {
                            $tipUnlocked = $this->createTipUnlockRecord($localTxn, $trackingId);
                            $txnModel->update($localTxn['id'], [
                                'status'       => 'success',
                                'tip_unlocked' => $tipUnlocked ? 1 : 0,
                            ]);
                            $unlockEligible = true;
                        } else {
                            $unlockReason = 'reference_or_msisdn_mismatch';
                            log_message(
                                'error',
                                '[PesaPal] SUCCESS but unlock blocked due to mismatch. '
                                . 'tracking_id=' . $trackingId
                                . ', db_reference=' . ($localTxn['transaction_id'] ?? 'n/a')
                                . ', api_reference=' . ($gatewayReference !== '' ? $gatewayReference : 'n/a')
                                . ', db_msisdn=' . ($localTxn['msisdn'] ?? 'n/a')
                                . ', api_msisdn=' . ($gatewayMsisdn !== '' ? $gatewayMsisdn : 'n/a')
                            );
                        }
                    }
                } else {
                    $unlockReason = 'local_transaction_not_found';
                    log_message('error', '[PesaPal] SUCCESS but local transaction not found for tracking_id=' . $trackingId);
                }
            }

            return $this->respond([
                'success' => true,
                'status'  => $status,
                'data'    => $txnData,
                'unlock_eligible' => $unlockEligible,
                'unlock_reason' => $unlockReason,
            ]);
        } catch (\Exception $e) {
            $this->logError('pesapal_status_error', $e);
            return $this->handleException($e);
        }
    }
}