<?php
/**
 * REVEALDI → RACF observation webhook (PHP 7.4).
 *
 * Drop this file at a path the RACF front controller does not own:
 *   https://www.rentacyberfriend.com/webhook_revealdi/index.php
 *
 * This file must be the script that sends the HTTP status. A CMS
 * route that already sent 200 + a racf= session cookie is not this
 * receiver — do not register that URL.
 *
 * Expected status when this function handles the request:
 *   GET / non-POST     → 405 application/json
 *   POST unsigned      → 401 application/json
 *   POST, secret unset → 503 application/json
 *   POST verified      → 204 empty body
 *
 * Register the exact HTTPS URL that returns 2xx with no redirect.
 * rentacyberfriend.com redirects to www — use the www host.
 *
 * Do NOT copy an Implio webhook. Implio is the live website decision.
 * This callback is OBSERVE_ONLY comparison. Do not hide, restore,
 * refuse, approve, or take a profile offline from this payload.
 *
 * Secret is REVEALDI_WEBHOOK_SECRET (whk_…), not REVEALDI_SECRET.
 * Do not put the secret in Git or this file.
 */

if (!function_exists('revealdi_webhook_header')) {
    function revealdi_webhook_header($name)
    {
        $key = 'HTTP_' . strtoupper(str_replace('-', '_', $name));
        if (isset($_SERVER[$key]) && is_string($_SERVER[$key])) {
            return trim($_SERVER[$key]);
        }

        return '';
    }
}

/**
 * Verify one inbound REVEALDI observation callback.
 *
 * $config:
 *   secret  (required) webhook HMAC secret, UTF-8
 *   skew    (optional) timestamp window seconds, default 300
 *
 * @param string $rawBody
 * @param array  $config
 * @return array{ok:bool,http_code:int,event_id:?string,payload:?array,error:?string}
 */
function revealdi_verify_observation_webhook($rawBody, array $config)
{
    $secret = isset($config['secret']) ? (string) $config['secret'] : '';
    $skew = isset($config['skew']) ? (int) $config['skew'] : 300;
    if ($skew <= 0) {
        $skew = 300;
    }

    if ($secret === '') {
        return array(
            'ok' => false,
            'http_code' => 503,
            'event_id' => null,
            'payload' => null,
            'error' => 'Missing REVEALDI_WEBHOOK_SECRET',
        );
    }

    $keyId = revealdi_webhook_header('X-Panpad-Webhook-Key-Id');
    $timestamp = revealdi_webhook_header('X-Panpad-Webhook-Timestamp');
    $eventId = revealdi_webhook_header('X-Panpad-Webhook-Event-Id');
    $contentSha = strtolower(revealdi_webhook_header('X-Panpad-Webhook-Content-SHA256'));
    $signature = strtolower(revealdi_webhook_header('X-Panpad-Webhook-Signature'));

    if ($keyId === '' || $timestamp === '' || $eventId === '' || $contentSha === '' || $signature === '') {
        return array(
            'ok' => false,
            'http_code' => 401,
            'event_id' => $eventId !== '' ? $eventId : null,
            'payload' => null,
            'error' => 'Missing webhook headers',
        );
    }

    if (!preg_match('/^[0-9]{1,12}$/', $timestamp)) {
        return array(
            'ok' => false,
            'http_code' => 401,
            'event_id' => $eventId,
            'payload' => null,
            'error' => 'Invalid timestamp',
        );
    }

    $now = time();
    if (abs($now - (int) $timestamp) > $skew) {
        return array(
            'ok' => false,
            'http_code' => 401,
            'event_id' => $eventId,
            'payload' => null,
            'error' => 'Timestamp outside allowed window',
        );
    }

    $digest = hash('sha256', $rawBody);
    if (!hash_equals($contentSha, $digest)) {
        return array(
            'ok' => false,
            'http_code' => 401,
            'event_id' => $eventId,
            'payload' => null,
            'error' => 'Body hash mismatch',
        );
    }

    $canonical = "POST\npanpad.webhook.v1\n{$timestamp}\n{$eventId}\napplication/json\n{$digest}";
    $expected = hash_hmac('sha256', $canonical, $secret);
    if (!hash_equals($expected, $signature)) {
        return array(
            'ok' => false,
            'http_code' => 401,
            'event_id' => $eventId,
            'payload' => null,
            'error' => 'Bad signature',
        );
    }

    $payload = json_decode($rawBody, true);
    if (!is_array($payload) || !isset($payload['event_id']) || !is_string($payload['event_id'])) {
        return array(
            'ok' => false,
            'http_code' => 401,
            'event_id' => $eventId,
            'payload' => null,
            'error' => 'Invalid JSON body',
        );
    }

    if ($payload['event_id'] !== $eventId) {
        return array(
            'ok' => false,
            'http_code' => 401,
            'event_id' => $eventId,
            'payload' => null,
            'error' => 'event_id mismatch',
        );
    }

    return array(
        'ok' => true,
        'http_code' => 204,
        'event_id' => $payload['event_id'],
        'payload' => $payload,
        'error' => null,
    );
}

