<?php
/**
 * bentbox_config.php
 * Settings, token store and HTTP helpers for the BentBox integration.
 * Include this at the top of every BentBox page.
 *
 * SECURITY MODEL
 *   - client_secret and all tokens live ONLY on the server.
 *   - The browser never receives access_token, refresh_token or connection_id.
 *   - Token exchange and every BentBox API call happen server-side (cURL).
 *   - No PHP sessions: state lives in encrypted cookies, so this works on
 *     several servers behind a load balancer as long as they share BB_COOKIE_KEY.
 *
 * SETTINGS come from environment variables (set them in your web server or
 * PHP-FPM pool config). Never hard-code secrets in a web-served file.
 *
 *   BENTBOX_CLIENT_ID      your OAuth client ID (mp_live_…)
 *   BENTBOX_CLIENT_SECRET  your OAuth client secret (mp_secret_…)
 *   BENTBOX_REDIRECT_URI   full URL of bentbox_callback.php, exactly as registered with BentBox
 *   BENTBOX_COOKIE_KEY     base64 32-byte key:  php -r "echo base64_encode(random_bytes(32)).PHP_EOL;"
 *   BENTBOX_COOKIE_DOMAIN  optional, e.g. ".example.com" to share the cookie across www + apex
 */

function bb_env(string $name, ?string $default = null): string {
    $v = getenv($name);
    if ($v === false || $v === '') {
        if ($default !== null) return $default;
        http_response_code(500);
        exit("BentBox integration is not configured: set the $name environment variable.");
    }
    return $v;
}

// --- Your settings -----------------------------------------------------------
define('BB_CLIENT_ID',     bb_env('BENTBOX_CLIENT_ID'));
define('BB_CLIENT_SECRET', bb_env('BENTBOX_CLIENT_SECRET'));
define('BB_REDIRECT_URI',  bb_env('BENTBOX_REDIRECT_URI'));
define('BB_COOKIE_KEY',    bb_env('BENTBOX_COOKIE_KEY'));
define('BB_COOKIE_DOMAIN', bb_env('BENTBOX_COOKIE_DOMAIN', ''));   // '' = current host only

const BB_SCOPES = 'upload_video';

// false = the browser uploads the file straight to the presigned storage URL
//         (best for large files; the short-lived URL is used by JS, never shown).
// true  = the file goes browser -> your server -> storage, so nothing storage-
//         related reaches the browser (heavier; limited by PHP upload limits).
const BB_PROXY_UPLOAD = false;

// Encrypted cookies
const BB_CONN_COOKIE  = 'bb_conn';            // the connection (tokens)
const BB_CONN_TTL     = 30 * 24 * 3600;       // 30 days, long enough to keep the refresh_token
const BB_STATE_COOKIE = 'bb_oauth_state';     // CSRF state during the OAuth round trip
const BB_STATE_TTL    = 15 * 60;              // 15 minutes to sign in and approve

// --- BentBox endpoints -------------------------------------------------------
const BB_API_BASE      = 'https://api.bentbox.co';
const BB_AUTHORIZE_URL = BB_API_BASE . '/oauth/authorize';
const BB_TOKEN_URL     = BB_API_BASE . '/oauth/token';
const BB_CREATE_VIDEO  = BB_API_BASE . '/v1/content/video';
const BB_UPLOAD_URL    = BB_API_BASE . '/v1/content/upload-url';
const BB_VIDEO_STATUS  = BB_API_BASE . '/v1/content/video/';   // + {video_id}

/** Cookie options shared by every cookie this integration sets. */
function bb_cookie_opts(int $expires): array {
    $o = ['expires' => $expires, 'path' => '/', 'secure' => true, 'httponly' => true, 'samesite' => 'Lax'];
    if (BB_COOKIE_DOMAIN !== '') $o['domain'] = BB_COOKIE_DOMAIN;
    return $o;
}

/**
 * ---- Token store (encrypted cookie, no server-side state) -------------------
 * The connection (access_token, refresh_token, connection_id, user_id, scopes)
 * is stored in ONE cookie, AES-256-GCM encrypted and authenticated with
 * BB_COOKIE_KEY. The browser can't read or forge it (HttpOnly + GCM auth tag),
 * and any of your servers can decrypt it — no sessions, no DB, no shared storage.
 *
 * An in-request cache mirrors writes so bb_get_connection() reflects a
 * bb_store_connection() call made earlier in the SAME request (e.g. after a
 * token refresh), since $_COOKIE isn't updated mid-request.
 */
