<?php
// myfatoorah.php — thin wrapper around MyFatoorah v3 API.
//
// Three functions:
//   mf_create_payment()      — POST /v3/payments → returns PaymentURL + InvoiceId
//   mf_get_payment_status()  — GET  /v3/payments/{paymentId} → returns Invoice + Transaction
//   mf_verify_connection()   — quick credentials probe used by the admin Test button
//
// All three return a status array shaped like:
//   ['ok' => bool, 'data' => mixed | null, 'error' => string | null,
//    'http_code' => int, 'raw' => string]
//
// We don't throw on API errors — we return the structured result so
// callers (place_order.php, the callback, the admin settings page)
// can decide how to surface the failure to the customer/admin.

require_once __DIR__ . '/config.php';

/**
 * Resolve the base URL for MyFatoorah's API based on the configured
 * environment + country. Test environment is universal; live is
 * country-specific (Kuwait/Bahrain/Jordan/Oman share one host,
 * UAE/SA/Qatar/Egypt have their own).
 */
function mf_base_url(string $environment, string $country): string
{
    if ($environment === 'test') return 'https://apitest.myfatoorah.com';
    // Live URLs per MyFatoorah country mapping:
    return match (strtoupper($country)) {
        'AE'  => 'https://api-ae.myfatoorah.com',
        'SAU' => 'https://api-sa.myfatoorah.com',
        'QAT' => 'https://api-qa.myfatoorah.com',
        'EGY' => 'https://api-eg.myfatoorah.com',
        // Kuwait, Bahrain, Jordan, Oman → default
        default => 'https://api.myfatoorah.com',
    };
}

/**
 * Read MyFatoorah credentials from the settings table.
 * Cached in a static so we don't re-query on multiple API calls
 * within the same request.
 */
function mf_credentials(): array
{
    static $cache = null;
    if ($cache !== null) return $cache;

    $rows = db()->query("SELECT key_name, value FROM settings WHERE key_name IN
        ('myfatoorah_token', 'myfatoorah_environment', 'myfatoorah_country')")
        ->fetchAll(PDO::FETCH_KEY_PAIR);

    $cache = [
        'token'       => trim($rows['myfatoorah_token'] ?? ''),
        'environment' => $rows['myfatoorah_environment'] ?? 'test',
        'country'     => $rows['myfatoorah_country'] ?? 'KWT',
    ];
    return $cache;
}

/**
 * Generic HTTP helper — POSTs or GETs to MyFatoorah with proper
 * auth headers, parses the JSON response, returns a normalized
 * structure.
 */
function mf_request(string $method, string $path, array $body = null): array
{
    $cred = mf_credentials();
    if ($cred['token'] === '') {
        return ['ok' => false, 'error' => 'No MyFatoorah token configured', 'http_code' => 0, 'raw' => '', 'data' => null];
    }
    $url = mf_base_url($cred['environment'], $cred['country']) . $path;

    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 25,
        CURLOPT_CONNECTTIMEOUT => 8,
        CURLOPT_HTTPHEADER     => [
            'Authorization: Bearer ' . $cred['token'],
            'Content-Type: application/json',
            'Accept: application/json',
        ],
    ]);
    if ($method === 'POST') {
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body ?? new stdClass()));
    } elseif ($method !== 'GET') {
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
    }

    $raw      = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $curlErr  = curl_error($ch);
    curl_close($ch);

    if ($raw === false) {
        return ['ok' => false, 'error' => 'Network error: ' . $curlErr,
                'http_code' => 0, 'raw' => '', 'data' => null];
    }
    $decoded = json_decode($raw, true);
    if (!is_array($decoded)) {
        return ['ok' => false, 'error' => 'Non-JSON response (HTTP ' . $httpCode . ')',
                'http_code' => $httpCode, 'raw' => $raw, 'data' => null];
    }

    // MyFatoorah v3 wraps everything in {IsSuccess, Message, Data, ValidationErrors}.
    // Treat IsSuccess=false as an error regardless of HTTP code.
    if (isset($decoded['IsSuccess']) && $decoded['IsSuccess'] === false) {
        $errMsg = $decoded['Message'] ?? 'Unknown error';
        if (!empty($decoded['ValidationErrors']) && is_array($decoded['ValidationErrors'])) {
            $details = array_map(fn($v) => is_array($v) ? implode(': ', $v) : (string)$v,
                                 $decoded['ValidationErrors']);
            $errMsg .= ' — ' . implode('; ', $details);
        }
        return ['ok' => false, 'error' => $errMsg, 'http_code' => $httpCode,
                'raw' => $raw, 'data' => $decoded['Data'] ?? null];
    }

    return ['ok' => true, 'data' => $decoded['Data'] ?? null,
            'http_code' => $httpCode, 'raw' => $raw, 'error' => null];
}

