<?php
/**
 * Ellidiya Business API — PHP client (single file, no dependencies, PHP 7.4+ with curl).
 *
 *   require 'ellidiya-business.php';
 *   $ell = new EllidiyaBusiness('ek_live_...');
 *   $msg = $ell->sendTemplate('+919876543210', 'order_shipped', ['Arjun', 'MS-1042'], 'order-1042');
 *   echo $msg['id'], ' ', $msg['status'];
 *
 * Webhook (in your endpoint):
 *   $body = file_get_contents('php://input');
 *   if (! EllidiyaBusiness::verifyWebhook($body, $_SERVER['HTTP_X_ELLIDIYA_SIGNATURE'] ?? '', 'whsec_...')) { http_response_code(400); exit; }
 *   $event = json_decode($body, true);   // ['event' => 'message.received', 'data' => [...]]
 *
 * Docs: https://ecom.ellidiya.com/docs.html
 */
class EllidiyaBusinessError extends Exception
{
    public $errorCode;
    public $httpStatus;

    public function __construct(string $message, string $code, int $status)
    {
        parent::__construct($message, $status);
        $this->errorCode = $code;
        $this->httpStatus = $status;
    }
}

class EllidiyaBusiness
{
    private $key;
    private $base;
    private $timeout;

    public function __construct(string $apiKey, string $baseUrl = 'https://api.ellidiya.com/api/v1', int $timeout = 15)
    {
        if (! preg_match('/^ek_live_[A-Za-z0-9]{40}$/', $apiKey)) {
            throw new InvalidArgumentException('That does not look like an Ellidiya API key (ek_live_...).');
        }
        $this->key = $apiKey;
        $this->base = rtrim($baseUrl, '/');
        $this->timeout = $timeout;
    }

    /** Your business: name, daily limit, credits. */
    public function me(): array
    {
        return $this->request('GET', '/me')['business'];
    }

    /** Approved templates. */
    public function templates(string $status = 'approved'): array
    {
        return $this->request('GET', '/templates'.($status === 'all' ? '?status=all' : ''))['templates'];
    }

    /**
     * Send an approved template. $to is a mobile number (+91...) or a customer id (cus_...).
     * $clientRef (optional) makes retries safe: the same ref is never sent twice.
     */
    public function sendTemplate(string $to, string $template, array $params = [], ?string $clientRef = null, ?string $language = null): array
    {
        $body = (strpos($to, 'cus_') === 0 ? ['customer' => $to] : ['to' => $to]) + ['template' => array_filter(['name' => $template, 'params' => array_values($params), 'language' => $language], fn ($v) => $v !== null)];
        if ($clientRef !== null) {
            $body['client_ref'] = $clientRef;
        }

        return $this->request('POST', '/messages', $body)['message'];
    }

    /** Free text: only within 24 hours of the customer's last message to you. */
    public function sendText(string $to, string $text, ?string $clientRef = null): array
    {
        $body = (strpos($to, 'cus_') === 0 ? ['customer' => $to] : ['to' => $to]) + ['text' => $text];
        if ($clientRef !== null) {
            $body['client_ref'] = $clientRef;
        }

        return $this->request('POST', '/messages', $body)['message'];
    }

    /** One message's status: sent, delivered, read or failed. $id is like "msg_123". */
    public function message(string $id): array
    {
        return $this->request('GET', '/messages/'.rawurlencode($id))['message'];
    }

    /** Replies from customers after a cursor (use the returned "next" for the following call). */
    public function replies(?string $after = null, int $limit = 50): array
    {
        $q = http_build_query(array_filter(['direction' => 'in', 'after' => $after, 'limit' => $limit]));

        return $this->request('GET', '/messages?'.$q);
    }

    /** Send up to 1000 messages in one call; they go out at your pace. Each item: ['to' or 'customer', 'template' => [...] or 'text', 'client_ref']. */
    public function sendBatch(array $messages, ?string $name = null): array
    {
        return $this->request('POST', '/messages/batch', array_filter(['name' => $name, 'messages' => array_values($messages)], fn ($v) => $v !== null))['batch'];
    }

