<?php
declare(strict_types=1);

/*
 * SQLite-API für den Stream-Zeitplan.
 *
 * Optional konfigurierbar:
 *   STREAMPLAN_PASSCODE  Zugangscode (Standard: 1304)
 *   STREAMPLAN_DB_PATH   Absoluter Pfad zur SQLite-Datei
 */

header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('X-Content-Type-Options: nosniff');

$isHttps = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
    || (($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https');

session_name('streamplan_session');
session_set_cookie_params([
    'lifetime' => 0,
    'path' => '/',
    'secure' => $isHttps,
    'httponly' => true,
    'samesite' => 'Strict',
]);
session_start();

const ALLOWED_PERSONS = [
    'zuckerstift', 'hoshizuki', 'raelia', 'frechelexi', 'kitepnp',
    'luna', 'viki', 'allianz', 'astralynia', 'urlaub',
];
const ALLOWED_CONTENT = ['chat', 'gaming', 'speedrun', 'art', 'music', 'irl', 'tourney'];

function sendJson(array $payload, int $status = 200): void
{
    http_response_code($status);
    echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    exit;
}

function sendError(string $message, int $status): void
{
    sendJson(['error' => $message], $status);
}

function requireMethod(string $expected): void
{
    if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== $expected) {
        header('Allow: ' . $expected);
        sendError('Diese HTTP-Methode ist nicht erlaubt.', 405);
    }
}

function requestBody(): array
{
    $raw = file_get_contents('php://input');
    if ($raw === false || trim($raw) === '') {
        return [];
    }

    try {
        $decoded = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
    } catch (JsonException $exception) {
        sendError('Die Anfrage enthält kein gültiges JSON.', 400);
    }

    if (!is_array($decoded)) {
        sendError('Die Anfrage muss ein JSON-Objekt enthalten.', 400);
    }

    return $decoded;
}

function requireLogin(): void
{
    if (empty($_SESSION['streamplan_authenticated'])) {
        sendError('Bitte zuerst mit dem Passcode entsperren.', 401);
    }
}

function requireCsrf(): void
{
    $sent = (string) ($_SERVER['HTTP_X_CSRF_TOKEN'] ?? '');
    $stored = (string) ($_SESSION['streamplan_csrf'] ?? '');
    if ($sent === '' || $stored === '' || !hash_equals($stored, $sent)) {
        sendError('Die Sitzung ist abgelaufen. Bitte lade die Seite neu.', 403);
    }
}

function database(): PDO
{
    static $pdo = null;
    if ($pdo instanceof PDO) {
        return $pdo;
    }

    if (!extension_loaded('pdo_sqlite')) {
        throw new RuntimeException('Die PHP-Erweiterung pdo_sqlite ist nicht installiert.');
    }

    $configuredPath = getenv('STREAMPLAN_DB_PATH');
    $databasePath = ($configuredPath !== false && trim($configuredPath) !== '')
        ? $configuredPath
        : __DIR__ . '/data/streamplan.sqlite';

    $databaseDirectory = dirname($databasePath);
    if (!is_dir($databaseDirectory) && !mkdir($databaseDirectory, 0770, true) && !is_dir($databaseDirectory)) {
        throw new RuntimeException('Das SQLite-Datenverzeichnis konnte nicht erstellt werden.');
    }
    if (!is_writable($databaseDirectory)) {
        throw new RuntimeException('Das SQLite-Datenverzeichnis ist für PHP nicht beschreibbar.');
    }

    $pdo = new PDO('sqlite:' . $databasePath, null, null, [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    ]);
    $pdo->exec('PRAGMA foreign_keys = ON');
    $pdo->exec('PRAGMA journal_mode = WAL');
    $pdo->exec('PRAGMA busy_timeout = 5000');

    $pdo->exec(
        'CREATE TABLE IF NOT EXISTS streams (
            id TEXT PRIMARY KEY,
            person TEXT NOT NULL,
            stream_date TEXT NOT NULL,
            start_time TEXT NOT NULL,
            end_time TEXT NOT NULL,
            game TEXT NOT NULL DEFAULT \'—\',
            note TEXT NOT NULL DEFAULT \'\',
            content TEXT NULL,
            collab_name TEXT NULL,
            collab_link TEXT NULL,
            created_at INTEGER NOT NULL
        )'
    );
    $pdo->exec('CREATE INDEX IF NOT EXISTS streams_date_start_idx ON streams (stream_date, start_time)');
    $pdo->exec(
        'CREATE TABLE IF NOT EXISTS settings (
            setting_key TEXT PRIMARY KEY,
            setting_value TEXT NOT NULL
        )'
    );
    $pdo->exec("INSERT OR IGNORE INTO settings (setting_key, setting_value) VALUES ('group_name', 'STREAM-GRUPPE')");

    return $pdo;
}

