<?php
/**
 * YiyoPay PHP SDK — v1
 *
 * A single-file client for the YiyoPay REST API. No Composer required —
 * copy this file into your project, `require_once` it, and go.
 *
 * INSTALL
 *   1. Save this file anywhere: e.g. lib/YiyoPay.php
 *   2. In your app: require_once __DIR__ . '/lib/YiyoPay.php';
 *   3. Set the API key (from Developers → API Keys) in an env var.
 *
 * QUICK EXAMPLE
 *   $yp = new YiyoPay(getenv('YIYOPAY_KEY'));
 *
 *   // Collect 5,000 UGX from a customer's phone
 *   $r = $yp->collect(5000, '0772000000', 'Order #1001', 'order-1001');
 *   echo $r['payment']['yiyopay_ref'];       // YPCOL...
 *
 *   // Pay a supplier
 *   $r = $yp->disburse(50000, '0700000000', 'Supplier INV-089');
 *
 *   // Check payment status
 *   $r = $yp->status('YPCOL2508259A3B1C');
 *   if ($r['payment']['status'] === 'successful') { ... }
 *
 *   // Verify an incoming webhook
 *   $body = file_get_contents('php://input');
 *   $sig  = $_SERVER['HTTP_X_YIYOPAY_SIGNATURE'] ?? '';
 *   if (YiyoPay::verify_webhook($body, $sig, getenv('YIYOPAY_WEBHOOK_SECRET'))) {
 *       $event = json_decode($body, true);
 *       // handle $event['event'] and $event['data']
 *   }
 *
 * REQUIREMENTS
 *   PHP 7.2+ with cURL extension.
 *
 * @license   MIT
 */