    /** Progress of a batch: sent, failed (with reasons), queued. */
    public function batch(string $id): array
    {
        return $this->request('GET', '/batches/'.rawurlencode($id))['batch'];
    }

    /** Submit a template for review. Track it with templates('all'). */
    public function createTemplate(string $name, string $category, string $body, array $example = [], array $buttons = [], string $language = 'en'): array
    {
        return $this->request('POST', '/templates', compact('name', 'category', 'body', 'example', 'buttons', 'language'))['template'];
    }

    /** Where Ellidiya posts this business's events. Returns ['webhook_url' => ..., 'signing_secret' => ...]. */
    public function setWebhook(string $url): array
    {
        return $this->request('POST', '/webhook', ['url' => $url]);
    }

    /** Partners: swap the code from "Connect with Ellidiya" for the business's API key. */
    public static function exchangeCode(string $clientId, string $clientSecret, string $code, string $redirectUri, string $baseUrl = 'https://api.ellidiya.com/api/v1'): array
    {
        $ch = curl_init(rtrim($baseUrl, '/').'/partner/token');
        curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15,
            CURLOPT_HTTPHEADER => ['Accept: application/json', 'Content-Type: application/json', 'User-Agent: ellidiya-business-php/1'],
            CURLOPT_POSTFIELDS => json_encode(['client_id' => $clientId, 'client_secret' => $clientSecret, 'code' => $code, 'redirect_uri' => $redirectUri])]);
        $raw = curl_exec($ch);
        $status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
        curl_close($ch);
        $j = is_string($raw) ? json_decode($raw, true) : null;
        if (! is_array($j) || empty($j['ok'])) {
            throw new EllidiyaBusinessError($j['error']['message'] ?? 'Code exchange failed', $j['error']['code'] ?? 'error', $status);
        }

        return $j; // ['api_key' => 'ek_live_...', 'business' => [...], 'connection_id' => ...]
    }

    /** Check X-Ellidiya-Signature ("t=<unix>,v1=<hex>") against the raw body. Rejects events older than 5 minutes. */
    public static function verifyWebhook(string $rawBody, string $signatureHeader, string $secret, int $toleranceSeconds = 300): bool
    {
        if (! preg_match('/t=(\d+),v1=([0-9a-f]{64})/', $signatureHeader, $m)) {
            return false;
        }
        if (abs(time() - (int) $m[1]) > $toleranceSeconds) {
            return false;
        }

        return hash_equals(hash_hmac('sha256', $m[1].'.'.$rawBody, $secret), $m[2]);
    }

    private function request(string $method, string $path, ?array $body = null): array
    {
        for ($attempt = 1; ; $attempt++) {
            $ch = curl_init($this->base.$path);
            curl_setopt_array($ch, [
                CURLOPT_CUSTOMREQUEST => $method, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => $this->timeout,
                CURLOPT_HTTPHEADER => ['Authorization: Bearer '.$this->key, 'Accept: application/json', 'Content-Type: application/json', 'User-Agent: ellidiya-business-php/1'],
            ]);
            if ($body !== null) {
                curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body, JSON_UNESCAPED_UNICODE));
            }
            $raw = curl_exec($ch);
            $status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
            $err = curl_error($ch);
            curl_close($ch);
            // retry network errors and 5xx (twice); 429 waits a little. Safe for sends only with a client_ref.
            $retryable = $raw === false || $status >= 500 || $status === 429;
            $safe = $method === 'GET' || ($body['client_ref'] ?? null) !== null;
            if ($retryable && $safe && $attempt < 3) {
                usleep(($status === 429 ? 2000000 : 500000) * $attempt);
                continue;
            }
            if ($raw === false) {
                throw new EllidiyaBusinessError('Network error: '.$err, 'network', 0);
            }
            $j = json_decode($raw, true);
            if (! is_array($j)) {
                throw new EllidiyaBusinessError('Unexpected response ('.$status.')', 'bad_response', $status);
            }
            if (empty($j['ok'])) {
                throw new EllidiyaBusinessError($j['error']['message'] ?? 'Request failed', $j['error']['code'] ?? 'error', $status);
            }

            return $j;
        }
    }
}