/**
 * Create a payment session for an order.
 *
 * @param string $method      MyFatoorah method code: "KNET" or "CARD"
 * @param float  $amount      total in merchant's base currency (e.g. KWD)
 * @param string $callbackUrl absolute URL MF will redirect to (must be HTTPS)
 * @param array  $orderMeta   optional: customer name, mobile, email, reference
 *
 * @return array status; on success Data has PaymentURL, InvoiceId
 */
function mf_create_payment(string $method, float $amount,
                           string $callbackUrl, array $orderMeta = []): array
{
    $body = [
        'PaymentMethod' => strtoupper($method),
        'Order' => [
            'Amount' => round($amount, 3),
        ],
        'IntegrationUrls' => [
            'Redirection' => $callbackUrl,
        ],
    ];

    // Optional customer info — populated when we have it. MyFatoorah
    // pre-fills the payment page with these so the customer doesn't
    // have to retype them.
    //
    // Important formatting requirements from MyFatoorah:
    //  - Mobile must NOT include the country code; use MobileCountryCode
    //    separately. Sending "+96550123456" in Mobile is rejected with
    //    "Invalid data - request.Customer.Mobile".
    //  - Name must be "two-part" (e.g. "Test Customer", not just "Test").
    //    A single word is rejected on some test environments.
    //  - All three fields (Name, Mobile, Email) are individually optional.
    //    We omit any that look malformed rather than fail the whole call.
    $customerBlock = [];
    if (!empty($orderMeta['customer_name'])) {
        $name = trim($orderMeta['customer_name']);
        // If the name is a single word, append a placeholder space-token
        // so it's accepted. Customer can update on the MF page if needed.
        if (!str_contains($name, ' ')) $name .= ' .';
        $customerBlock['Name'] = $name;
    }
    // ── Mobile field ─────────────────────────────────────────
    // We do NOT send Mobile to MyFatoorah, even though we collect
    // it from the customer in our own checkout form.
    //
    // Reason: MyFatoorah's validator (at least in the test
    // environment) rejects every Mobile format we've tried with
    // an empty error message — see ValidationErrors[].Error = "".
    // Their docs say Mobile is optional; we tested without it and
    // the request succeeds cleanly. Their hosted payment page will
    // collect the customer's mobile itself if they want an SMS
    // receipt — so we lose nothing functionally by omitting it.
    //
    // We continue to store the phone (with country code from the
    // picker) in our own orders table and use it for WhatsApp
    // links — that part of the flow is unchanged.
    //
    // If MyFatoorah's production environment requires Mobile, we
    // can re-enable this branch later. For now, omitting is safest.
    /* DISABLED — see comment above
    if (!empty($orderMeta['customer_country_code']) && !empty($orderMeta['customer_phone_local'])) {
        $cc    = preg_replace('/\D/', '', (string)$orderMeta['customer_country_code']);
        $local = preg_replace('/\D/', '', (string)$orderMeta['customer_phone_local']);
        if ($cc !== '' && $local !== '' && strlen($local) >= 6 && strlen($local) <= 15) {
            $customerBlock['MobileCountryCode'] = $cc;
            $customerBlock['Mobile']            = $local;
        }
    } elseif (!empty($orderMeta['customer_mobile'])) {
        $rawMobile = trim($orderMeta['customer_mobile']);
        $matched = false;
        $knownPrefixes = ['965', '966', '971', '973', '974', '968', '20', '962', '961', '963', '964', '967'];
        foreach ($knownPrefixes as $cc) {
            $prefix = '+' . $cc;
            if (str_starts_with($rawMobile, $prefix)) {
                $local = substr($rawMobile, strlen($prefix));
                if (preg_match('/^\d{6,15}$/', $local)) {
                    if ($cc === '965' && strlen($local) !== 8) break;
                    $customerBlock['MobileCountryCode'] = $cc;
                    $customerBlock['Mobile']            = $local;
                    $matched = true;
                    break;
                }
            }
        }
        if (!$matched && preg_match('/^\+(\d{1,4}?)(\d{6,15})$/', $rawMobile, $m)) {
            $customerBlock['MobileCountryCode'] = $m[1];
            $customerBlock['Mobile']            = $m[2];
        }
    }
    */
    if (!empty($orderMeta['customer_email'])) {
        $customerBlock['Email'] = $orderMeta['customer_email'];
    }
    if (!empty($customerBlock)) {
        $body['Customer'] = $customerBlock;
    }

    // Reference shown in MyFatoorah dashboard so the merchant can
    // cross-reference back to our order_number (e.g. "LTK-1042").
    if (!empty($orderMeta['order_number'])) {
        $body['Order']['Reference'] = $orderMeta['order_number'];
    }

    // Special case: ALL_GATEWAYS routes through the v2 SendPayment
    // endpoint, which returns an "invoice URL" that displays every
    // enabled gateway in the merchant's portal. The /v3/payments
    // endpoint requires a specific PaymentMethod (KNET, CARD, etc.)
    // and would skip the picker.
    //
    // SendPayment uses a different request shape, so we transform
    // the v3-style body into the v2-style fields it expects, and
    // normalize the response back to the {PaymentURL, InvoiceId}
    // shape callers depend on.
    if (strtoupper($method) === 'ALL_GATEWAYS') {
        $sendBody = [
            'InvoiceValue'      => $body['Order']['Amount'],
            'NotificationOption' => 'Lnk',  // returns invoice URL only, no SMS/email
            'CustomerName'      => $body['Customer']['Name'] ?? 'Customer',
            'CallBackUrl'       => $callbackUrl,
            'ErrorUrl'          => $callbackUrl,  // same callback handles both outcomes
            'Language'          => 'en',
        ];
        if (!empty($body['Customer']['MobileCountryCode'])) $sendBody['MobileCountryCode'] = $body['Customer']['MobileCountryCode'];
        if (!empty($body['Customer']['Mobile']))            $sendBody['CustomerMobile']    = $body['Customer']['Mobile'];
        if (!empty($body['Customer']['Email']))             $sendBody['CustomerEmail']     = $body['Customer']['Email'];
        if (!empty($orderMeta['order_number']))             $sendBody['CustomerReference'] = $orderMeta['order_number'];

        $r = mf_request('POST', '/v2/SendPayment', $sendBody);
        if ($r['ok']) {
            // Map SendPayment's response field names to the same
            // shape callers expect: data.PaymentURL + data.InvoiceId
            $r['data'] = [
                'PaymentURL' => $r['data']['InvoiceURL'] ?? '',
                'InvoiceId'  => $r['data']['InvoiceId']  ?? '',
            ];
        }
        return $r;
    }

    return mf_request('POST', '/v3/payments', $body);
}