/**
 * Persist one verified observation. Replace this with RACF storage.
 * Dedupe on event_id. Do not change website visibility.
 *
 * @param array $payload
 * @return bool true if newly stored or already seen
 */
function revealdi_store_observation(array $payload)
{
    $eventId = isset($payload['event_id']) ? (string) $payload['event_id'] : '';
    if ($eventId === '') {
        return false;
    }

    $dir = getenv('REVEALDI_WEBHOOK_STORE');
    if (!is_string($dir) || $dir === '') {
        $dir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'revealdi-observe';
    }
    if (!is_dir($dir) && !mkdir($dir, 0700, true) && !is_dir($dir)) {
        return false;
    }

    $safe = preg_replace('/[^A-Za-z0-9._-]/', '_', $eventId);
    $path = $dir . DIRECTORY_SEPARATOR . $safe . '.json';
    if (is_file($path)) {
        return true;
    }

    $keep = array(
        'event_id' => $payload['event_id'],
        'event_type' => isset($payload['event_type']) ? $payload['event_type'] : null,
        'source_event_id' => isset($payload['source_event_id']) ? $payload['source_event_id'] : null,
        'external_ref' => isset($payload['external_ref']) ? $payload['external_ref'] : null,
        'submission_id' => isset($payload['submission_id']) ? $payload['submission_id'] : null,
        'processing_status' => isset($payload['processing_status']) ? $payload['processing_status'] : null,
        'proposed_decision' => isset($payload['proposed_decision']) ? $payload['proposed_decision'] : null,
        'reason_codes' => isset($payload['reason_codes']) ? $payload['reason_codes'] : null,
        'occurred_at' => isset($payload['occurred_at']) ? $payload['occurred_at'] : null,
    );
    $encoded = json_encode($keep);
    if ($encoded === false) {
        return false;
    }

    return file_put_contents($path, $encoded, LOCK_EX) !== false;
}

function revealdi_observe_webhook_finish($code, $body)
{
    while (ob_get_level() > 0) {
        ob_end_clean();
    }

    $code = (int) $code;
    if (!headers_sent()) {
        http_response_code($code);
        if ($code === 204) {
            header('Content-Length: 0');
        } else {
            if ($code === 405) {
                header('Allow: POST');
            }
            header('Content-Type: application/json');
        }
    }

    if ($code !== 204 && $body !== '') {
        echo $body;
    }
    exit;
}

function revealdi_observe_webhook_respond()
{
    if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] !== 'POST') {
        revealdi_observe_webhook_finish(405, '{"ok":false,"error":"Method not allowed"}');
    }

    $raw = file_get_contents('php://input');
    if (!is_string($raw)) {
        $raw = '';
    }

    $result = revealdi_verify_observation_webhook(
        $raw,
        array('secret' => getenv('REVEALDI_WEBHOOK_SECRET'))
    );

    if (empty($result['ok'])) {
        revealdi_observe_webhook_finish(
            (int) $result['http_code'],
            json_encode(array('ok' => false, 'error' => $result['error']))
        );
    }

    $payload = $result['payload'];
    if (!is_array($payload) || !revealdi_store_observation($payload)) {
        revealdi_observe_webhook_finish(503, '{"ok":false,"error":"Store failed"}');
    }

    revealdi_observe_webhook_finish(204, '');
}

/**
 * Compact correction from REVEALDI. RACF owns title, subtitle, and CTA.
 * Use user_refusal.bio / user_refusal.photo / message when present.
 * Do not concatenate reasons[] into a paragraph.
 * Implio remains the live website decision.
 *
 * @param array $payload
 * @return string
 */
function revealdi_refusal_card_html(array $payload)
{
    $card = isset($payload['user_refusal']) && is_array($payload['user_refusal'])
        ? $payload['user_refusal']
        : null;
    if ($card === null) {
        return '';
    }
    $esc = static function ($value) {
        return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
    };
    $html = '<article class="user-refusal-card" style="max-width:390px;width:100%;overflow-wrap:anywhere">';
    $html .= '<h2>Profile review not approved</h2>';
    $html .= '<p>Your profile didn\'t pass our review. Please update the required sections to go online.</p>';
    if (!empty($card['bio'])) {
        $html .= '<p>Bio: ' . $esc($card['bio']) . '</p>';
    }
    if (!empty($card['photo'])) {
        $html .= '<p>Photo: ' . $esc($card['photo']) . '</p>';
    }
    $html .= '<p>Fix profile</p>';
    $html .= '</article>';

    return $html;
}

if (PHP_SAPI !== 'cli' && realpath($_SERVER['SCRIPT_FILENAME']) === realpath(__FILE__)) {
    revealdi_observe_webhook_respond();
}
