<?php
/**
 * RACF → REVEALDI observe-only intake (PHP 7.4).
 *
 * POST https://api.revealdi.com/v1/submissions
 * Implio stays the live website decision. This is comparison only.
 *
 * Do not put key_id or secret in Git, this file, or chat.
 * Load them from environment or the RACF secret store.
 */

if (!function_exists('revealdi_uuid_v4')) {
    function revealdi_uuid_v4()
    {
        $bytes = random_bytes(16);
        $bytes[6] = chr((ord($bytes[6]) & 0x0f) | 0x40);
        $bytes[8] = chr((ord($bytes[8]) & 0x3f) | 0x80);
        $hex = bin2hex($bytes);

        return sprintf(
            '%s-%s-%s-%s-%s',
            substr($hex, 0, 8),
            substr($hex, 8, 4),
            substr($hex, 12, 4),
            substr($hex, 16, 4),
            substr($hex, 20, 12)
        );
    }
}

/**
 * Retry 408, 429, 5xx, connect/read timeout, and connection reset.
 * Do not retry other 4xx. Each attempt must use a new Request-Id.
 *
 * @param array $result
 * @return bool
 */
function revealdi_submit_should_retry(array $result)
{
    if (!empty($result['ok'])) {
        return false;
    }
    $code = isset($result['http_code']) ? (int) $result['http_code'] : 0;
    if ($code === 408 || $code === 429) {
        return true;
    }
    if ($code >= 500 && $code <= 599) {
        return true;
    }
    return $code === 0;
}

/**
 * Sign and POST one source event to REVEALDI.
 *
 * $payload must already match the handoff schema. Typical keys:
 *   client_submission_id, content_type, profile, assets[], external_ref
 *
 * $config:
 *   key_id        (required) X-Panpad-Key-Id
 *   secret        (required) HMAC secret, UTF-8
 *   base_url      (optional) default https://api.revealdi.com
 *   timeout       (optional) seconds, default 15
 *   max_attempts  (optional) default 5
 *
 * A 201/200 means REVEALDI accepted the event, not that moderation finished.
 * The evaluation arrives later on observation.result. Do not block Implio on this call.
 *
 * Retries connection reset / timeout / 408 / 429 / 5xx with a new Request-Id
 * and the same client_submission_id. Backoff 5 / 15 / 45 / 90 seconds.
 *
 * @param array $payload
 * @param array $config
 * @return array
 */
function revealdi_submit(array $payload, array $config)
{
    $maxAttempts = isset($config['max_attempts']) ? (int) $config['max_attempts'] : 5;
    if ($maxAttempts < 1) {
        $maxAttempts = 1;
    }
    $delays = array(5, 15, 45, 90);
    $last = null;
    $requestIds = array();
    for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
        $last = revealdi_submit_once($payload, $config);
        if (isset($last['request_id'])) {
            $requestIds[] = $last['request_id'];
        }
        $last['attempts'] = $attempt;
        $last['request_ids'] = $requestIds;
        if (!empty($last['ok']) || !revealdi_submit_should_retry($last) || $attempt === $maxAttempts) {
            return $last;
        }
        sleep($delays[min($attempt - 1, count($delays) - 1)]);
    }
    return $last;
}

/**
 * One signed HTTP attempt. Always generates a fresh Request-Id and timestamp.
 *
 * @param array $payload
 * @param array $config
 * @return array
 */
