<?php
/**
 * livekit_token.php — enforced token minting for Itahuka Studio
 *
 * Goal:
 *  - Ensure Guest and CoProducer always connect to the SAME (wss, room) resolved from DB project mapping.
 *  - Prevent room drift: token room grant MUST match resolved projects.room_name.
 *
 * Supports (preferred):
 *   role=guest|operator
 *   code=<join_code>            (guest or operator)
 *   project=<project_id>        (operator; guest optional if you allow)
 *   name=<display name>         (optional)
 *   identity=<identity>         (optional; server will generate if missing)
 *
 * Back-compat (optional):
 *   room=<room_name>            (ONLY used when no code/project is provided)
 *
 * Environment (either real env vars or .env in same directory):
 *   LIVEKIT_API_KEY
 *   LIVEKIT_API_SECRET
 *   LIVEKIT_URL_WS                 (e.g. wss://livekit.itahukamedia.com)
 *
 *   DB_HOST / DB_NAME / DB_USER / DB_PASS
 *   (or MYSQL_HOST / MYSQL_DATABASE / MYSQL_USER / MYSQL_PASSWORD)
 *
 * Security:
 *   - operator role requires a valid session and project ownership (projects.created_by = session user_id)
 *   - guest role requires a valid join_code (projects.join_code), OR falls back to room if allowed and provided
 */

declare(strict_types=1);

header('Content-Type: application/json; charset=utf-8');

/* -------------------- helpers -------------------- */

