<?php
/**
 * Stream-proxy HMAC token signer / verifier.
 *
 * Same wire format as the Python implementation (backend/core/proxy_signer.py)
 * so an admin migrating between the cPanel PHP backend and the FastAPI
 * backend doesn't have to invalidate any open player sessions:
 *
 *   token = base64url(payload_json) . "." . base64url(hmac_sha256(payload_json))
 *
 * Payload schema:
 *   {
 *     "v":   1,
 *     "k":   "live" | "vod" | "seg",
 *     "pid": "shared:abc123",        # shared profile id (NEVER user-owned)
 *     "sid": "12345",                # stream id
 *     "st":  "movie" | "series",     # vod sub-type (vod kind only)
 *     "ext": "mp4",                  # file extension (vod kind only)
 *     "ub":  "http://server.tld/...", # upstream base URL (live kind only)
 *     "u":   "http://server.tld/seg.ts", # full upstream URL (seg kind only)
 *     "exp": 1735689600
 *   }
 *
 * The HMAC is keyed with the JWT secret already used for auth tokens
 * — guarantees a stolen proxy token can't be forged without leaking
 * the master secret.
 */

if (!defined('NDP_PROXY_SIGNER_LOADED')) {
    define('NDP_PROXY_SIGNER_LOADED', true);

    /** Default token TTL — 30 min, matches the Python implementation. */
    define('NDP_PROXY_TOKEN_TTL', 1800);

    function _ndp_proxy_secret(): string {
        // Re-use the project's JWT secret. config.php on cPanel sets
        // a top-level `JWT_SECRET` constant; if that's not present we
        // also accept `NDP_JWT_SECRET` for installs that prefer the
        // namespaced name. Last resort: getenv('JWT_SECRET'). Failing
        // all three throws — a dev who forgot to configure must see
        // the error, NOT silently sign with a guessable default.
        if (defined('JWT_SECRET') && JWT_SECRET !== '')      return (string)JWT_SECRET;
        if (defined('NDP_JWT_SECRET') && NDP_JWT_SECRET !== '') return (string)NDP_JWT_SECRET;
        $env = getenv('JWT_SECRET');
        if ($env && $env !== '') return $env;
        throw new RuntimeException('JWT secret is not configured');
    }

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

    function _ndp_b64u_decode(string $s): string {
        $pad = strlen($s) % 4;
        if ($pad) $s .= str_repeat('=', 4 - $pad);
        $r = base64_decode(strtr($s, '-_', '+/'), true);
        return $r === false ? '' : $r;
    }

    /**
     * Sign a payload and return the URL-safe token string.
     *
     * @param array $payload  Anything JSON-encodable. `exp` is set
     *                        automatically; do NOT pass it yourself.
     * @param int   $ttl      Time-to-live in seconds.
     */
    function ndp_proxy_sign_token(array $payload, int $ttl = NDP_PROXY_TOKEN_TTL): string {
        $payload['exp'] = time() + max(1, $ttl);
        ksort($payload);                       // deterministic key order
        $raw = json_encode($payload, JSON_UNESCAPED_SLASHES);
        if ($raw === false) throw new RuntimeException('payload not JSON-encodable');
        $sig = hash_hmac('sha256', $raw, _ndp_proxy_secret(), true);
        return _ndp_b64u_encode($raw) . '.' . _ndp_b64u_encode($sig);
    }

    /**
     * Verify and decode a token.
     *
     * @return array|null   payload on success, null on any failure
     *                      (caller should turn null into HTTP 403 — do
     *                      NOT log the precise failure reason to the
     *                      client to avoid token-oracle leaks).
     */
    function ndp_proxy_verify_token(?string $token): ?array {
        if (!$token || strpos($token, '.') === false) return null;
        $parts = explode('.', $token, 2);
        if (count($parts) !== 2) return null;
        [$body_b64, $sig_b64] = $parts;
        $raw = _ndp_b64u_decode($body_b64);
        $sig = _ndp_b64u_decode($sig_b64);
        if ($raw === '' || $sig === '') return null;
        try {
            $expected = hash_hmac('sha256', $raw, _ndp_proxy_secret(), true);
        } catch (Throwable $e) {
            return null;
        }
        if (!hash_equals($expected, $sig)) return null;
        $payload = json_decode($raw, true);
        if (!is_array($payload)) return null;
        $exp = $payload['exp'] ?? null;
        if (!is_int($exp) || $exp < time()) return null;
        return $payload;
    }
}