function &bb_conn_cache(): array {
    static $c = ['loaded' => false, 'data' => null];
    return $c;
}

function bb_store_connection(array $tokens): void {
    $existing = bb_get_connection() ?? [];
    $data = [
        'access_token'  => $tokens['access_token']  ?? $existing['access_token']  ?? null,
        'refresh_token' => $tokens['refresh_token'] ?? $existing['refresh_token'] ?? null,
        'connection_id' => $tokens['connection_id'] ?? $existing['connection_id'] ?? null,
        'user_id'       => $tokens['user_id']       ?? $existing['user_id']       ?? null,
        'scopes'        => $tokens['scopes']        ?? $existing['scopes']        ?? [],
        'expires_at'    => isset($tokens['expires_in'])
                            ? time() + (int)$tokens['expires_in']
                            : ($existing['expires_at'] ?? 0),
    ];

    setcookie(BB_CONN_COOKIE, bb_cookie_encode($data), bb_cookie_opts(time() + BB_CONN_TTL));

    $cache = &bb_conn_cache();
    $cache['loaded'] = true;
    $cache['data']   = $data;
}

function bb_get_connection(): ?array {
    $cache = &bb_conn_cache();
    if (!$cache['loaded']) {
        $cache['data']   = bb_cookie_decode($_COOKIE[BB_CONN_COOKIE] ?? '');
        $cache['loaded'] = true;
    }
    return $cache['data'];
}

function bb_is_connected(): bool {
    $c = bb_get_connection();
    return $c && !empty($c['access_token']) && !empty($c['connection_id']);
}

function bb_clear_connection(): void {
    setcookie(BB_CONN_COOKIE, '', bb_cookie_opts(time() - 3600));
    $cache = &bb_conn_cache();
    $cache['loaded'] = true;
    $cache['data']   = null;
}

/**
 * ---- Cookie encryption (AES-256-GCM, url-safe base64) -----------------------
 */
function bb_cookie_key(): string {
    $key = base64_decode(BB_COOKIE_KEY, true);
    if ($key === false || strlen($key) !== 32) {
        throw new RuntimeException(
            'BENTBOX_COOKIE_KEY must be a base64-encoded 32-byte key. Generate one with: ' .
            'php -r "echo base64_encode(random_bytes(32)).PHP_EOL;"'
        );
    }
    return $key;
}

function bb_b64url(string $bin): string {
    return rtrim(strtr(base64_encode($bin), '+/', '-_'), '=');
}

function bb_b64url_decode(string $s) {
    return base64_decode(strtr($s, '-_', '+/'));
}

function bb_cookie_encode(array $data): string {
    $iv  = random_bytes(12);
    $tag = '';
    $ct  = openssl_encrypt(json_encode($data), 'aes-256-gcm', bb_cookie_key(),
                           OPENSSL_RAW_DATA, $iv, $tag);
    if ($ct === false) throw new RuntimeException('Cookie encryption failed');
    return bb_b64url($iv . $tag . $ct);   // iv(12) | tag(16) | ciphertext
}

function bb_cookie_decode(string $raw): ?array {
    if ($raw === '') return null;
    $bin = bb_b64url_decode($raw);
    if ($bin === false || strlen($bin) < 28) return null;   // 12 + 16 minimum
    $iv  = substr($bin, 0, 12);
    $tag = substr($bin, 12, 16);
    $ct  = substr($bin, 28);
    $pt  = openssl_decrypt($ct, 'aes-256-gcm', bb_cookie_key(), OPENSSL_RAW_DATA, $iv, $tag);
    if ($pt === false) return null;   // tampered, wrong key, or corrupt
    $data = json_decode($pt, true);
    return is_array($data) ? $data : null;
}

/**
 * Server-side JSON POST. Sends the token both as a Bearer header and (by the
 * caller) in the body, since the content endpoints read it from the body.
 */
function bb_http_post_json(string $url, array $body, ?string $bearer = null): array {
    $headers = ['Content-Type: application/json', 'Accept: application/json'];
    if ($bearer) $headers[] = 'Authorization: Bearer ' . $bearer;

    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => json_encode($body),
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_SSL_VERIFYPEER => true,
        CURLOPT_TIMEOUT        => 30,
        CURLOPT_HTTPHEADER     => $headers,
    ]);
    $raw  = curl_exec($ch);
    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $err  = curl_error($ch);
    curl_close($ch);

    return ['http' => $code, 'json' => json_decode($raw, true), 'raw' => $raw, 'error' => $err];
}