function revealdi_submit_once(array $payload, array $config)
{
    $keyId = isset($config['key_id']) ? trim((string) $config['key_id']) : '';
    $secret = isset($config['secret']) ? (string) $config['secret'] : '';
    $baseUrl = isset($config['base_url']) ? rtrim((string) $config['base_url'], '/') : 'https://api.revealdi.com';
    $timeout = isset($config['timeout']) ? (int) $config['timeout'] : 15;
    $path = '/v1/submissions';

    if ($keyId === '' || $secret === '') {
        return array(
            'ok' => false,
            'http_code' => 0,
            'status' => null,
            'body' => null,
            'raw' => '',
            'error' => 'Missing REVEALDI key_id or secret',
        );
    }

    $body = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    if ($body === false) {
        return array(
            'ok' => false,
            'http_code' => 0,
            'status' => null,
            'body' => null,
            'raw' => '',
            'error' => 'JSON encode failed',
        );
    }

    $timestamp = (string) time();
    $requestId = revealdi_uuid_v4();
    $contentSha = hash('sha256', $body);
    $canonical = "POST\n{$path}\n{$timestamp}\n{$requestId}\napplication/json\n{$contentSha}";
    $signature = hash_hmac('sha256', $canonical, $secret);

    $headers = array(
        'Content-Type: application/json',
        'Accept: application/json',
        'X-Panpad-Key-Id: ' . $keyId,
        'X-Panpad-Timestamp: ' . $timestamp,
        'X-Panpad-Request-Id: ' . $requestId,
        'X-Panpad-Content-SHA256: ' . $contentSha,
        'X-Panpad-Signature: ' . $signature,
    );

    $ch = curl_init($baseUrl . $path);
    if ($ch === false) {
        return array(
            'ok' => false,
            'http_code' => 0,
            'status' => null,
            'body' => null,
            'raw' => '',
            'error' => 'curl_init failed',
            'request_id' => $requestId,
        );
    }

    curl_setopt_array($ch, array(
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => $body,
        CURLOPT_HTTPHEADER => $headers,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HEADER => false,
        CURLOPT_FOLLOWLOCATION => false,
        CURLOPT_CONNECTTIMEOUT => 5,
        CURLOPT_TIMEOUT => $timeout > 0 ? $timeout : 15,
        CURLOPT_SSL_VERIFYPEER => true,
        CURLOPT_SSL_VERIFYHOST => 2,
    ));

    $raw = curl_exec($ch);
    $errno = curl_errno($ch);
    $error = $errno ? curl_error($ch) : null;
    $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($raw === false) {
        return array(
            'ok' => false,
            'http_code' => $httpCode,
            'status' => null,
            'body' => null,
            'raw' => '',
            'error' => $error !== null ? $error : 'curl failed',
            'request_id' => $requestId,
        );
    }

    $decoded = json_decode($raw, true);
    if (!is_array($decoded)) {
        $decoded = null;
    }

    return array(
        'ok' => ($httpCode === 201 || $httpCode === 200),
        'http_code' => $httpCode,
        'status' => isset($decoded['processing_status']) ? $decoded['processing_status'] : null,
        'body' => $decoded,
        'raw' => $raw,
        'error' => isset($decoded['error']['code']) ? $decoded['error']['code'] : $error,
        'request_id' => $requestId,
    );
}

/**
 * Example (do not run with secrets in source):
 *
 * $result = revealdi_submit(
 *     array(
 *         'client_submission_id' => 'racf-evt-1001',
 *         'content_type' => 'profile_v1',
 *         'profile' => array(
 *             'display_name' => 'Ada',
 *             'bio' => 'Hello from a controlled test.',
 *         ),
 *         'assets' => array(
 *             array(
 *                 'role' => 'profile_image',
 *                 'image_id' => 'photo-a',
 *                 'source_url' => 'https://cdn.example.test/p/a.jpg',
 *             ),
 *         ),
 *         'external_ref' => array(
 *             'user_id' => '123456',
 *             'item_id' => 'item-88',
 *             'profile_revision' => '3',
 *             'username' => 'ada',
 *             'email' => 'ada@example.test',
 *         ),
 *     ),
 *     array(
 *         'key_id' => getenv('REVEALDI_KEY_ID'),
 *         'secret' => getenv('REVEALDI_SECRET'),
 *     )
 * );
 *
 * Fire-and-forget beside Implio. The helper retries 502/reset/timeout.
 * If ok is still false after retries, continue the Implio path.
 */
