<?php
/**
 * Trustport SDK for PHP — single-file drop-in.
 *
 * Zero dependencies. PHP >= 7.1. Just download into your project and:
 *
 *   require_once 'trustport.php';
 *   $tp = new Trustport_Client(['apiKey' => getenv('TRUSTPORT_API_KEY')]);
 *
 *   $s = $tp->startSession(['subject' => $username]);
 *   // $s['sid'], $s['claim_token'], $s['verify_url'], $s['qr_url'], $s['expires_at']
 *
 *   $r = $tp->verifySession(['sid' => $s['sid'], 'claim_token' => $s['claim_token']]);
 *   // $r['ok'] === true   -> $r['subject'], $r['user_id'], $r['verified_at']
 *   // $r['ok'] === false  -> $r['status']  ('pending'|'expired'|'rejected'|'claimed')
 *
 * For the composer package version: `composer require trustport/sdk`
 * Docs: https://trustport.vospen.com/docs
 *
 * License: MIT
 */

if (!function_exists('curl_init')) {
    // Hard error at include time so failure is obvious during onboarding.
    throw new RuntimeException('Trustport: ext-curl is required.');
}

class Trustport_Exception extends RuntimeException
{
    public $httpStatus;
    public $errorCode;
    public $body;

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

class Trustport_Client
{
    const SDK_VERSION = '0.1.0';
    const DEFAULT_BASE_URL = 'https://trustport.vospen.com';

    private $apiKey;
    private $baseUrl;
    private $timeoutSeconds;

    /**
     * @param array $options ['apiKey' => string, 'baseUrl' => string?, 'timeoutSeconds' => int?]
     */
    public function __construct(array $options)
    {
        if (empty($options['apiKey']) || !is_string($options['apiKey'])) {
            throw new InvalidArgumentException('Trustport: options[apiKey] is required.');
        }
        $this->apiKey = $options['apiKey'];
        $this->baseUrl = rtrim(
            isset($options['baseUrl']) ? $options['baseUrl'] : self::DEFAULT_BASE_URL,
            '/'
        );
        $this->timeoutSeconds = isset($options['timeoutSeconds']) ? (int)$options['timeoutSeconds'] : 10;
    }

    /**
     * Open a verification session bound to a subject.
     *
     * @param array $params ['subject' => string]
     * @return array { sid, claim_token, verify_url, qr_url, expires_at }
     * @throws Trustport_Exception
     */
    public function startSession(array $params)
    {
        if (empty($params['subject']) || !is_string($params['subject'])) {
            throw new InvalidArgumentException('Trustport.startSession: subject is required.');
        }
        $resp = $this->httpJson('POST', '/v1/sessions', array('subject' => $params['subject']));
        if ($resp['status'] !== 200) {
            $this->throwForError($resp);
        }
        $body = $resp['body'];
        return array(
            'sid'         => (string)$body['sid'],
            'claim_token' => (string)$body['claim_token'],
            'verify_url'  => (string)$body['verify_url'],
            'qr_url'      => (string)$body['qr_url'],
            'expires_at'  => (string)$body['expires_at'],
        );
    }

    /**
     * Confirm whether a session has been approved. Single-use.
     *
     * @param array $params ['sid' => string, 'claim_token' => string]
     * @return array
     *   On approval:    { ok: true,  subject, user_id, verified_at }
     *   Not approved:   { ok: false, status: 'pending'|'expired'|'rejected'|'claimed' }
     * @throws Trustport_Exception
     */
    public function verifySession(array $params)
    {
        if (empty($params['sid']) || empty($params['claim_token'])) {
            throw new InvalidArgumentException('Trustport.verifySession: sid and claim_token are required.');
        }
        $path = '/v1/sessions/' . rawurlencode((string)$params['sid']) . '/verify';
        $resp = $this->httpJson('POST', $path, array('claim_token' => (string)$params['claim_token']));
        $status = $resp['status'];
        $body   = $resp['body'];

        if ($status === 200 && is_array($body) && isset($body['ok']) && $body['ok'] === true) {
            return array(
                'ok'          => true,
                'subject'     => (string)$body['subject'],
                'user_id'     => (string)$body['user_id'],
                'verified_at' => (string)$body['verified_at'],
            );
        }
        if ($status === 200 && is_array($body) && isset($body['ok']) && $body['ok'] === false) {
            return array('ok' => false, 'status' => isset($body['status']) ? (string)$body['status'] : 'pending');
        }
        if ($status === 409 && is_array($body) && isset($body['error']) && $body['error'] === 'already_claimed') {
            return array('ok' => false, 'status' => 'claimed');
        }
        $this->throwForError($resp);
        return array('ok' => false, 'status' => 'unknown'); // unreachable
    }

    // ---- internals ---------------------------------------------------------

    private function httpJson($method, $path, $body)
    {
        $ch = curl_init();
        $headers = array(
            'Authorization: Bearer ' . $this->apiKey,
            'Content-Type: application/json',
            'Accept: application/json',
            'User-Agent: trustport/sdk-php-single/' . self::SDK_VERSION,
        );
        curl_setopt_array($ch, array(
            CURLOPT_URL            => $this->baseUrl . $path,
            CURLOPT_CUSTOMREQUEST  => $method,
            CURLOPT_HTTPHEADER     => $headers,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT        => $this->timeoutSeconds,
            CURLOPT_CONNECTTIMEOUT => max(1, (int)floor($this->timeoutSeconds / 2)),
            CURLOPT_FOLLOWLOCATION => false,
        ));
        if ($body !== null) {
            $json = json_encode($body, JSON_UNESCAPED_SLASHES);
            if ($json === false) {
                curl_close($ch);
                throw new Trustport_Exception('encode_error', 0, 'encode_error');
            }
            curl_setopt($ch, CURLOPT_POSTFIELDS, $json);
        }
        $raw    = curl_exec($ch);
        $errno  = curl_errno($ch);
        $err    = curl_error($ch);
        $status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($errno !== 0 || !is_string($raw)) {
            throw new Trustport_Exception($err ? $err : 'network_error', 0, 'network_error');
        }
        $decoded = null;
        if ($raw !== '') {
            $decoded = json_decode($raw, true);
            if (!is_array($decoded)) {
                $decoded = array('error' => 'invalid_json_response', 'raw' => $raw);
            }
        }
        return array('status' => $status, 'body' => $decoded);
    }

    private function throwForError($resp)
    {
        $status = $resp['status'];
        $body   = $resp['body'];
        $code = (is_array($body) && isset($body['error']) && is_string($body['error']))
            ? $body['error']
            : ('http_' . $status);
        throw new Trustport_Exception($code, $status, $code, $body);
    }
}