if (!class_exists('YiyoPay')) {

class YiyoPay {

    protected $base;
    protected $key;
    protected $timeout;

    /**
     * @param string $key      API key from Developers → API Keys (sk_live_* or sk_sandbox_*)
     * @param string $base     Base URL of the YiyoPay install
     * @param int    $timeout  cURL timeout in seconds (default 30)
     */
    public function __construct($key, $base = 'https://yiyopay.com', $timeout = 30) {
        if (empty($key))              throw new InvalidArgumentException('API key is required');
        if (!extension_loaded('curl')) throw new RuntimeException('PHP cURL extension not loaded');
        $this->key     = $key;
        $this->base    = rtrim($base, '/');
        $this->timeout = (int)$timeout;
    }

    // =========================================================================
    // HIGH-LEVEL HELPERS — the common cases in one call each
    // =========================================================================

    /**
     * Collect money from a customer. Sends a mobile-money PIN prompt to their
     * phone via MTN or Airtel (auto-detected from the prefix).
     *
     * @param int|float   $amount    Amount in shillings (e.g. 10000 = 10,000 UGX)
     * @param string      $phone     Payer phone: '07XXXXXXXX' or '2567XXXXXXXX'
     * @param string      $purpose   Free-text description shown on your dashboard
     * @param string|null $idem_key  Optional idempotency key (safe retries)
     * @return array                 ['payment' => [...]]
     * @throws YiyoPayException
     */
    public function collect($amount, $phone, $purpose = '', $idem_key = null) {
        return $this->request('POST', 'payments', [
            'amount'      => $amount,
            'payer_phone' => $phone,
            'purpose'     => $purpose,
        ], $idem_key);
    }

    /**
     * Pay money out to any MTN or Airtel wallet.
     *
     * @param int|float   $amount    Amount in shillings
     * @param string      $phone     Recipient phone
     * @param string      $purpose   Free-text description
     * @param string|null $idem_key  Optional idempotency key
     * @return array                 ['disbursement' => [...]]
     * @throws YiyoPayException
     */
    public function disburse($amount, $phone, $purpose = '', $idem_key = null) {
        return $this->request('POST', 'disbursements', [
            'amount'          => $amount,
            'recipient_phone' => $phone,
            'purpose'         => $purpose,
        ], $idem_key);
    }

    /** Fetch the current status of a payment by its yiyopay_ref. */
    public function status($ref) {
        return $this->request('GET', 'payments/' . rawurlencode($ref));
    }

    /**
     * Create a hosted-checkout payment link. Send the returned URL to the
     * customer via email, SMS, WhatsApp, or a Pay button on your site.
     */
    public function create_link($title, $amount = null, $description = '') {
        return $this->request('POST', 'payment-links', [
            'type'        => $amount ? 'fixed' : 'open',
            'title'       => $title,
            'amount'      => $amount,
            'description' => $description,
        ]);
    }

    /** Create a customer record you can reference on future payments. */
    public function create_customer($name, $phone, $email = null, $reference = null) {
        return $this->request('POST', 'customers', [
            'name'      => $name,
            'phone'     => $phone,
            'email'     => $email,
            'reference' => $reference,
        ]);
    }

    /** List all customers under this API key's organisation. */
    public function list_customers($search = null) {
        $qs = $search ? ('?q=' . rawurlencode($search)) : '';
        return $this->request('GET', 'customers' . $qs);
    }

    // =========================================================================
    // LOW-LEVEL — escape hatch for anything the helpers don't cover
    // =========================================================================

    public function request($method, $path, $body = null, $idem_key = null) {
        $url = $this->base . '/api/v1/' . ltrim($path, '/');
        $headers = [
            'Authorization: Bearer ' . $this->key,
            'Content-Type: application/json',
            'Accept: application/json',
        ];
        if ($idem_key !== null && $idem_key !== '') {
            $headers[] = 'Idempotency-Key: ' . $idem_key;
        }

        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_CUSTOMREQUEST  => strtoupper($method),
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER     => $headers,
            CURLOPT_POSTFIELDS     => ($body !== null) ? json_encode($body) : null,
            CURLOPT_TIMEOUT        => $this->timeout,
            CURLOPT_CONNECTTIMEOUT => 10,
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
        ]);
        $raw  = curl_exec($ch);
        $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $err  = curl_error($ch);
        curl_close($ch);

        if ($raw === false) {
            throw new YiyoPayException('Network error: ' . $err, 0);
        }
        $decoded = json_decode($raw, true);
        if (!is_array($decoded)) {
            throw new YiyoPayException('Invalid JSON response (HTTP ' . $code . ')', $code, $raw);
        }
        if ($code >= 400 || !empty($decoded['success']) === false && isset($decoded['success']) && $decoded['success'] === false) {
            throw new YiyoPayException(
                $decoded['message'] ?? ('HTTP ' . $code),
                $code,
                $raw,
                $decoded['data'] ?? []
            );
        }
        return $decoded['data'] ?? [];
    }

    // =========================================================================
    // WEBHOOK SIGNATURE VERIFICATION (static — no instance needed)
    // =========================================================================

    /**
     * Verify a YiyoPay webhook signature. Reject the request if this returns false.
     *
     * The X-Yiyopay-Signature header looks like:  t=1735048800,v1=abc123...
     * The signed payload is:  {t}.{raw_body}
     *
     * @param string $body_raw    Raw request body (from php://input)
     * @param string $sig_header  Contents of the X-Yiyopay-Signature header
     * @param string $secret      Your webhook signing secret from Developers → Webhooks
     * @param int    $tolerance   Max age of the timestamp in seconds (default 300)
     */
    public static function verify_webhook($body_raw, $sig_header, $secret, $tolerance = 300) {
        if (!preg_match('/t=(\d+),v1=([a-f0-9]+)/', (string)$sig_header, $m)) {
            return false;
        }
        list($_, $ts, $sig) = $m;
        if (abs(time() - (int)$ts) > $tolerance) {
            return false;
        }
        $expected = hash_hmac('sha256', $ts . '.' . $body_raw, $secret);
        return hash_equals($expected, $sig);
    }
}

class YiyoPayException extends Exception {
    public $raw_response;
    public $data;
    public function __construct($msg, $code = 0, $raw = null, $data = []) {
        parent::__construct($msg, (int)$code);
        $this->raw_response = $raw;
        $this->data         = $data;
    }
}

} // end class_exists guard