function stringLength(string $value): int
{
    return function_exists('mb_strlen') ? mb_strlen($value, 'UTF-8') : strlen($value);
}

function limitedString(mixed $value, string $field, int $maximum, bool $required = false): string
{
    $text = trim((string) ($value ?? ''));
    if ($required && $text === '') {
        sendError($field . ' fehlt.', 422);
    }
    if (stringLength($text) > $maximum) {
        sendError($field . ' ist zu lang (maximal ' . $maximum . ' Zeichen).', 422);
    }
    return $text;
}

function validDate(string $date): bool
{
    $parsed = DateTimeImmutable::createFromFormat('!Y-m-d', $date);
    return $parsed !== false && $parsed->format('Y-m-d') === $date;
}

function validTime(string $time): bool
{
    return preg_match('/^(?:[01][0-9]|2[0-3]):[0-5][0-9]$/', $time) === 1;
}

function rowToEntry(array $row): array
{
    $collab = null;
    if ($row['collab_name'] !== null && $row['collab_name'] !== '') {
        $collab = [
            'name' => $row['collab_name'],
            'link' => $row['collab_link'] ?? '',
        ];
    }

    return [
        'id' => $row['id'],
        'person' => $row['person'],
        'date' => $row['stream_date'],
        'start' => $row['start_time'],
        'end' => $row['end_time'],
        'game' => $row['game'],
        'note' => $row['note'],
        'content' => $row['content'],
        'collab' => $collab,
        'createdAt' => (int) $row['created_at'],
    ];
}

$action = (string) ($_GET['action'] ?? '');