function b64url_encode(string $data): string {
  return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
function jwt_hs256(array $header, array $payload, string $secret): string {
  $h = b64url_encode(json_encode($header, JSON_UNESCAPED_SLASHES));
  $p = b64url_encode(json_encode($payload, JSON_UNESCAPED_SLASHES));
  $sig = hash_hmac('sha256', $h . '.' . $p, $secret, true);
  return $h . '.' . $p . '.' . b64url_encode($sig);
}
function env_get(string $key): ?string {
  $v = getenv($key);
  if ($v !== false && $v !== '') return $v;
  return null;
}
function load_dotenv(string $path): array {
  if (!is_file($path)) return [];
  $out = [];
  $lines = file($path, FILE_IGNORE_NEW_LINES);
  if (!$lines) return [];
  foreach ($lines as $ln) {
    $ln = trim($ln);
    if ($ln === '' || $ln[0] === '#') continue;
    if (strpos($ln, '=') === false) continue;
    [$k, $v] = explode('=', $ln, 2);
    $k = trim($k);
    $v = trim($v);
    if ($k === '') continue;
    // strip quotes
    if (strlen($v) >= 2) {
      $first = $v[0];
      $last = $v[strlen($v)-1];
      if (($first === '"' && $last === '"') || ($first === "'" && $last === "'")) {
        $v = substr($v, 1, -1);
      }
    }
    $out[$k] = $v;
  }
  return $out;
}
function json_fail(int $code, string $msg, array $extra = []): void {
  http_response_code($code);
  echo json_encode(array_merge(['ok' => false, 'error' => $msg], $extra), JSON_UNESCAPED_SLASHES);
  exit;
}
function str_get(string $k): string {
  return isset($_GET[$k]) ? trim((string)$_GET[$k]) : '';
}

/* -------------------- env load (.env same folder) -------------------- */

$dotenv = load_dotenv(__DIR__ . '/.env');

$apiKey = env_get('LIVEKIT_API_KEY') ?? ($dotenv['LIVEKIT_API_KEY'] ?? null);
$apiSecret = env_get('LIVEKIT_API_SECRET') ?? ($dotenv['LIVEKIT_API_SECRET'] ?? null);
$lkUrlWs = env_get('LIVEKIT_URL_WS') ?? ($dotenv['LIVEKIT_URL_WS'] ?? null) ?? 'wss://livekit.itahukamedia.com';

if (!$apiKey || !$apiSecret) {
  json_fail(500, 'LIVEKIT_API_KEY / LIVEKIT_API_SECRET not set');
}

function env_any(array $keys, array $dotenv): ?string {
  foreach ($keys as $k) {
    $v = env_get($k) ?? ($dotenv[$k] ?? null);
    if ($v !== null && $v !== '') return $v;
  }
  return null;
}

/* -------------------- inputs -------------------- */

$role = strtolower(str_get('role'));
if ($role !== 'operator' && $role !== 'guest') $role = 'guest';

$code = str_get('code');
if ($code === '') $code = str_get('join_code');
if ($code === '') $code = str_get('join');

$projectId = str_get('project');
if ($projectId === '') $projectId = str_get('project_id');
if ($projectId === '') $projectId = str_get('id');

$roomFallback = str_get('room'); // back-compat only if no code/project
$nameIn = str_get('name');
$identityIn = str_get('identity');

$publishParam = str_get('publish');
$subscribeParam = str_get('subscribe');

/* -------------------- session (operator only) -------------------- */

$userId = null;
if ($role === 'operator') {
  // conservative session start, compatible with shared hosting
  if (session_status() !== PHP_SESSION_ACTIVE) {
    @session_start();
  }
  // accept multiple possible session keys
  foreach (['user_id','uid','auth_user_id'] as $k) {
    if (isset($_SESSION[$k]) && is_numeric($_SESSION[$k])) { $userId = (int)$_SESSION[$k]; break; }
  }
  if (!$userId) json_fail(401, 'operator role requires login session');
}

/* -------------------- DB connect (only if code/project provided) -------------------- */

$resolvedRoom = '';
$resolvedProjectId = '';
$resolvedJoinCode = '';
$resolvedOwnerId = null;

$wantDbResolve = ($code !== '' || $projectId !== '');

if ($wantDbResolve) {
  $dbHost = env_any(['DB_HOST','MYSQL_HOST'], $dotenv) ?? 'localhost';
  $dbName = env_any(['DB_NAME','MYSQL_DATABASE'], $dotenv);
  $dbUser = env_any(['DB_USER','MYSQL_USER'], $dotenv);
  $dbPass = env_any(['DB_PASS','MYSQL_PASSWORD'], $dotenv) ?? '';

  if (!$dbName || !$dbUser) {
    json_fail(500, 'DB_NAME/DB_USER not set in env/.env (required for code/project mapping)');
  }

  try {
    $dsn = "mysql:host={$dbHost};dbname={$dbName};charset=utf8mb4";
    $pdo = new PDO($dsn, $dbUser, $dbPass, [
      PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
      PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    ]);
  } catch (Throwable $e) {
    json_fail(500, 'DB connection failed', ['detail' => $e->getMessage()]);
  }

  if ($code !== '') {
    $st = $pdo->prepare("SELECT project_id, name, room_name, join_code, created_by FROM projects WHERE join_code = ? LIMIT 1");
    $st->execute([$code]);
    $row = $st->fetch();
    if (!$row) json_fail(404, 'invalid join code');
  } else {
    $st = $pdo->prepare("SELECT project_id, name, room_name, join_code, created_by FROM projects WHERE project_id = ? LIMIT 1");
    $st->execute([$projectId]);
    $row = $st->fetch();
    if (!$row) json_fail(404, 'invalid project id');
  }

  $resolvedProjectId = (string)($row['project_id'] ?? '');
  $resolvedJoinCode  = (string)($row['join_code'] ?? '');
  $resolvedOwnerId   = isset($row['created_by']) ? (int)$row['created_by'] : null;

  $resolvedRoom = trim((string)($row['room_name'] ?? ''));
  if ($resolvedRoom === '') {
    // deterministic fallback: unique room per project
    $resolvedRoom = $resolvedProjectId !== '' ? $resolvedProjectId : 'demo';
  }

  // operator ownership enforcement
  if ($role === 'operator') {
    if (!$resolvedOwnerId || $resolvedOwnerId !== $userId) {
      json_fail(403, 'project not owned by current user');
    }
  }
} else {
  // no mapping supplied, fall back to room param (legacy)
  if ($roomFallback === '') json_fail(400, 'missing code/project (preferred) or room (legacy)');
  $resolvedRoom = $roomFallback;
}

/* -------------------- permissions -------------------- */

$canPublish = false;
$canSubscribe = true;
$canPublishData = true;

if ($role === 'operator') {
  $canPublish = true;
  $canSubscribe = true;
  $canPublishData = true;
} else {
  // guest defaults: subscribe=true, publish=false (mic/cam controlled by client opts)
  $canPublish = false;
  $canSubscribe = true;
  $canPublishData = true; // needed for "raise hand" / control acks
}

// allow explicit override (rare)
if ($publishParam !== '') $canPublish = ($publishParam === '1' || strtolower($publishParam) === 'true');
if ($subscribeParam !== '') $canSubscribe = !($subscribeParam === '0' || strtolower($subscribeParam) === 'false');

// Enforce operator canPublish regardless of client params
if ($role === 'operator') { $canPublish = true; $canSubscribe = true; $canPublishData = true; }

/* -------------------- identity/name -------------------- */

$identity = $identityIn !== '' ? $identityIn : '';
if ($identity === '') {
  if ($role === 'operator') {
    $identity = 'op_' . $userId . '_' . ($resolvedProjectId !== '' ? $resolvedProjectId : substr(bin2hex(random_bytes(4)),0,8));
  } else {
    $identity = 'guest-' . substr(bin2hex(random_bytes(4)), 0, 8);
  }
}
$name = $nameIn !== '' ? $nameIn : $identity;

/* -------------------- mint token -------------------- */

$now = time();
$exp = $now + 6 * 3600;

$header = ['alg' => 'HS256', 'typ' => 'JWT'];
$payload = [
  'iss' => $apiKey,
  'sub' => $identity,
  'name' => $name,
  'nbf' => $now - 10,
  'exp' => $exp,
  'jti' => bin2hex(random_bytes(8)),
  'video' => [
    'roomJoin' => true,
    'room' => $resolvedRoom,
    'canPublish' => $canPublish,
    'canSubscribe' => $canSubscribe,
    'canPublishData' => $canPublishData,
  ],
];

$token = jwt_hs256($header, $payload, $apiSecret);

/* -------------------- response -------------------- */

echo json_encode([
  'ok' => true,
  'url' => $lkUrlWs,
  'room' => $resolvedRoom,
  'role' => $role,
  'project_id' => $resolvedProjectId ?: null,
  'join_code' => $resolvedJoinCode ?: null,
  'identity' => $identity,
  'name' => $name,
  'token' => $token,
  'expires' => $exp,
], JSON_UNESCAPED_SLASHES);
