<?php
/**
 * BounceZero - official PHP client (single file, requires ext-curl, PHP 8.0+).
 *
 * Usage:
 *   require 'BounceZero.php';
 *   $client = new \BounceZero\BounceZero('bz_live_...');
 *   $result = $client->verify('someone@example.com');
 *   echo $result['classification'], ' ', $result['score'];
 *
 * Docs: https://docs.bouncezero.io
 */

namespace BounceZero;

class BounceZeroException extends \Exception
{
    public ?int $statusCode;
    public array $payload;

    public function __construct(string $message, ?int $statusCode = null, array $payload = [])
    {
        parent::__construct($message);
        $this->statusCode = $statusCode;
        $this->payload = $payload;
    }
}

class AuthenticationException extends BounceZeroException {}
class InsufficientCreditsException extends BounceZeroException {}
class NotFoundException extends BounceZeroException {}
class RateLimitException extends BounceZeroException
{
    public ?float $retryAfter;

    public function __construct(string $message, ?int $statusCode = null, array $payload = [], ?float $retryAfter = null)
    {
        parent::__construct($message, $statusCode, $payload);
        $this->retryAfter = $retryAfter;
    }
}

class BounceZero
{
    public const VERSION = '0.1.0';
    public const DEFAULT_BASE_URL = 'https://app.bouncezero.io';

    private string $apiKey;
    private string $baseUrl;
    private int $timeout;
    private int $maxRetries;

    public function __construct(string $apiKey, string $baseUrl = self::DEFAULT_BASE_URL, int $timeout = 60, int $maxRetries = 2)
    {
        if ($apiKey === '') {
            throw new \InvalidArgumentException('apiKey is required');
        }
        $this->apiKey = $apiKey;
        $this->baseUrl = rtrim($baseUrl, '/');
        $this->timeout = $timeout;
        $this->maxRetries = $maxRetries;
    }

    /* ── verification ────────────────────────────────────────────────── */

    /** Verify one address. $depth: 'standard' | 'deep' | 'ultra'. */
    public function verify(string $email, string $depth = 'standard'): array
    {
        return $this->request('POST', '/api/v1/verify', ['email' => $email, 'depth' => $depth]);
    }

    /** Synchronously verify up to 100 addresses in one call. */
    public function verifyBatch(array $emails): array
    {
        return $this->request('POST', '/api/v1/verify/batch', ['emails' => array_values($emails)]);
    }

    /**
     * Submit an async bulk job. Pass $idempotencyKey to make retries safe -
     * replaying the same key returns the original job instead of re-charging.
     */
    public function verifyBulk(array $emails, ?string $idempotencyKey = null): array
    {
        $headers = $idempotencyKey !== null ? ['Idempotency-Key: ' . $idempotencyKey] : [];
        return $this->request('POST', '/api/v1/verify/bulk', ['emails' => array_values($emails)], $headers);
    }

    public function bulkStatus(string $jobId): array
    {
        return $this->request('GET', "/api/v1/verify/bulk/{$jobId}/status");
    }

    public function bulkResults(string $jobId, int $limit = 1000, int $offset = 0): array
    {
        return $this->request('GET', "/api/v1/verify/bulk/{$jobId}/results?limit={$limit}&offset={$offset}");
    }

    /** Download completed job results as a CSV string. */
    public function bulkDownload(string $jobId): string
    {
        return $this->request('GET', "/api/v1/verify/bulk/{$jobId}/download", null, [], true);
    }

    /** Poll until the job finishes. Returns the final status payload. */
    public function waitForBulk(string $jobId, float $pollInterval = 5.0, float $timeout = 3600.0): array
    {
        $deadline = microtime(true) + $timeout;
        while (true) {
            $status = $this->bulkStatus($jobId);
            if (!in_array($status['status'] ?? '', ['processing', 'queued'], true)) {
                return $status;
            }
            if (microtime(true) >= $deadline) {
                throw new BounceZeroException("Timed out waiting for bulk job {$jobId}");
            }
            usleep((int) ($pollInterval * 1_000_000));
        }
    }

    /* ── intelligence ────────────────────────────────────────────────── */

    /** Domain-level intelligence (MX, provider, catch-all status, ...). */
    public function domain(string $domain): array
    {
        return $this->request('GET', '/api/v1/domain/' . rawurlencode($domain));
    }

    /** Free pre-flight quality analysis of an email list (no verification). */
    public function analyzeList(array $emails): array
    {
        return $this->request('POST', '/api/v1/analyze/list', ['emails' => array_values($emails)]);
    }

    /* ── webhooks ────────────────────────────────────────────────────── */

    /**
     * Return true if an X-BounceZero-Signature header matches the raw request body.
     * Always verify with the *raw* body (file_get_contents('php://input')), before JSON parsing.
     */
    public static function verifyWebhookSignature(string $rawBody, string $signatureHeader, string $signingSecret): bool
    {
        $expected = 'sha256=' . hash_hmac('sha256', $rawBody, $signingSecret);
        return hash_equals($expected, $signatureHeader);
    }

    /* ── internals ───────────────────────────────────────────────────── */

    private function request(string $method, string $path, ?array $body = null, array $headers = [], bool $raw = false)
    {
        $url = $this->baseUrl . $path;
        $attempt = 0;
        while (true) {
            $ch = curl_init($url);
            $requestHeaders = array_merge([
                'X-API-Key: ' . $this->apiKey,
                'User-Agent: bouncezero-php/' . self::VERSION,
            ], $headers);
            $opts = [
                CURLOPT_CUSTOMREQUEST => $method,
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_TIMEOUT => $this->timeout,
                CURLOPT_HEADER => true,
            ];
            if ($body !== null) {
                $requestHeaders[] = 'Content-Type: application/json';
                $opts[CURLOPT_POSTFIELDS] = json_encode($body);
            }
            $opts[CURLOPT_HTTPHEADER] = $requestHeaders;
            curl_setopt_array($ch, $opts);

            $response = curl_exec($ch);
            if ($response === false) {
                $err = curl_error($ch);
                curl_close($ch);
                if ($attempt < $this->maxRetries) {
                    $attempt++;
                    sleep(min(2 ** $attempt, 30));
                    continue;
                }
                throw new BounceZeroException("Connection error: {$err}");
            }

            $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
            $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
            curl_close($ch);
            $rawHeaders = substr($response, 0, $headerSize);
            $bodyText = substr($response, $headerSize);

            if ($status >= 200 && $status < 300) {
                return $raw ? $bodyText : (json_decode($bodyText, true) ?? []);
            }

            $payload = json_decode($bodyText, true) ?? [];
            $detail = $payload['detail'] ?? "HTTP {$status}";
            $retryAfter = null;
            if (preg_match('/^Retry-After:\s*([0-9.]+)/mi', $rawHeaders, $m)) {
                $retryAfter = (float) $m[1];
            }

            if (($status === 429 || $status >= 500) && $attempt < $this->maxRetries) {
                $attempt++;
                sleep((int) ($retryAfter ?? min(2 ** $attempt, 30)));
                continue;
            }
            if ($status === 401 || $status === 403) {
                throw new AuthenticationException($detail, $status, $payload);
            }
            if ($status === 402) {
                throw new InsufficientCreditsException($detail, $status, $payload);
            }
            if ($status === 404) {
                throw new NotFoundException($detail, $status, $payload);
            }
            if ($status === 429) {
                throw new RateLimitException($detail, $status, $payload, $retryAfter);
            }
            throw new BounceZeroException($detail, $status, $payload);
        }
    }
}