/** Server-side JSON GET with a Bearer token. */
function bb_http_get_json(string $url, ?string $bearer = null): array {
    $headers = ['Accept: application/json'];
    if ($bearer) $headers[] = 'Authorization: Bearer ' . $bearer;

    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_SSL_VERIFYPEER => true,
        CURLOPT_TIMEOUT        => 15,
        CURLOPT_HTTPHEADER     => $headers,
    ]);
    $raw  = curl_exec($ch);
    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $err  = curl_error($ch);
    curl_close($ch);

    return ['http' => $code, 'json' => json_decode($raw, true), 'raw' => $raw, 'error' => $err];
}

/**
 * ---- OAuth round trip -------------------------------------------------------
 * bb_authorize_url() makes a random state, keeps it in a short-lived encrypted
 * cookie and returns the BentBox consent URL. bb_check_state() verifies the
 * state that comes back on the callback, once.
 */
function bb_authorize_url(): string {
    $state = bin2hex(random_bytes(16));
    setcookie(BB_STATE_COOKIE, bb_cookie_encode(['s' => $state, 't' => time()]),
              bb_cookie_opts(time() + BB_STATE_TTL));
    return BB_AUTHORIZE_URL . '?' . http_build_query([
        'client_id'     => BB_CLIENT_ID,
        'redirect_uri'  => BB_REDIRECT_URI,
        'scope'         => BB_SCOPES,
        'state'         => $state,
        'response_type' => 'code',
    ]);
}

function bb_check_state(string $returned): bool {
    $saved = bb_cookie_decode($_COOKIE[BB_STATE_COOKIE] ?? '');
    setcookie(BB_STATE_COOKIE, '', bb_cookie_opts(time() - 3600));   // single use
    return $saved
        && isset($saved['s'], $saved['t'])
        && time() - (int)$saved['t'] <= BB_STATE_TTL
        && hash_equals((string)$saved['s'], $returned);
}

/** Exchange the authorization code for tokens and store them. */
function bb_exchange_code(string $code): array {
    $r = bb_http_post_json(BB_TOKEN_URL, [
        'grant_type'    => 'authorization_code',
        'code'          => $code,
        'client_id'     => BB_CLIENT_ID,
        'client_secret' => BB_CLIENT_SECRET,
        'redirect_uri'  => BB_REDIRECT_URI,
    ]);
    if (empty($r['json']['access_token']) || empty($r['json']['connection_id'])) {
        return ['ok' => false, 'error' => bb_error_text($r, 'Could not connect your BentBox account')];
    }
    bb_store_connection($r['json']);
    return ['ok' => true];
}

/**
 * Refresh the access token using the stored refresh token. Updates the store
 * on success (standard OAuth refresh_token grant).
 */
function bb_refresh(array &$conn): bool {
    if (empty($conn['refresh_token'])) return false;
    $r = bb_http_post_json(BB_TOKEN_URL, [
        'grant_type'    => 'refresh_token',
        'refresh_token' => $conn['refresh_token'],
        'client_id'     => BB_CLIENT_ID,
        'client_secret' => BB_CLIENT_SECRET,
    ]);
    if (!empty($r['json']['access_token'])) {
        bb_store_connection(array_merge($conn, $r['json']));
        $conn = bb_get_connection();
        return true;
    }
    return false;
}

/**
 * Publish, part 1: create the video, then get a presigned upload URL.
 * (Part 2, sending the file, happens in bentbox_upload.php.)
 * Retries once after a token refresh on 403 (expired token).
 */