try {
    if ($action === 'unlock') {
        requireMethod('POST');
        $body = requestBody();
        $provided = (string) ($body['passcode'] ?? '');
        $configuredPasscode = getenv('STREAMPLAN_PASSCODE');
        $expected = ($configuredPasscode !== false && $configuredPasscode !== '')
            ? $configuredPasscode
            : '1304';

        if (!hash_equals($expected, $provided)) {
            sendError('Falscher Passcode.', 401);
        }

        session_regenerate_id(true);
        $_SESSION['streamplan_authenticated'] = true;
        $_SESSION['streamplan_csrf'] = bin2hex(random_bytes(32));
        sendJson(['ok' => true, 'csrf' => $_SESSION['streamplan_csrf']]);
    }

    requireLogin();

    if ($action === 'state') {
        requireMethod('GET');
        $pdo = database();
        $rows = $pdo->query(
            'SELECT id, person, stream_date, start_time, end_time, game, note, content,
                    collab_name, collab_link, created_at
             FROM streams
             ORDER BY stream_date ASC, start_time ASC, created_at ASC'
        )->fetchAll();
        $groupName = $pdo->query(
            "SELECT setting_value FROM settings WHERE setting_key = 'group_name'"
        )->fetchColumn();

        sendJson([
            'entries' => array_map('rowToEntry', $rows),
            'groupName' => $groupName !== false ? $groupName : 'STREAM-GRUPPE',
            'csrf' => (string) ($_SESSION['streamplan_csrf'] ?? ''),
        ]);
    }

    if ($action === 'streams' && ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
        requireCsrf();
        $body = requestBody();

        $person = limitedString($body['person'] ?? '', 'Person', 40, true);
        if (!in_array($person, ALLOWED_PERSONS, true)) {
            sendError('Die ausgewählte Person ist ungültig.', 422);
        }

        $date = limitedString($body['date'] ?? '', 'Datum', 10, true);
        $start = limitedString($body['start'] ?? '', 'Startzeit', 5, true);
        $end = limitedString($body['end'] ?? '', 'Endzeit', 5, true);
        if (!validDate($date) || !validTime($start) || !validTime($end)) {
            sendError('Datum oder Uhrzeit ist ungültig.', 422);
        }

        $game = limitedString($body['game'] ?? '', 'Spiel / Titel', 120);
        $note = limitedString($body['note'] ?? '', 'Notiz', 300);
        $content = limitedString($body['content'] ?? '', 'Content-Typ', 30);
        if ($content !== '' && !in_array($content, ALLOWED_CONTENT, true)) {
            sendError('Der ausgewählte Content-Typ ist ungültig.', 422);
        }

        $collabName = '';
        $collabLink = '';
        if (isset($body['collab']) && is_array($body['collab'])) {
            $collabName = limitedString($body['collab']['name'] ?? '', 'Collab-Partner:in', 120, true);
            $collabLink = limitedString($body['collab']['link'] ?? '', 'Collab-Link', 500);
            if ($collabLink !== '') {
                $scheme = strtolower((string) parse_url($collabLink, PHP_URL_SCHEME));
                if (!filter_var($collabLink, FILTER_VALIDATE_URL) || !in_array($scheme, ['http', 'https'], true)) {
                    sendError('Der Collab-Link ist keine gültige HTTP(S)-Adresse.', 422);
                }
            }
        }

        $entry = [
            'id' => 'e_' . bin2hex(random_bytes(12)),
            'person' => $person,
            'date' => $date,
            'start' => $start,
            'end' => $end,
            'game' => $game !== '' ? $game : '—',
            'note' => $note,
            'content' => $content !== '' ? $content : null,
            'collab' => $collabName !== '' ? ['name' => $collabName, 'link' => $collabLink] : null,
            'createdAt' => (int) round(microtime(true) * 1000),
        ];

        $statement = database()->prepare(
            'INSERT INTO streams (
                id, person, stream_date, start_time, end_time, game, note, content,
                collab_name, collab_link, created_at
             ) VALUES (
                :id, :person, :stream_date, :start_time, :end_time, :game, :note, :content,
                :collab_name, :collab_link, :created_at
             )'
        );
        $statement->execute([
            ':id' => $entry['id'],
            ':person' => $entry['person'],
            ':stream_date' => $entry['date'],
            ':start_time' => $entry['start'],
            ':end_time' => $entry['end'],
            ':game' => $entry['game'],
            ':note' => $entry['note'],
            ':content' => $entry['content'],
            ':collab_name' => $entry['collab']['name'] ?? null,
            ':collab_link' => $entry['collab']['link'] ?? null,
            ':created_at' => $entry['createdAt'],
        ]);

        sendJson(['entry' => $entry], 201);
    }

    if ($action === 'streams' && ($_SERVER['REQUEST_METHOD'] ?? '') === 'DELETE') {
        requireCsrf();
        $body = requestBody();
        $id = limitedString($body['id'] ?? '', 'Stream-ID', 80, true);
        $statement = database()->prepare('DELETE FROM streams WHERE id = :id');
        $statement->execute([':id' => $id]);
        if ($statement->rowCount() < 1) {
            sendError('Der Stream wurde nicht gefunden.', 404);
        }
        sendJson(['ok' => true]);
    }

    if ($action === 'group') {
        requireMethod('PATCH');
        requireCsrf();
        $body = requestBody();
        $groupName = limitedString($body['groupName'] ?? '', 'Gruppenname', 60, true);
        $groupName = function_exists('mb_strtoupper')
            ? mb_strtoupper($groupName, 'UTF-8')
            : strtoupper($groupName);

        $statement = database()->prepare(
            "INSERT INTO settings (setting_key, setting_value) VALUES ('group_name', :name)
             ON CONFLICT(setting_key) DO UPDATE SET setting_value = excluded.setting_value"
        );
        $statement->execute([':name' => $groupName]);
        sendJson(['groupName' => $groupName]);
    }

    if ($action === 'logout') {
        requireMethod('POST');
        requireCsrf();
        $_SESSION = [];
        session_destroy();
        sendJson(['ok' => true]);
    }

    sendError('API-Endpunkt nicht gefunden.', 404);
} catch (Throwable $exception) {
    error_log('Stream-Zeitplan API: ' . $exception->getMessage());
    $safeConfigurationErrors = [
        'Die PHP-Erweiterung pdo_sqlite ist nicht installiert.',
        'Das SQLite-Datenverzeichnis konnte nicht erstellt werden.',
        'Das SQLite-Datenverzeichnis ist für PHP nicht beschreibbar.',
    ];
    $publicMessage = in_array($exception->getMessage(), $safeConfigurationErrors, true)
        ? $exception->getMessage()
        : 'Interner Serverfehler beim Zugriff auf SQLite.';
    sendError($publicMessage, 500);
}