/**
 * Inquire payment status — used after MF redirects back with paymentId.
 * Returns the full Data block which includes:
 *   Data.Invoice.Status      ("PAID", "UNPAID", "EXPIRED", etc.)
 *   Data.Transaction.Status  ("SUCCESS", "FAILED")
 *   Data.Amount, Data.Customer, Data.Card …
 */
function mf_get_payment_status(string $paymentId): array
{
    if ($paymentId === '') {
        return ['ok' => false, 'error' => 'Empty paymentId',
                'http_code' => 0, 'raw' => '', 'data' => null];
    }
    return mf_request('GET', '/v3/payments/' . urlencode($paymentId));
}

/**
 * Test that the configured token works at all. Used by the admin
 * "Test connection" button. We don't have a dedicated ping endpoint,
 * so we attempt a tiny ExecutePayment-style request and look for
 * an auth-related response. A 401 means the token is wrong; any
 * non-401 (even a validation error) means auth succeeded.
 *
 * Specifically: we POST a minimal payment for 1 KWD with a method
 * we know exists ("CARD"). We expect either success or a
 * payment-method-disabled error — both prove auth works. Only a
 * 401 or token-related message indicates broken credentials.
 */
function mf_verify_connection(): array
{
    $cred = mf_credentials();
    if ($cred['token'] === '') {
        return ['ok' => false, 'error' => 'No token configured. Paste your MyFatoorah API token in Settings.'];
    }

    // Try a minimal create-payment call with throwaway URLs.
    $r = mf_request('POST', '/v3/payments', [
        'PaymentMethod' => 'CARD',
        'Order' => ['Amount' => 1.0],
        'IntegrationUrls' => ['Redirection' => 'https://example.com/cb'],
    ]);

    if ($r['ok']) {
        return [
            'ok'   => true,
            'env'  => $cred['environment'],
            'note' => 'Token works. Test payment URL would be: ' . ($r['data']['PaymentURL'] ?? '(none)'),
        ];
    }

    // Auth failures explicitly: 401, "token", "unauthor", "permission"
    $msg = strtolower($r['error'] ?? '');
    if ($r['http_code'] === 401 || str_contains($msg, 'token') || str_contains($msg, 'unauthor') || str_contains($msg, 'permission')) {
        return ['ok' => false, 'error' => 'Authentication failed: ' . $r['error']];
    }

    // Any other error means auth succeeded but something else failed
    // — that's still a "connection works" signal for our purposes.
    return [
        'ok'   => true,
        'env'  => $cred['environment'],
        'note' => 'Token authenticated. Server responded with: ' . $r['error'],
    ];
}