function bb_create_and_sign(array $meta, bool $allowRefresh = true): array {
    $conn  = bb_get_connection();
    $token = $conn['access_token'];

    $body = [
        'access_token'  => $token,
        'connection_id' => $conn['connection_id'],
        'title'         => $meta['title'],
        'filename'      => $meta['filename'],
        'filesize'      => $meta['filesize'],
        'duration'      => $meta['duration'],
        'description'   => $meta['description'],
        'tags'          => $meta['tags'],
        'price'         => $meta['price'],
        'is_premium'    => $meta['is_premium'],
    ];

    // Consent: BentBox requires sole_performer = true OR a ProntoID
    // release_form_id. Only send what the creator actually chose.
    if (array_key_exists('sole_performer', $meta)) {
        $body['sole_performer'] = (bool)$meta['sole_performer'];
    }
    if (!empty($meta['release_form_id'])) {
        $body['release_form_id'] = $meta['release_form_id'];
    }
    // Optional: false keeps the video as a draft the creator publishes on BentBox.
    if (array_key_exists('publish_on_ready', $meta)) {
        $body['publish_on_ready'] = (bool)$meta['publish_on_ready'];
    }

    $create = bb_http_post_json(BB_CREATE_VIDEO, $body, $token);

    if ($create['http'] === 403 && $allowRefresh && bb_refresh($conn)) {
        return bb_create_and_sign($meta, false); // retry once with fresh token
    }
    if (empty($create['json']['success']) || empty($create['json']['video_id'])) {
        return ['ok' => false, 'stage' => 'create_metadata', 'http' => $create['http'],
                'error' => bb_error_text($create, 'Failed to create metadata')];
    }

    $videoId = $create['json']['video_id'];

    $sign = bb_http_post_json(BB_UPLOAD_URL, [
        'access_token'  => $token,
        'connection_id' => $conn['connection_id'],
        'video_id'      => $videoId,
        'title'         => $meta['title'],
        'price'         => $meta['price'],
        'duration'      => $meta['duration'],
        'description'   => $meta['description'],
        'tags'          => $meta['tags'],
        'adult_content' => $meta['adult_content'],
    ], $token);

    if (empty($sign['json']['success']) || empty($sign['json']['upload_url'])) {
        return ['ok' => false, 'stage' => 'get_upload_url', 'http' => $sign['http'],
                'video_id' => $videoId,
                'error' => bb_error_text($sign, 'Failed to get upload URL')];
    }

    return ['ok' => true, 'video_id' => $videoId,
            'upload_url' => $sign['json']['upload_url'],
            'content_type' => $sign['json']['content_type'] ?? 'video/mp4'];
}

/**
 * Track: read a video's status (GET /v1/content/video/{video_id}).
 * Returns ['ok' => true, 'status' => …, …] or ['ok' => false, 'error' => …].
 * Retries once after a token refresh when the token is rejected.
 */
function bb_video_status(string $videoId, bool $allowRefresh = true): array {
    $conn = bb_get_connection();
    $url  = BB_VIDEO_STATUS . rawurlencode($videoId)
          . '?connection_id=' . rawurlencode($conn['connection_id']);

    $r = bb_http_get_json($url, $conn['access_token']);

    $code = $r['json']['error'] ?? '';
    if ($allowRefresh && in_array($r['http'], [401, 403], true)
        && in_array($code, ['invalid_token', 'unauthorized'], true) && bb_refresh($conn)) {
        return bb_video_status($videoId, false);
    }
    if (empty($r['json']['success'])) {
        return ['ok' => false, 'http' => $r['http'], 'code' => $code,
                'error' => bb_error_text($r, 'Could not read the video status')];
    }

    // Pass through only what the page needs.
    $keep = ['status', 'status_description', 'is_live', 'title',
             'video_page_url', 'video_settings_url', 'checked_at'];
    return ['ok' => true] + array_intersect_key($r['json'], array_flip($keep));
}

/**
 * Opaque, encrypted reference to a video for the browser, so the page can ask
 * for a status without ever holding the raw video_id. Bound to the current
 * connection: a ref from another connection is rejected.
 */
function bb_video_ref(string $videoId): string {
    $conn = bb_get_connection();
    return bb_cookie_encode(['v' => $videoId, 'c' => $conn['connection_id'] ?? '']);
}

function bb_video_from_ref(string $ref): ?string {
    $d = bb_cookie_decode($ref);
    $conn = bb_get_connection();
    if (!$d || empty($d['v']) || ($d['c'] ?? null) !== ($conn['connection_id'] ?? '')) return null;
    return (string)$d['v'];
}

/**
 * Best human-readable error from a BentBox response. The API puts it in
 * error_description; errors passed straight through from a Lambda often only
 * have a readable sentence in "error".
 */
function bb_error_text(array $resp, string $fallback): string {
    $j = $resp['json'] ?? [];
    if (!empty($j['error_description'])) return (string)$j['error_description'];
    if (!empty($j['error']) && strpos((string)$j['error'], ' ') !== false) return (string)$j['error'];
    if (!empty($j['error'])) return $fallback . ' (' . $j['error'] . ')';
    if (!empty($resp['error'])) return $fallback . ': ' . $resp['error'];
    return $fallback . ' (HTTP ' . ($resp['http'] ?? 0) . ')';
}

function bb_json($payload): void {
    header('Content-Type: application/json');
    echo json_encode($payload);
    exit;
}
