<?php

/**
 * DB4js_all.php
 * Vereinheitlichte API für alle bisherigen Einzel-Endpoints:
 *  - Öffentliche Führungen (anmeldungen)
 *  - Sonderführungen (SoFue2 + sofianmeld)
 *  - Termine (fdatum1)
 *  - BEOs (beos)
 *  - Statistiken
 *  - Kalender-Platzhalter
 *
 * Verbesserungen gegenüber Vorgängerversionen:
 *  - PDO Prepared Statements (SQL-Injection Schutz)
 *  - Einheitliche Fehler- / Antwortstruktur (JSON)
 *  - Zentrale Dispatch-Funktion (Command -> Handler)
 *  - Optionale Basic-Auth (Umgebungsvariablen API_USER / API_PASS)
 *  - Eingabevalidierung & Typ-Casts
 *  - Fallback auf config_stern.php wenn keine ENV-Variablen gesetzt
 *  - Saubere Trennung von Repositories / Services / Controller
 *  - Erweiterbares Command-Register (self::COMMANDS)
 *  - Konsistente UTF-8 Header & CORS
 *  - Logging von Fehlern ohne interne Details an Client
 *
 * Rückwärtskompatible Commands (Alte Aufrufe funktionieren weiter):
 *  GET_ANMELD, GET_ONEANMELD, GET_COUNTS, GET_COUNTS_DATE,
 *  INSERT_TLN, UPDATE_TLN, DELETE_TLN,
 *  GET_SOFIANMELD, GET_ONESOFIANMELD, GET_SOFIANMELD_COUNT,
 *  INSERT_SOFIANMELD, UPDATE_SOFIANMELD, DELETE_SOFIANMELD,
 *  GET_TERMINE, GET_ONETERMIN, GET_FID, GET_TIME,
 *  GET_BEOS, GET_ONEBEO,
 *  GET_ONE, GET_ONETERMIN_SOFUE, GET_MANY, UPDATE, UPDATEAFTER, DELETE,
 *  GET_STATISTIK_SOFUE, GET_STATISTIK_ANMELD, GET_STATISTIK_BEO, GET_STATISTIK_GESAMT,
 *  SEND_CONFIRMATION, SENDMAILZUSAGE, SENDMAIL2BEO, SENDMAIL2LISTE,
 *  PUT2KALENDER
 * Zusätzliche neue Commands:
 *  PING -> Gesundheitscheck
 *  LIST_COMMANDS -> gibt alle verfügbaren Kommandos inkl. Beschreibung zurück
 */

// ---- Basis HTTP / CORS ----
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    http_response_code(204);
    exit;
}

// Früher Lebenszeichen-Check: hilft zu unterscheiden, ob der Parser/Include scheitert
if (isset($_GET['alive']) && $_GET['alive'] === '1') {
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode(['alive' => true, 'ts' => date('c')]);
    exit;
}

// ---- Fehlerbehandlung ----
error_reporting(E_ALL);
ini_set('display_errors', 0); // Keine direkten Fehlerausgaben
ini_set('log_errors', '1');
// Schreibe Fehler in ein projektlokales Logfile, damit /var/log nicht nötig ist
// Stelle sicher, dass der Webserver Schreibrechte hat (www-data/apache user)
ini_set('error_log', __DIR__ . '/db4js_error.log');

// Signalisiere dem config_stern.php, dass kein mysqli-Connect durchgeführt werden soll
if (!defined('DB4JS_ALL')) {
    define('DB4JS_ALL', true);
}

// Lokales Logging initialisieren: Logdatei anlegen, wenn möglich
function setupLocalLogging(): void {
    try {
        $logFile = ini_get('error_log');
        if (!$logFile) {
            $logFile = __DIR__ . '/db4js_error.log';
            ini_set('error_log', $logFile);
        }
        if (!file_exists($logFile)) {
            // create empty file
            @file_put_contents($logFile, "");
        }
        // Testeintrag
        @error_log('DB4js_all LOG-INIT OK -> ' . $logFile);
    } catch (Throwable $e) {
        // still attempt a fallback
        @file_put_contents(__DIR__ . '/db4js_error.log', 'DB4js_all LOG-INIT FAIL: ' . $e->getMessage() . "\n", FILE_APPEND);
    }
}
setupLocalLogging();
// Zusätzliche robuste Fehler- und Request-Logs für Server-Diagnose
register_shutdown_function(function () {
    $err = error_get_last();
    if ($err && in_array($err['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
        error_log('DB4js_all FATAL: ' . $err['message'] . ' @' . ($err['file'] ?? '-') . ':' . ($err['line'] ?? '-'));
        // Liefere eine JSON-Antwort, damit Clients keinen leeren Body sehen
        http_response_code(500);
        echo json_encode(['error' => 'Internal error (fatal)', 'details' => 'See server logs'], JSON_UNESCAPED_UNICODE);
    }
});

// Basis-Request-Logging (Content-Type, Methode, Rohdaten) zur 500-Analyse
try {
    $ct = $_SERVER['CONTENT_TYPE'] ?? $_SERVER['HTTP_CONTENT_TYPE'] ?? '';
    $meth = $_SERVER['REQUEST_METHOD'] ?? '';
    $rawPreview = '';
    $rawBody = file_get_contents('php://input');
    if ($rawBody !== false) {
        $rawPreview = substr($rawBody, 0, 512);
    }
    error_log('DB4js_all REQ: method=' . $meth . ' ct=' . $ct . ' raw=' . $rawPreview);
} catch (Throwable $e) {
    // Ignoriere Logging-Fehler
}

// ---- Konstanten für Tabellen ----
const TBL_SOFUE = 'SoFue2';
const TBL_ANMELD = 'anmeldungen';
const TBL_FDATUM = 'fdatum1';
const TBL_SONNEDATUM = 'sonnedatum';
const TBL_BEOS = 'beos';
const TBL_SOFIANMELD = 'sofianmeld';
const TBL_FDATES = 'fdates';
const TBL_SONNEANMELD = 'sonneanmeld';

const URL_KALENDER = 'https://sternwarte-welzheim.de/kalender/';
const URL_BEO_FORM = 'beoform/beoFormular.php?id=';
const LISTE_EMAIL = 'sofue-liste@sternwarte-welzheim.de';

// ---- Utility: Einheitliche Antwort ----
function respond($data, int $status = 200)
{
    http_response_code($status);
    echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
    exit;
}

function respondError(string $message, int $status = 400, array $extra = [])
{
    $payload = array_merge(['error' => $message], $extra);
    respond($payload, $status);
}

// ---- Auth (optional) ----
function ensureAuth(): void
{
    $apiUser = getenv('API_USER');
    $apiPass = getenv('API_PASS');
    if (!$apiUser || !$apiPass) { // Auth deaktiviert wenn ENV nicht gesetzt
        return;
    }
    if (!isset($_SERVER['HTTP_AUTHORIZATION'])) {
        respondError('Unauthorized', 401);
    }
    if (!preg_match('/Basic\s+(.*)$/i', $_SERVER['HTTP_AUTHORIZATION'], $m)) {
        respondError('Invalid auth header', 401);
    }
    $decoded = base64_decode(trim($m[1]));
    if (!$decoded || strpos($decoded, ':') === false) {
        respondError('Invalid credentials format', 401);
    }
    [$user, $pass] = explode(':', $decoded, 2);
    if (!hash_equals($apiUser, $user) || !hash_equals($apiPass, $pass)) {
        respondError('Authentication failed', 401);
    }
}

// ---- Input Laden (JSON bevorzugt, Fallback FormData/Query) ----
$raw = $rawBody ?? file_get_contents('php://input');
$input = [];
if ($raw !== false && strlen(trim($raw)) > 0) {
    $decoded = json_decode($raw, true);
    if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
        $input = $decoded;
    }
}
if (empty($input)) { // Fallback auf $_POST
    $input = $_POST;
}
$method = $_SERVER['REQUEST_METHOD'];

// ---- Command extrahieren ----
$cmd = $input['cmd'] ?? ($method === 'GET' ? ($_GET['cmd'] ?? null) : null);
if ($method === 'GET' && !$cmd) { // einfache GET Health Check
    respond(['status' => 'ok', 'message' => 'API erreichbar']);
}
if (!$cmd) {
    respondError('Command missing', 422);
}

// ---- Datenbank (PDO) ----
class DB
{
    private static $pdo = null; // untyped for PHP 7.2 compatibility

    public static function conn(): PDO
    {
        if (self::$pdo === null) {
//            require_once __DIR__ . '/config_stern.php';
            include "config_stern.php";
            // config_stern.php sollte $host,$dbase,$user,$pass setzen
            $hostEnv = getenv('DB_HOST') ?: ($host ?? 'localhost');
            $nameEnv = getenv('DB_NAME') ?: ($dbase ?? 'sternwarte');
            $userEnv = getenv('DB_USER') ?: ($user ?? 'root');
            $passEnv = getenv('DB_PASS') ?: ($pass ?? '');

            $dsn = "mysql:host=$hostEnv;dbname=$nameEnv;charset=utf8mb4";
            $opt = [
                PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
                PDO::ATTR_EMULATE_PREPARES => false,
            ];
            try {
                self::$pdo = new PDO($dsn, $userEnv, $passEnv, $opt);
            } catch (Throwable $e) {
                error_log('DB CONNECT ERROR: ' . $e->getMessage());
                respondError('Database connection failed', 500);
            }
        }
        return self::$pdo;
    }

    public static function all(string $sql, array $params = []): array
    {
        $st = self::conn()->prepare($sql);
        $st->execute($params);
        return $st->fetchAll();
    }
    //"SELECT * FROM SoFue2 WHERE deleted=0 AND status=? AND wtermin >= NOW() ORDER BY wtermin DESC, id DESC LIMIT ? OFFSET ?"

    public static function one(string $sql, array $params = []): ?array
    {
        $st = self::conn()->prepare($sql);
        $st->execute($params);
        $row = $st->fetch();
        return $row === false ? null : $row;
    }

    public static function exec(string $sql, array $params = []): int
    {
        $st = self::conn()->prepare($sql);
        $st->execute($params);
        return $st->rowCount();
    }

    public static function insertId(): string
    {
        return self::conn()->lastInsertId();
    }
}

// ---- Repositories ----
class RepoAnmeld
{
    public static function getByFid(int $fid): array
    {
        return DB::all("SELECT * FROM " . TBL_ANMELD . " WHERE fid=? ORDER BY angemeldet DESC", [$fid]);
    }
    public static function getByDate(string $date, string $typ): array
    {
        $table = ($typ === 'regular') ? TBL_ANMELD : TBL_SONNEANMELD;
        // expects $date as YYYYMMDD numeric string
        $dateNum = (int)preg_replace('/[^0-9]/', '', (string)$date);
        return DB::all("SELECT * FROM " . $table . " WHERE fdatum=? ORDER BY angemeldet DESC", [$dateNum]);
    }
    public static function getById(int $id, string $typ = ''): ?array
    {
        $table = ($typ === 'sonnen') ? TBL_SONNEANMELD : TBL_ANMELD;
        return DB::one("SELECT * FROM " . $table . " WHERE id=?", [$id]);
    }
    public static function getByName(string $name): array
    {
        return DB::all("SELECT * FROM " . TBL_ANMELD . " WHERE name=? OR vorname=?", [$name, $name]);
    }
    public static function countByFid(int $fid, string $typ): int
    {
        $table = ($typ == 'regular') ? TBL_ANMELD : TBL_SONNEANMELD;
        $r = DB::one("SELECT SUM(anzahl) c FROM " . $table . " WHERE fid=?", [$fid]);
        return (int)($r['c'] ?? 0);
    }
    public static function countByDate(string $date, string $typ): int
    {
        $dateNum = (int)preg_replace('/[^0-9]/', '', (string)$date);
        $table = ($typ == 'regular') ? TBL_ANMELD : TBL_SONNEANMELD;
        $r = DB::one("SELECT SUM(anzahl) c FROM " . $table . " WHERE fdatum=?", [$dateNum]);
        return (int)($r['c'] ?? 0);
    }
    public static function lastAnmeldungAfter(string $date, string $typ): ?int
    {
        $table = ($typ == 'regular') ? TBL_ANMELD : TBL_SONNEANMELD;
        $dateNum = (int)preg_replace('/[^0-9]/', '', (string)$date);
        $r = DB::one("SELECT MAX(fdatum) lastdate FROM " . $table . " WHERE fdatum>=? AND anzahl!=0", [$dateNum]);
        return isset($r['lastdate']) ? (int)$r['lastdate'] : null;
    }
    public static function getNew(string $special, string $date)
    {
        // Implement only the commonly used variant from legacy: 'alllater'
        $dateNum = (int)preg_replace('/[^0-9]/', '', (string)$date);
        if ($special === 'alllater') {
            return DB::all("SELECT * FROM " . TBL_ANMELD . " WHERE fdatum > ? AND name != '-' ORDER BY fdatum", [$dateNum]);
        }
        if ($special === 'normal') {
            return DB::all("SELECT * FROM " . TBL_ANMELD . " WHERE fdatum = ?", [$dateNum]);
        }
        if ($special === 'all') {
            return DB::all("SELECT * FROM " . TBL_ANMELD . " WHERE name != '-'", []);
        }
        if ($special === 'abgesagt') {
            // Legacy had '=1'; be robust and accept non-null values as abgesagt
            return DB::all("SELECT * FROM " . TBL_ANMELD . " WHERE name != '-' AND (abgesagt = 1 OR abgesagt IS NOT NULL)");
        }
        if ($special === 'nichtda') {
            // Keep simple and approximate legacy behavior: older than yesterday and not participated
            return DB::all("SELECT * FROM " . TBL_ANMELD . " WHERE fdatum < ? AND COALESCE(teilgenommen,0)=0 AND name != '-'", [(int)date('Ymd', strtotime('-1 day'))]);
        }
        if ($special === 'zualt') {
            // Interpret date as day-offset
            $days = (int)$date;
            $cut = (int)date('Ymd', strtotime("-$days day"));
            return DB::all("SELECT * FROM " . TBL_ANMELD . " WHERE fdatum <= ? AND name != '-'", [$cut]);
        }
        return [];
    }
    public static function insert(array $d): int
    {
        $sql = "INSERT INTO " . TBL_ANMELD . " (name,vorname,strasse,plz,stadt,telefon,email,anzahl,remarks,fid,angemeldet) VALUES (?,?,?,?,?,?,?,?,?,?,CURDATE())";
        DB::exec($sql, [
            $d['name'],
            $d['vorname'] ?? '',
            $d['strasse'] ?? '',
            (int)($d['plz'] ?? 0),
            $d['stadt'] ?? '',
            $d['telefon'] ?? '',
            $d['email'],
            (int)$d['anzahl'],
            $d['remarks'] ?? '',
            (int)$d['fid']
        ]);
        return (int)DB::insertId();
    }
    public static function update(int $id, array $d): int
    {
        $sql = "UPDATE " . TBL_ANMELD . " SET name=?,vorname=?,strasse=?,plz=?,stadt=?,telefon=?,email=?,anzahl=?,remarks=?,fid=? WHERE id=?";
        return DB::exec($sql, [
            $d['name'],
            $d['vorname'] ?? '',
            $d['strasse'] ?? '',
            (int)($d['plz'] ?? 0),
            $d['stadt'] ?? '',
            $d['telefon'] ?? '',
            $d['email'],
            (int)$d['anzahl'],
            $d['remarks'] ?? '',
            (int)$d['fid'],
            $id
        ]);
    }
    public static function delete(int $id, string $typ): int
    {
        $table = ($typ == 'regular') ? TBL_ANMELD : TBL_SONNEANMELD;
        return DB::exec("DELETE FROM " . $table . " WHERE id=?", [$id]);
    }
    public static function bulkUpdateField(array $ids, string $field, $value): int
    {
        // Whitelist to avoid arbitrary column updates
        $allowed = ['abgesagt', 'teilgenommen', 'remarks'];
        if (!in_array($field, $allowed, true)) {
            return 0;
        }
        $ids = array_values(array_filter(array_map('intval', $ids), function ($v) { return $v > 0; }));
        if (empty($ids)) return 0;
        
        $placeholders = implode(',', array_fill(0, count($ids), '?'));
        $sql = "UPDATE " . TBL_ANMELD . " SET $field=? WHERE id IN ($placeholders)";
        $params = array_merge([$value], $ids);
        return DB::exec($sql, $params);
    }
}

class RepoSoFiAnmeld
{
    public static function getAll(): array
    {
        return DB::all("SELECT * FROM " . TBL_SOFIANMELD . " ORDER BY angemeldet DESC");
    }
    public static function getBySoFue(int $sid): array
    {
        return DB::all("SELECT * FROM " . TBL_SOFIANMELD . " WHERE sofue_id=? ORDER BY angemeldet DESC", [$sid]);
    }
    public static function getById(int $id): ?array
    {
        return DB::one("SELECT * FROM " . TBL_SOFIANMELD . " WHERE id=?", [$id]);
    }
    public static function countBySoFue(int $sid): int
    {
        $r = DB::one("SELECT SUM(anzahl) c FROM " . TBL_SOFIANMELD . " WHERE sofue_id=?", [$sid]);
        return (int)($r['c'] ?? 0);
    }
    public static function insert(array $d): int
    {
        $sql = "INSERT INTO " . TBL_SOFIANMELD . " (name,vorname,strasse,plz,stadt,telefon,email,anzahl,remarks,sofue_id,angemeldet) VALUES (?,?,?,?,?,?,?,?,?,?,CURDATE())";
        DB::exec($sql, [
            $d['name'],
            $d['vorname'] ?? '',
            $d['strasse'] ?? '',
            (int)($d['plz'] ?? 0),
            $d['stadt'] ?? '',
            $d['telefon'] ?? '',
            $d['email'],
            (int)$d['anzahl'],
            $d['remarks'] ?? '',
            (int)$d['sofue_id']
        ]);
        return (int)DB::insertId();
    }
    public static function update(int $id, array $d): int
    {
        $sql = "UPDATE " . TBL_SOFIANMELD . " SET name=?,vorname=?,strasse=?,plz=?,stadt=?,telefon=?,email=?,anzahl=?,remarks=? WHERE id=?";
        return DB::exec($sql, [
            $d['name'],
            $d['vorname'] ?? '',
            $d['strasse'] ?? '',
            (int)($d['plz'] ?? 0),
            $d['stadt'] ?? '',
            $d['telefon'] ?? '',
            $d['email'],
            (int)$d['anzahl'],
            $d['remarks'] ?? '',
            $id
        ]);
    }
    public static function delete(int $id): int
    {
        return DB::exec("DELETE FROM " . TBL_SOFIANMELD . " WHERE id=?", [$id]);
    }
}

class RepoTermine
{
    public static function getAll(bool $includeOld = false): array
    {
        $sql = "SELECT * FROM " . TBL_FDATUM;
        if (!$includeOld) {
            $sql .= " WHERE datum >= CURDATE()";
        }
        $sql .= " ORDER BY datum";
        return DB::all($sql);
    }
    public static function getNextDates(int $amount = 50, ?string $fromDate = null, string $typ = 'regular'): array
    {
        // sanitize and clamp
        $limit = max(1, min((int)$amount, 365));
        // incoming date can be like YYYYMMDD or YYYY-MM-DD, keep only digits
        $fromNum = $fromDate ? (int)preg_replace('/[^0-9]/', '', (string)$fromDate) : (int)date('Ymd');

        if ($typ === 'sonnen') {
            // Sonnenführungen: Tabelle 'sonnedatum' enthält Datumswerte
            $sql = "SELECT datum FROM " . TBL_SONNEDATUM . " WHERE datum >= ? ORDER BY datum ASC LIMIT $limit";
            return DB::all($sql, [$fromNum]);
        }

        // Reguläre Führungen aus fdatum1 mit Zusatzinfos
        $sql = "SELECT wtag, datum, uhrzeit, grp FROM " . TBL_FDATUM . " WHERE datum >= ? ORDER BY datum ASC LIMIT $limit";
        return DB::all($sql, [$fromNum]);
    }
    public static function getById(int $id): ?array
    {
        return DB::one("SELECT * FROM " . TBL_FDATUM . " WHERE id=?", [$id]);
    }
    public static function getByDate(string $date, string $typ): ?array
    {
        $table = ($typ == 'regular') ? TBL_FDATUM : TBL_SONNEDATUM;
        return DB::one("SELECT * FROM " . TBL_FDATUM . " WHERE datum=?", [$date]);
    }
    public static function fidByDate(string $date, string $typ): ?int
    {
        $r = self::getByDate($date, $typ);
        return $r ? (int)$r['id'] : null;
    }
    public static function timeByDate(string $date, string $typ = ''): string
    {
        if ($typ === 'sonnen') return '11 Uhr';
        $r = self::getByDate($date, $typ);
        return $r['uhrzeit'] ?? '';
    }
    public static function decCountByDate(string $date, int $anzahl): int
    {
        $dateNum = (int)preg_replace('/[^0-9]/', '', (string)$date);
        // Ensure positive decrement
        $anz = max(0, (int)$anzahl);
        $sql = "UPDATE " . TBL_FDATUM . " SET count = CASE WHEN count >= ? THEN count - ? ELSE 0 END WHERE datum = ?";
        return DB::exec($sql, [$anz, $anz, $dateNum]);
    }
}

class RepoBeos
{
    public static function getAll(bool $onlyGuides = false, string $fields = '*'): array
    {
        // sanitize requested fields to avoid SQL injection and schema mismatches
        $select = '*';
        if ($fields !== '*') {
            $parts = array_map('trim', explode(',', (string)$fields));
            $cols = [];
            // Known aliases to keep backward compatibility
            $aliasMap = [
                'email' => 'email_1 AS email',
            ];
            foreach ($parts as $p) {
                if ($p === '') continue;
                if (isset($aliasMap[$p])) {
                    $cols[] = $aliasMap[$p];
                    continue;
                }
                if (preg_match('/^[a-zA-Z0-9_]+$/', $p)) {
                    $cols[] = $p;
                }
            }
            if (!empty($cols)) {
                $select = implode(',', $cols);
            }
        }

        $sql = "SELECT $select FROM " . TBL_BEOS;
        if ($onlyGuides) {
            $sql .= " WHERE gruppe != ''";
        }
        $sql .= " ORDER BY name";
        try {
            return DB::all($sql);
        } catch (Throwable $e) {
            // Fallback: falls Feldliste nicht passt, liefere alle Spalten
            error_log('RepoBeos/getAll fallback to *: ' . $e->getMessage());
            $sql = "SELECT * FROM " . TBL_BEOS;
            if ($onlyGuides) {
                $sql .= " WHERE gruppe != ''";
            }
            $sql .= " ORDER BY name";
            return DB::all($sql);
        }
    }
    public static function getById(int $id, string $fields = '*'): ?array
    {
        return DB::one("SELECT $fields FROM " . TBL_BEOS . " WHERE id=?", [$id]);
    }
    public static function getByName(string $name): ?array
    {
        return DB::one("SELECT * FROM " . TBL_BEOS . " WHERE name=?", [$name]);
    }
    public static function vorname(string $name): string
    {
        $r = self::getByName($name);
        return $r['vorname'] ?? '';
    }
    public static function email(string $name): string
    {
        $r = self::getByName($name);
        return $r['email_1'] ?? '';
    }
}

class RepoFdates
{
    public static function groupByDate(string $dateTime): ?string
    {
        $r = DB::one("SELECT grp FROM " . TBL_FDATES . " WHERE dateTime=?", [$dateTime]);
        return $r['grp'] ?? null;
    }
}

class RepoSoFue
{
    public static function getById(int $id): ?array
    {
        return DB::one("SELECT * FROM " . TBL_SOFUE . " WHERE id=?", [$id]);
    }
    public static function getByTermin(string $termin): ?array
    {
        return DB::one("SELECT * FROM " . TBL_SOFUE . " WHERE wtermin=? AND deleted=0", [$termin]);
    }
    public static function getAfterDate(string $date)
    {
        // Accept YYYYMMDD or YYYY-MM-DD; compare by DATE(wtermin)
        $digits = preg_replace('/[^0-9]/', '', (string)$date);
        if (strlen($digits) >= 8) {
            $norm = substr($digits, 0, 4) . '-' . substr($digits, 4, 2) . '-' . substr($digits, 6, 2);
        } else {
            $norm = date('Y-m-d');
        }
        $sql = "SELECT * FROM " . TBL_SOFUE . " WHERE deleted=0 AND DATE(wtermin) > ? ORDER BY wtermin";
        return DB::all($sql, [$norm]);
    }
    public static function getRecords(string $status = 'all', int $rows = 10, int $page = 1, ?string $termin = null): array
    {
        // Lastdate: 9 Monate zurück
        $lastdate = new DateTime();
        $lastdate->sub(new DateInterval('P9M'));
        $lastdateStr = $lastdate->format('Y-m-d');
        
        $params = [];
        $countSql = "SELECT COUNT(*) as count FROM " . TBL_SOFUE . " WHERE deleted=0";
        
        // WHERE Bedingungen aufbauen
        if ($status !== 'all') {
            if ((int)$status === 4) {
                $countSql .= " AND stattgefunden=1";
            } else {
                $countSql .= " AND status=?";
                $params[] = (int)$status;
            }
        }
        // Bei status=4 (stattgefunden) macht 'neu' keinen Sinn, ignoriere termin
        // termin kann 'all', 'neu' sein - nur 'neu' wird behandelt
        if ($termin === 'neu' && (int)$status !== 4) {
            $countSql .= " AND wtermin >= CURDATE() - INTERVAL 1 DAY ";
        }
        
        // Lastdate-Filter auch beim Count hinzufügen
        $countSql .= " AND DATE(wtermin) >= ?";
        $params[] = $lastdateStr;
        
        // Anzahl der Records holen
        $countResult = DB::one($countSql, $params);
        $count = (int)($countResult['count'] ?? 0);
        
        // Anzahl der Seiten berechnen
        $totalPages = $rows > 0 ? ceil($count / $rows) : 1;
        
        // Falls angeforderte Seite > Anzahl der Seiten, letzte Seite verwenden
        if ($page > $totalPages && $totalPages > 0) {
            $page = $totalPages;
        }
        
        // Start-Record berechnen
        $offset = $rows * ($page - 1);
        if ($offset < 0) {
            $offset = 0;
        }
        
        // Daten abrufen mit lastdate-Filter
        $sql = "SELECT * FROM " . TBL_SOFUE . " WHERE deleted=0";
        $dataParams = [];
        
        if ($status !== 'all') {
            if ((int)$status === 4) {
                $sql .= " AND stattgefunden=1";
            } else {
                $sql .= " AND status=?";
                $dataParams[] = (int)$status;
            }
        }
        // Bei status=4 (stattgefunden) macht 'neu' keinen Sinn, ignoriere termin
        // termin kann 'all', 'neu' sein - nur 'neu' wird behandelt
        if ($termin === 'neu' && (int)$status !== 4) {
            $sql .= " AND wtermin >= CURDATE() - INTERVAL 1 DAY ";
        }
        
        // Lastdate-Filter hinzufügen
        $sql .= " AND DATE(wtermin) >= ?";
        $dataParams[] = $lastdateStr;
        
        $sql .= " ORDER BY wtermin DESC, id DESC LIMIT ? OFFSET ?";
        $dataParams[] = $rows;
        $dataParams[] = $offset;
        
        $records = DB::all($sql, $dataParams);
        
        // Response mit Pagination-Info
        return [
            'page' => $page,
            'total' => $totalPages,
            'records' => $count,
            'rows' => $records
        ];
    }
// "SELECT * FROM SoFue2 WHERE deleted=0 AND stattgefunden=1 AND wtermin >= NOW() AND DATE(wtermin) >= ? ORDER BY wtermin DESC, id DESC LIMIT ? OFFSET ?"
    public static function update(int $id, array $d): int
    {
        $sql = "UPDATE " . TBL_SOFUE . " SET mitarbeiter=?,status=?,bemerkung=?,wtermin=?,atermin=?,erledigt_datum=? WHERE id=?";
        return DB::exec($sql, [$d['mitarbeiter'] ?? '', (int)($d['status'] ?? 0), $d['bemerkung'] ?? '', $d['wtermin'] ?? null, $d['atermin'] ?? null, $d['erledigt_datum'] ?? null, $id]);
    }
    public static function updateAfter(int $id, array $d): int
    {
        $fields = [];
        $params = [];
        $map = ['stattgefunden' => 'stattgefunden', 'besucher' => 'anzahl_echt', 'remark' => 'remarks', 'bezahlt' => 'bezahlt', 'wtermin' => 'wtermin', 'status' => 'status'];
        foreach ($map as $in => $col) {
            if (array_key_exists($in, $d) && $d[$in] !== '' && $d[$in] !== null) {
                $fields[] = "$col=?";
                $params[] = in_array($in, ['besucher', 'status', 'stattgefunden']) ? (int)$d[$in] : $d[$in];
            }
        }
        if (!$fields) return 0;
        $params[] = $id;
        $sql = "UPDATE " . TBL_SOFUE . " SET " . implode(',', $fields) . " WHERE id=?";
        return DB::exec($sql, $params);
    }
    public static function delete(int $id): int
    {
        return DB::exec("UPDATE " . TBL_SOFUE . " SET deleted=1 WHERE id=?", [$id]);
    }
}

class RepoStatistik
{
    public static function sofue(int $year): array
    {
        $sql = "SELECT MONTH(wtermin) m, COUNT(*) angefragt, SUM(CASE WHEN status>=2 THEN 1 ELSE 0 END) zugesagt, SUM(CASE WHEN status=1 THEN 1 ELSE 0 END) abgesagt, SUM(CASE WHEN stattgefunden=1 THEN 1 ELSE 0 END) stattgefunden FROM " . TBL_SOFUE . " WHERE YEAR(wtermin)=? AND deleted=0 GROUP BY MONTH(wtermin) ORDER BY m";
        $rows = DB::all($sql, [$year]);
        $base = [];
        for ($i = 1; $i <= 12; $i++) {
            $base[$i] = ['month' => $i, 'angefragt' => 0, 'zugesagt' => 0, 'abgesagt' => 0, 'stattgefunden' => 0];
        }
        foreach ($rows as $r) {
            $base[(int)$r['m']] = ['month' => (int)$r['m'], 'angefragt' => (int)$r['angefragt'], 'zugesagt' => (int)$r['zugesagt'], 'abgesagt' => (int)$r['abgesagt'], 'stattgefunden' => (int)$r['stattgefunden']];
        }
        return ['year' => $year, 'data' => array_values($base)];
    }
    public static function anmeld(int $year): array
    {
        $sql = "SELECT MONTH(f.datum) m, COUNT(DISTINCT f.id) fuehrungen, COALESCE(SUM(a.anzahl),0) teilnehmer FROM " . TBL_FDATUM . " f LEFT JOIN " . TBL_ANMELD . " a ON f.id=a.fid WHERE YEAR(f.datum)=? GROUP BY MONTH(f.datum) ORDER BY m";
        $rows = DB::all($sql, [$year]);
        $base = [];
        for ($i = 1; $i <= 12; $i++) {
            $base[$i] = ['month' => $i, 'fuehrungen' => 0, 'teilnehmer' => 0];
        }
        foreach ($rows as $r) {
            $base[(int)$r['m']] = ['month' => (int)$r['m'], 'fuehrungen' => (int)$r['fuehrungen'], 'teilnehmer' => (int)$r['teilnehmer']];
        }
        return ['year' => $year, 'data' => array_values($base)];
    }
    public static function beo(int $year): array
    {
        $sql = "SELECT mitarbeiter, COUNT(*) anzahl_fuehrungen, SUM(anzahl_echt) gesamt_besucher FROM " . TBL_SOFUE . " WHERE YEAR(wtermin)=? AND deleted=0 AND stattgefunden=1 AND mitarbeiter!='' GROUP BY mitarbeiter ORDER BY anzahl_fuehrungen DESC";
        return DB::all($sql, [$year]);
    }
    public static function gesamt(int $year): array
    {
        $sofue = DB::one("SELECT COUNT(*) gesamt, SUM(CASE WHEN stattgefunden=1 THEN 1 ELSE 0 END) durch, SUM(CASE WHEN stattgefunden=1 THEN anzahl_echt ELSE 0 END) besucher FROM " . TBL_SOFUE . " WHERE YEAR(wtermin)=? AND deleted=0", [$year]);
        $oeff = DB::one("SELECT COUNT(DISTINCT f.id) gesamt, COALESCE(SUM(a.anzahl),0) besucher FROM " . TBL_FDATUM . " f LEFT JOIN " . TBL_ANMELD . " a ON f.id=a.fid WHERE YEAR(f.datum)=?", [$year]);
        return ['year' => $year, 'sonderfuehrungen' => ['gesamt' => (int)($sofue['gesamt'] ?? 0), 'durchgefuehrt' => (int)($sofue['durch'] ?? 0), 'besucher' => (int)($sofue['besucher'] ?? 0)], 'oeffentlich' => ['gesamt' => (int)($oeff['gesamt'] ?? 0), 'besucher' => (int)($oeff['besucher'] ?? 0)]];
    }
}

// ---- Statistik Jahre Repository (StatistikJahre table) ----
class RepoStatistikJahre
{
    const TBL = 'StatistikJahre';

    public static function getByDate(string $datum): ?array
    {
        return DB::one("SELECT * FROM " . self::TBL . " WHERE datum=?", [$datum]);
    }

    public static function getByYear(int $year): array
    {
        $sql = "SELECT * FROM " . self::TBL . " WHERE YEAR(datum)=? ORDER BY datum";
        $data = DB::all($sql, [$year]);

        // Calculate sums
        $sumB = 0;
        $sumA = 0;
        $sumBZ = 0;
        $sumBT = 0;
        foreach ($data as $row) {
            $sumB += ($row['besucherNormal'] ?? 0) + ($row['besucherSonder'] ?? 0) + ($row['besucherToT'] ?? 0);
            $sumA += ($row['fuehrungen'] ?? 0) + ($row['beobachtungen'] ?? 0) + ($row['techdienst'] ?? 0);
            $sumBZ += ($row['beoZeit'] ?? 0);
            $sumBT += ($row['beoTage'] ?? 0);
        }

        // Get Gesamt bemerkung
        $gesamt = DB::one("SELECT bemerkung FROM StatistikGesamt WHERE jahr=?", [$year]);
        $bemG = $gesamt['bemerkung'] ?? '';

        return [
            'data' => $data,
            'sumB' => $sumB,
            'sumA' => $sumA,
            'sumBZ' => $sumBZ,
            'sumBT' => $sumBT,
            'bemG' => $bemG
        ];
    }

    public static function createOrUpdate(array $post): array
    {
        $existing = self::getByDate($post['datum']);

        if (!$existing) {
            // Insert
            $sql = "INSERT INTO " . self::TBL . " (fuehrungen, beobachtungen, techdienst, besucherNormal, besucherSonder, besucherToT, bemerkung, datum, beoZeit, beoTage) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
            DB::exec($sql, [
                $post['fueh'] ?? 0,
                $post['beob'] ?? 0,
                $post['tech'] ?? 0,
                $post['besN'] ?? 0,
                $post['besS'] ?? 0,
                $post['besT'] ?? 0,
                $post['beme'] ?? '',
                $post['datum'],
                $post['beoZ'] ?? 0,
                $post['beoT'] ?? 0
            ]);
        } else {
            // Update
            $sql = "UPDATE " . self::TBL . " SET fuehrungen=?, beobachtungen=?, techdienst=?, besucherNormal=?, besucherSonder=?, besucherToT=?, bemerkung=?, beoZeit=?, beoTage=? WHERE datum=?";
            DB::exec($sql, [
                $post['fueh'] ?? 0,
                $post['beob'] ?? 0,
                $post['tech'] ?? 0,
                $post['besN'] ?? 0,
                $post['besS'] ?? 0,
                $post['besT'] ?? 0,
                $post['beme'] ?? '',
                $post['beoZ'] ?? 0,
                $post['beoT'] ?? 0,
                $post['datum']
            ]);
        }

        return ['datum' => $post['datum']];
    }

    public static function getYearList(): array
    {
        $sql = "SELECT YEAR(datum) as jahr FROM " . self::TBL . " GROUP BY YEAR(datum) ORDER BY YEAR(datum) DESC";
        $rows = DB::all($sql);
        return array_map(function($r) { return (int)$r['jahr']; }, $rows);
    }
}

// ---- Statistik Gesamt Repository (StatistikGesamt table) ----
class RepoStatistikGesamt
{
    const TBL = 'StatistikGesamt';

    public static function getAll(): array
    {
        $data = DB::all("SELECT * FROM " . self::TBL . " ORDER BY jahr DESC");

        // Get last date info
        $lastYearRow = DB::one("SELECT MAX(YEAR(datum)) as lastYear FROM StatistikJahre");
        $lastYear = $lastYearRow['lastYear'] ?? date('Y');

        $fullYear = DB::all("SELECT * FROM StatistikJahre WHERE YEAR(datum)=?", [$lastYear]);

        // Calculate sums
        $sumB = DB::one("SELECT SUM(besucher) as sum FROM " . self::TBL)['sum'] ?? 0;
        $sumA = DB::one("SELECT SUM(aktivitaeten) as sum FROM " . self::TBL)['sum'] ?? 0;

        return [
            'data' => $data,
            'lastDate' => [
                'lastYear' => (int)$lastYear,
                'fullYear' => $fullYear
            ],
            'sumB' => (int)$sumB,
            'sumA' => (int)$sumA
        ];
    }

    public static function getByYear(int $year): ?array
    {
        return DB::one("SELECT * FROM " . self::TBL . " WHERE jahr=?", [$year]);
    }

    public static function createOrUpdate(array $post): array
    {
        $existing = self::getByYear((int)$post['jahr']);

        if (!$existing) {
            // Insert
            $sql = "INSERT INTO " . self::TBL . " (aktivitaeten, besucher, bemerkung, jahr) VALUES (?, ?, ?, ?)";
            DB::exec($sql, [
                $post['suma'] ?? 0,
                $post['sumb'] ?? 0,
                $post['bemG'] ?? '',
                $post['jahr']
            ]);
        } else {
            // Update
            $sql = "UPDATE " . self::TBL . " SET aktivitaeten=?, besucher=?, bemerkung=? WHERE jahr=?";
            DB::exec($sql, [
                $post['suma'] ?? 0,
                $post['sumb'] ?? 0,
                $post['bemG'] ?? '',
                $post['jahr']
            ]);
        }

        return ['datum' => $post['jahr']];
    }
}

// ---- Kalender Repository ----
class RepoKalender
{
    const TBL = 'kalender';


    public static function getEntries(string $start, string $end): array
    {
        $s = date('Ymd', strtotime($start));
        $e = date('Ymd', strtotime($end));
        return DB::all("SELECT * FROM " . self::TBL . " WHERE start >= ? AND start <= ?", [$s, $e]);
    }

    public static function getOneEntry(string $start): array
    {
        $end = new DateTime($start);
        $end->modify('+1 day');
        $end = $end->format('Y-m-d');
        return RepoKalender::getEntries($start, $end);
    }

    public static function insert(array $data): bool
    {
        $sql = "INSERT INTO " . self::TBL . " (start, end, title, description) VALUES (?, ?, ?, ?)";
        DB::exec($sql, [$data['start'], $data['end'] ?? $data['start'], $data['title'], $data['description'] ?? '']);
        return true;
    }

    public static function delete(int $id): bool
    {
        DB::exec("DELETE FROM " . self::TBL . " WHERE id=?", [$id]);
        return true;
    }

    public static function updateBeos(int $id, string $mitarbeiter): bool
    {
        // First, get the existing calendar entry
        $existingEntry = DB::one("SELECT * FROM " . self::TBL . " WHERE id=?", [$id]);

        if (!$existingEntry) {
            error_log("RepoKalender::updateBeos - Calendar entry with ID {$id} not found.");
            return false;
        }

        // Extract the original Sonderführung name from the existing title
        // Expected format: "WK, SF [Sonderführung Name], [Old Mitarbeiter]"
        $oldTitle = $existingEntry['title'];
        $sofueName = '';
        if (preg_match('/^WK, SF (.*), .*$/', $oldTitle, $matches)) {
            $sofueName = trim($matches[1]);
        }

        $newTitle = '';
        if (!empty($sofueName)) {
            $newTitle = "WK, SF {$sofueName}, {$mitarbeiter}";
        } else {
            // Fallback: If we can't extract the original Sonderführung name,
            // we'll try to keep the original structure if possible, or
            // simply create a title indicating the BEO update.
            // For now, let's just make it clear it's a BEO update.
            $newTitle = "Kalender BEO: {$mitarbeiter}"; // More general fallback
            error_log("RepoKalender::updateBeos - Could not parse original SF name from title '{$oldTitle}'. Using generic title fallback.");
        }
        
        $sql = "UPDATE " . self::TBL . " SET title=? WHERE id=?";
        DB::exec($sql, [$newTitle, $id]);
        error_log("Kalender-Eintrag ID {$id} BEOs aktualisiert zu: {$mitarbeiter} (Titel: '{$newTitle}')");
        return true;
    }

    public static function findEntryBySofueIdAndTermin(int $sofueId, string $wtermin): ?int
    {
        // Fetch Sonderführung details to get its name
        $sofue = RepoSoFue::getById($sofueId);
        if (!$sofue) {
            error_log("RepoKalender::findEntryBySofueIdAndTermin - Sofue ID {$sofueId} not found.");
            return null;
        }

        $sofueName = trim($sofue['name']);
        $searchTitlePart = "WK, SF " . $sofueName; // We'll look for this part in the title

        // Convert wtermin to YYYY-MM-DD H:i format for comparison with kalender.start
        $terminDate = new DateTime($wtermin);
        $startDateStr = $terminDate->format('Y-m-d H:i');

        // Find calendar entry that matches the start date and contains the sofue name in its title
        // Use LIKE for title matching because the full title includes the BEO's name which might change.
        $sql = "SELECT id FROM " . self::TBL . " WHERE start = ? AND title LIKE ?";
        $params = [$startDateStr, "%{$searchTitlePart}%"];
        
        $result = DB::one($sql, $params);
        
        return $result ? (int)$result['id'] : null;
    }
}

// ---- Email Service (einfach) ----
class Mailer
{
    public static function sendPlain(string $to, string $subject, string $body, ?string $cc = null): bool
    {
        require_once __DIR__ . '/phpmailer/dosendmail.php';

        $ccList = $cc ? [$cc] : [];
        $result = sendmail(
            $subject,
            'sternwarte.welzheim@gmx.de',
            $body,
            $ccList,
            [],
            [$to]
        );

        if ($result['error']) {
            error_log('Mailer Error: ' . ($result['errortext'] ?? 'Unknown error'));
            return false;
        }

        return true;
    }

    public static function sendAdvanced(array $toList, string $subject, string $body, array $ccList = [], array $bccList = []): bool
    {
        require_once __DIR__ . '/phpmailer/dosendmail.php';

        // sanitize lists
        $toList = array_values(array_filter($toList, function($v) { return is_string($v) && trim($v) !== ''; }));
        $ccList = array_values(array_filter($ccList, function($v) { return is_string($v) && trim($v) !== ''; }));
        $bccList = array_values(array_filter($bccList, function($v) { return is_string($v) && trim($v) !== ''; }));

        if (empty($toList)) {
            return false;
        }

        $result = sendmail(
            $subject,
            'sternwarte.welzheim@gmx.de',
            $body,
            $ccList,
            $bccList,
            $toList
        );

        if ($result['error']) {
            error_log('Mailer Error (adv): ' . ($result['errortext'] ?? 'Unknown error'));
            return false;
        }
        return true;
    }
}

// ---- Command Registry (Beschreibung für LIST_COMMANDS) ----
class Commands
{
    public const MAP = [
        'PING' => 'Health-Check',
        'PING_LOG' => 'Schreibt Testzeile ins Log',
        'READ_LOG' => 'Liest letzte N Zeilen aus lokalem Log',
        'GET_ANMELD' => 'Liste Anmeldungen für fid/Datum',
        'GET_ANMELDNEW' => 'Anmeldungen-Varianten (special/date)',
        'GET_TEILN_ID' => 'Teilnehmer nach id',
        'GET_TEILN_NAME' => 'Teilnehmer nach Name/Vorname',
        'GET_COUNTS' => 'Summe Anmeldungen für fid/Datum',
        'GET_COUNTS_DATE' => 'Summe Anmeldungen für Datum',
        'UPDATE_TLN' => 'Anmeldung ändern',
        'DELETE_TLN' => 'Anmeldung löschen',
        'UPDATE_TLN_BULK' => 'Mehrere Anmeldungen Feld-Update',
        'DELETEONE' => 'Teilnehmer löschen (Alias)',
        'GET_LASTANMELDUNG' => 'Letzte Anmeldung (Datum) ab Start',
        'UPDATECOUNT' => 'Zähler in fdatum1 für Datum reduzieren',
        'GET_TERMINE' => 'Termine öffentliche Führungen',
        'GET_DATES' => 'Nächste Führungstermine (limit + ab Datum)',
        'GET_FID' => 'Fid für Datum',
        'GET_TIME' => 'Uhrzeit für Datum',
        'GET_BEOS' => 'Liste BEOs optional nur Guides',
        'GET_ONEBEO' => 'Ein BEO nach name',
        'GET_ONE' => 'Sonderführung nach id',
        'GET_MANY' => 'Gefilterte Sonderführungen',
        'UPDATE' => 'Sonderführung Standard-Update',
        'UPDATEAFTER' => 'Nachbearbeitung Sonderführung',
        'DELETE' => 'Sonderführung löschen (soft)',
        'GET_STATISTIK_SOFUE' => 'Statistik Sonderführungen Jahr',
        'GET_STATISTIK_ANMELD' => 'Statistik öffentliche Führungen Jahr',
        'GET_STATISTIK_BEO' => 'Statistik BEO Jahr',
        'GET_STATISTIK_GESAMT' => 'Gesamtstatistik Jahr',
        'SENDMYMAIL' => 'Mail mit BCC versenden',
        'SENDMAILZUSAGE' => 'Zusage an Anfragenden',
        'SENDMAIL2BEO' => 'Mail an Mitarbeiter',
        'SENDMAIL2LISTE' => 'Mail an Verteiler',
        'PUT2KALENDER' => 'Kalender-Eintrag',
        'UPDATE_KALENDER_BEO' => 'Kalender-Eintrag BEOS aktualisieren',
        'GET_FDATES' => 'Führungstermine für Kalenderansicht',
        'GET_CALENTRIES' => 'Kalendereinträge abrufen',
        'GET_ONEENTRY' => 'einen Kalendereintrag abrufen',
        'PUT_CALENTRY' => 'Kalendereintrag erstellen',
        'DEL_CALENTRY' => 'Kalendereintrag löschen',
        'GET_YEARS' => 'Liste verfügbare Jahre (Statistik)',
        'GET_ONE_A' => 'Statistik-Eintrag für Datum',
        'GET_ALL_A' => 'Alle Statistik-Einträge für Jahr',
        'CRUP_A' => 'Statistik-Eintrag erstellen/aktualisieren (Jahre)',
        'GET_ALL_G' => 'Gesamtstatistik alle Jahre',
        'GET_ONE_G' => 'Gesamtstatistik für Jahr',
        'CRUP_G' => 'Gesamtstatistik erstellen/aktualisieren',
        'GET_TIME_BY_DATE' => 'Uhrzeit für Datum abrufen',
        'DELETE_ENTRY' => 'Anmeldung löschen (Storno)',
        'GET_FUEHRUNGEN' => 'Führungen in Zeitbereich',
        'UPDATETLNFD' => 'Teilnehmer-Datum aktualisieren',
        'SEND_MAIL_HTML' => 'Mail versenden (Text)',
        'GET_ALLTEILN' => 'Alle Teilnehmer ab Datum',
        'LIST_COMMANDS' => 'Liste aller Kommandos'
    ];
}

// ---- Dispatcher ----
try {
    ensureAuth();
    $typ = $input['typ'] ?? 'regular';

    switch ($cmd) {
        case 'PING':
            respond(['pong' => true, 'timestamp' => date('c')]);

        case 'PING_LOG':
            $logFile = ini_get('error_log') ?: (__DIR__ . '/db4js_error.log');
            error_log('DB4js_all PING_LOG at ' . date('c'));
            $ok = file_exists($logFile);
            respond(['ok' => $ok, 'logfile' => $logFile]);

        case 'READ_LOG':
            $logFile = ini_get('error_log') ?: (__DIR__ . '/db4js_error.log');
            $lines = (int)($input['lines'] ?? 100);
            $lines = max(10, min($lines, 1000));
            if (!is_readable($logFile)) {
                respond(['error' => 'Logfile not readable', 'logfile' => $logFile], 404);
            }
            // Tail simple implementation
            $content = @file($logFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
            $out = [];
            if (is_array($content)) {
                $out = array_slice($content, -$lines);
            }
            respond(['logfile' => $logFile, 'lines' => $lines, 'data' => $out]);

            // Öffentliche Führungen
        case 'GET_ANMELD':
            // Backcompat: if an 8+ digit numeric 'id' is passed, treat it as date (fdatum)
            if (isset($input['fid'])) {
                respond(RepoAnmeld::getByFid((int)$input['fid']));
            }
            if (isset($input['id'])) {
                $digits = preg_replace('/[^0-9]/', '', (string)$input['id']);
                if (strlen($digits) >= 8) {
                    // For sonnen type, use the appropriate table/repository
                    if ($typ === 'sonnen') {
                        // For Sonnenführungen, we need to get registrations by date from anmeldungen table
                        // The anmeldungen table stores all types, so we just query by date
                        respond(RepoAnmeld::getByDate($digits, $typ));
                    } else {
                        respond(RepoAnmeld::getByDate($digits, $typ));
                    }
                } else {
                    respond(RepoAnmeld::getByFid((int)$input['id']));
                }
            }
            respondError('id or fid missing');
        case 'GET_ANMELDNEW':
            $special = $input['special'] ?? 'alllater';
            $date = $input['date'] ?? date('Ymd');
            respond(RepoAnmeld::getNew($special, $date));
        case 'GET_TEILN_ID':
            $r = RepoAnmeld::getById((int)$input['id'], $typ);
            respond($r ? [$r] : []);
        case 'GET_TEILN_NAME':
            respond(RepoAnmeld::getByName($input['name']));
        case 'GET_COUNTS':
            if (isset($input['fdate'])) {
                respond(RepoAnmeld::countByDate($input['fdate'], $typ));
            }
            if (isset($input['date'])) {
                respond(RepoAnmeld::countByDate($input['date'], $typ));
            }
            if (isset($input['fid'])) {
                respond(RepoAnmeld::countByFid((int)$input['fid'], $typ));
            }
            if (isset($input['id'])) {
                respond(RepoAnmeld::countByFid((int)$input['id'], $typ));
            }
            respondError('Missing identifier for GET_COUNTS');
        case 'GET_COUNTS_DATE':
            respond(['count' => RepoAnmeld::countByDate($input['date'], $typ)]);
        case 'UPDATE_TLN':
            if (!isset($input['id'])) respondError('id missing');
            RepoAnmeld::update((int)$input['id'], $input);
            respond(['success' => true]);
        case 'DELETE_TLN':
            RepoAnmeld::delete((int)$input['id'], $input['typ']);
            respond(['success' => true]);
        case 'DELETEONE': // alias for legacy
            RepoAnmeld::delete((int)$input['id'], $input['typ']);
            respond(['success' => true]);
        case 'UPDATE_TLN_BULK':
            if (!isset($input['ids'], $input['field'], $input['values'])) respondError('Missing fields');
            $ids = is_array($input['ids']) ? $input['ids'] : [];
            $val = is_array($input['values']) ? ($input['values'][0] ?? null) : $input['values'];
            if ($val === null) respondError('Missing value');
            $n = RepoAnmeld::bulkUpdateField($ids, (string)$input['field'], $val);
            respond(['success' => $n > 0, 'updated' => $n]);

            // Termine
        case 'GET_TERMINE':
            respond(RepoTermine::getAll(($input['includeOld'] ?? 'false') === 'true'));
        case 'GET_DATES':
            $amount = isset($input['anzahl']) ? (int)$input['anzahl'] : 50;
            $from = $input['date'] ?? date('Ymd');
            respond(RepoTermine::getNextDates($amount, $from, $typ));
        case 'GET_FID':
            respond(['fid' => RepoTermine::fidByDate($input['datum'], $typ)]);
        case 'GET_TIME':
            respond(['time' => RepoTermine::timeByDate($input['date'], $input['typ'] ?? '')]);
        case 'GET_LASTANMELDUNG':
            $val = RepoAnmeld::lastAnmeldungAfter($input['date'] ?? date('Ymd'), $typ);
            respond($val);
        case 'UPDATECOUNT':
            if (!isset($input['date'], $input['anzahl'])) respondError('Missing fields');
            RepoTermine::decCountByDate($input['date'], (int)$input['anzahl']);
            respond(['success' => true]);

            // BEOs
        case 'GET_BEOS':
            $og = $input['onlyguides'] ?? false;
            $onlyGuides = ($og === true) || ($og === 1) || ($og === '1') || ($og === 'true');
            respond(RepoBeos::getAll($onlyGuides, $input['what'] ?? '*'));
        case 'GET_ONEBEO':
            $r = RepoBeos::getByName($input['name']);
            respond($r ?: ['error' => 'Not found']);

            // Sonderführungen
        case 'GET_ONE':
            $r = RepoSoFue::getById((int)$input['id']);
            respond($r ?: ['error' => 'Not found']);
        case 'GET_MANY':
            respond(RepoSoFue::getRecords($input['status'] ?? 'all', (int)($input['rows'] ?? 10), (int)($input['page'] ?? 1), $input['termin'] ?? null));
        case 'UPDATE':
            if (!isset($input['id'])) respondError('id missing');
            $old = RepoSoFue::getById((int)$input['id']);
            RepoSoFue::update((int)$input['id'], $input);
            if ($old && isset($input['mitarbeiter']) && $input['mitarbeiter'] !== ($old['mitarbeiter'] ?? '')) {
                $mail = RepoBeos::email($input['mitarbeiter']);
                if ($mail) {
                    Mailer::sendPlain($mail, 'Sonderführung aktualisiert', 'Sie haben eine aktualisierte Führung am ' . $input['wtermin']);
                }
            }
            respond(['success' => true]);
        case 'UPDATEAFTER':
            if (!isset($input['id'])) respondError('id missing');
            RepoSoFue::updateAfter((int)$input['id'], $input);
            respond(['success' => true]);
        case 'DELETE':
            RepoSoFue::delete((int)$input['id']);
            respond(['success' => true]);

            // Statistiken
        case 'GET_STATISTIK_SOFUE':
            respond(RepoStatistik::sofue((int)($input['year'] ?? date('Y'))));
        case 'GET_STATISTIK_ANMELD':
            respond(RepoStatistik::anmeld((int)($input['year'] ?? date('Y'))));
        case 'GET_STATISTIK_BEO':
            respond(RepoStatistik::beo((int)($input['year'] ?? date('Y'))));
        case 'GET_STATISTIK_GESAMT':
            respond(RepoStatistik::gesamt((int)($input['year'] ?? date('Y'))));

            // Mail
        case 'SENDMYMAIL':
            // Legacy-compatible mail send with BCC list
            $subject = $input['subject'] ?? ($input['betreff'] ?? null);
            if (!$subject) respondError('Missing subject');
            $body = $input['body'] ?? '';
            $to = $input['to'] ?? [];
            if (!is_array($to)) $to = [$to];
            $bcc = $input['bcc'] ?? [];
            if (!is_array($bcc)) $bcc = [$bcc];
            $cc = $input['cc'] ?? [];
            if (!is_array($cc)) $cc = [$cc];
            $ok = Mailer::sendAdvanced($to, $subject, $body, $cc, $bcc);
            respond(['success' => $ok]);
        case 'SENDMAILZUSAGE':
            $info = RepoSoFue::getById((int)$input['id']);
            if (!$info) respondError('Führung nicht gefunden', 404);
            $ma = RepoBeos::getByName($input['mitarbeiter']);
            $ma_name = $ma['name'];
            $ma_vorname = $ma['vorname'];
            $gender = $ma['gender'] == 'm'; 
            $ma_mail = $ma['email_1'];
            $ge1 = $gender ? "unser ehrenamtlicher Mitarbeiter, Herr" : "unsere ehrenamtliche Mitarbeiterin, Frau";
            $ge2 = $gender ? "ihn" : "sie";
            $ge3 = $gender ? "Herrn" : "Frau";
            $dt = date('d.m.Y H:i', strtotime($input['termin']));
            $subject = 'ZUSAGE - Sternführung am ' . $dt . ' Uhr';
            $body = "
    Guten Tag,
    
    für Ihren Wunschtermin, {$dt} Uhr, hat sich {$ge1} {$ma_vorname} {$ma_name} bereit erklärt, 
    die Sonderführung zu übernehmen. Sie erreichen {$ge2} über die e-mail-Adresse: {$ma_mail}

    Um nähere Besuchsmodalitäten zu klären, bitten wir Sie, mit {$ge3} {$ma_name} Kontakt aufzunehmen.

    Wir bitten Sie, die Spende in Höhe von €50.00 auf unten aufgeführtes Konto zu überweisen oder in bar zur Führung mitzubringen.

    Gesellschaft zur Förderung des Planetariums Stuttgart und der Sternwarte Welzheim e.V.
    BANKVERBINDUNG: Deutsche Bank AG Stuttgart
    IBAN DE18 6007 0070 0122 0383 00
    BIC: DEUTDESSXXX

    Mit sternfreundlichen Grüßen
    Reinhard X. Fürst
    Sternwarte Welzheim
            ";
            $ok = Mailer::sendPlain($info['email'], $subject, $body, 'rexfue@gmail.com');
            respond(['success' => $ok]);

        case 'SENDMAIL2BEO':
            $mailNote = trim((string)($input['mail_note'] ?? ''));
            $mailNoteBlock = $mailNote !== '' ? "\n    " . $mailNote . "\n" : "\n";
            $mail = RepoBeos::email($input['ma']);
            $vor = RepoBeos::vorname($input['ma']);
            $dt = date('d.m.Y H:i', strtotime($input['termin']));
            if (!$mail) respondError('Mitarbeiter nicht gefunden', 404);
            $info = RepoSoFue::getByTermin($input['termin']);
            if (!$info) respondError('Führung nicht gefunden', 404);
            $subject = 'Vereinbarte Sonderführung  am ' . date('d.m.Y', strtotime($input['termin']));
            $body = "
Hallo " . $vor .",
 
    vielen Dank für die Bereitschaft, die Sonderführung am {$dt} zu übernehmen.
    Bitte den Termin nicht vergessen und bitte ggf. auch das Teammitglied, das die
    Führung mitmacht, informieren. 
    
    Der Termin wurde in den Sternwartenkalender eingetragen.
    
    Die Kontaktdaten sind auf der Sonderführungsseite ( https://sternwarte-welzheim.de/intern/sofue/sofue.php ) zu finden.

{$mailNoteBlock}
    
    Viele Grüße
    Reinhard

    Diese Meldung wurde automatisch erzeugt. Es kann nicht geantwortet werden.";

           
            $ok = Mailer::sendPlain($mail, $subject, $body, 'rexfue@gmail.com');
            respond(['success' => $ok]);
        case 'SENDMAIL2LISTE':
            $info = RepoSoFue::getById((int)$input['id']);
            if (!$info) respondError('Führung nicht gefunden', 404);
            $mailNote = trim((string)($input['mail_note'] ?? ''));
            $to = $input['to'] ?? LISTE_EMAIL;
            $subject = 'Neue Anfrage Sonderführung am ' . date('d.m.Y', strtotime($info['wtermin']));
            $body = "
Liebe BEOs,

wer kann folgende Sonderführung übernehmen?
---------------------------------------------------------------------------------------------------";

$body = $body . "
Name, Vorname: " . $info['name'] . " " . $info['vorname'] . "
Verein / Organisation : " . $info['verein'] . "
Wunsch - Termin: " . $info['wtermin'] . "
Teilnehmerzahl ca.: " . $info['anzahl'] . " 

Weitere Fragen oder Mitteilungen: 
" . $info['mitteilung'] . " 
Spendenbescheinigung: \t" . $info['spende'] . "
---------------------------------------------------------------------------------------------------";
            if ($mailNote !== '') {
                $body .= "\n\n" . $mailNote . "\n";
            }
            $body .= "\n\nViele Grüße\nReinhard\n";
        $ok = Mailer::sendPlain($to, $subject, $body);
            respond(['success' => $ok]);

        case 'SEND_CONFIRMATION':
            if (!isset($input['to'], $input['subject'], $input['body'])) respondError('Missing fields');
            $ok = Mailer::sendPlain($input['to'],$input['subject'],$input['body']);
            respond(['success' => $ok]);

            // Kalender
        case 'PUT2KALENDER':
            if (!isset($input['id'], $input['termin'], $input['mitarbeiter'])) respondError('Missing fields');
            
            // Sonderführung laden
            $sofue = RepoSoFue::getById((int)$input['id']);
            if (!$sofue) respondError('Sonderführung nicht gefunden', 404);
            
            // Datum aus termin extrahieren und in YYYYMMDD Format konvertieren
            $terminDate = new DateTime($input['termin']);
            $dateStr = $terminDate->format('Y-m-d H:i');
            $endTime = $terminDate->modify('+2hours');
            $endStr =  $endTime->format('Y-m-d H:i');
            
            // Titel mit Mitarbeiter für Kalendereintrag erstellen
            $title = "WK, SF " . trim($sofue['name']) . ", " . $input['mitarbeiter'];
            
            // Kalendereintrag erstellen
            RepoKalender::insert([
                'start' => $dateStr,
                'end' => $endStr,
                'title' => $title,
                'description' => '' 
            ]);
            error_log('Kalender-Eintrag erstellt: ' . $input['id'] . ' ' . $input['termin'] . ' ' . $input['mitarbeiter']);
            respond(['success' => true]);

        case 'UPDATE_KALENDER_BEO':
            if (!isset($input['id'], $input['mitarbeiter'])) respondError('Missing fields for calendar update');
            
            $sofueId = (int)$input['id'];
            $mitarbeiter = $input['mitarbeiter'];

            // Fetch Sonderführung details to get wtermin
            $sofue = RepoSoFue::getById($sofueId);
            if (!$sofue) respondError('Sonderführung not found for calendar update', 404);

            $wtermin = $sofue['wtermin'];
            
            // Find the calendar entry ID based on sofueId and wtermin
            $kalenderId = RepoKalender::findEntryBySofueIdAndTermin($sofueId, $wtermin);

            if (!$kalenderId) {
                error_log("UPDATE_KALENDER_BEO: Could not find calendar entry for Sofue ID {$sofueId} and wtermin {$wtermin}.");
                respondError('Corresponding calendar entry not found.', 404);
            }

            RepoKalender::updateBeos($kalenderId, $mitarbeiter);
            respond(['success' => true]);
        case 'GET_FDATES':
            // Returns führungen for calendar display
            if (!isset($input['start'], $input['end'])) respondError('start and end required');
            // Convert ISO date strings to YYYYMMDD format
            $startObj = new DateTime($input['start']);
            $endObj = new DateTime($input['end']);
            $s = $startObj->format('Ymd');
            $e = $endObj->format('Ymd');
            $sql = "SELECT * FROM " . TBL_FDATUM . " WHERE datum >= ? AND datum <= ? ORDER BY datum ASC";
            $rows = DB::all($sql, [$s, $e]);
            $result = [];
            foreach ($rows as $r) {
                $count = RepoAnmeld::countByDate($r['datum'], 'regular');
                $result[] = [
                    'start' => $r['datum'],
                    'uhr' => substr($r['uhrzeit'] ?? '', 0, 2),
                    'title' => $r['grp'] ?? '',
                    'count' => $count
                ];
            }
            respond($result);
        case 'GET_CALENTRIES':
            if (!isset($input['start'], $input['end'])) respondError('start and end required');
            respond(RepoKalender::getEntries($input['start'], $input['end']));
        case 'GET_ONEENTRY':
            if (!isset($input['date'])) respondError('date required');
            respond(RepoKalender::getOneEntry($input['date']));
        case 'PUT_CALENTRY':
            RepoKalender::insert($input['data']);
            respond(['success' => true]);
        case 'DEL_CALENTRY':
            RepoKalender::delete((int)$input['id']);
            respond(['success' => true]);

            // Statistik - Jahre
        case 'GET_YEARS':
            respond(RepoStatistikJahre::getYearList());
        case 'GET_ONE_A':
            $datum = $input['datum'] ?? respondError('datum missing');
            $result = RepoStatistikJahre::getByDate($datum);
            if ($result) {
                // Add bemG from Gesamt
                $year = substr($datum, 0, 4);
                $gesamt = RepoStatistikGesamt::getByYear((int)$year);
                $result['bemG'] = $gesamt['bemerkung'] ?? '';
            }
            respond($result ?: ['error' => 'Not found']);
        case 'GET_ALL_A':
            $year = (int)($input['jahr'] ?? date('Y'));
            respond(RepoStatistikJahre::getByYear($year));
        case 'CRUP_A':
            if (!isset($input['toInsert'])) respondError('toInsert missing');
            respond(RepoStatistikJahre::createOrUpdate($input['toInsert']));

            // Statistik - Gesamt
        case 'GET_ALL_G':
            respond(RepoStatistikGesamt::getAll());
        case 'GET_ONE_G':
            $year = (int)($input['jahr'] ?? date('Y'));
            $result = RepoStatistikGesamt::getByYear($year);
            respond($result ?: ['error' => 'Not found']);
        case 'CRUP_G':
            if (!isset($input['toInsert'])) respondError('toInsert missing');
            respond(RepoStatistikGesamt::createOrUpdate($input['toInsert']));

            // Storno module commands
        case 'GET_TIME_BY_DATE':
            $dt = $input['dt'] ?? respondError('dt missing');
            $typ = $input['typ'] ?? 'regular';
            if ($typ === 'sonnen') {
                respond(['time' => '11 Uhr']);
            }
            $time = DB::one("SELECT uhrzeit FROM " . TBL_FDATUM . " WHERE datum=?", [$dt]);
            respond(['time' => $time['uhrzeit'] ?? '']);
        case 'DELETE_ENTRY':
            $id = (int)($input['id'] ?? respondError('id missing'));
            DB::exec("DELETE FROM " . TBL_ANMELD . " WHERE id=?", [$id]);
            respond(['success' => true]);
        case 'GET_FUEHRUNGEN':
            $start = $input['start'] ?? respondError('start missing');
            $end = $input['end'] ?? respondError('end missing');
            $typ = $input['typ'] ?? 'regular';
            $table = ($typ === 'sonnen') ? 'sonnedatum' : TBL_FDATUM;
            $sql = "SELECT * FROM $table WHERE datum >= ? AND datum <= ? ORDER BY datum ASC";
            respond(DB::all($sql, [$start, $end]));
        case 'UPDATETLNFD':
            if (!isset($input['id'], $input['fdatum'], $input['fid'])) respondError('Missing fields');
            $sql = "UPDATE " . TBL_ANMELD . " SET fdatum=?, fid=?, abgesagt=NULL WHERE id=?";
            DB::exec($sql, [$input['fdatum'], $input['fid'], $input['id']]);
            respond(['success' => true]);
        case 'SEND_MAIL_HTML':
            if (!isset($input['subject'], $input['to'], $input['body_txt'])) respondError('Missing mail fields');
            require_once __DIR__ . '/phpmailer/dosendmail.php';
            // Note: body_html is ignored because sendmail doesn't support it
            $result = sendmail(
                $input['subject'],
                'noreply@sternwarte-welzheim.de',
                $input['body_txt'],
                [],
                [],
                is_array($input['to']) ? $input['to'] : [$input['to']]
            );
            respond(['success' => !($result['error'] ?? false)]);
        case 'GET_ALLTEILN':
            $fdatum = $input['fdatum'] ?? respondError('fdatum missing');
            $sql = "SELECT * FROM " . TBL_ANMELD . " WHERE fdatum >= ? ORDER BY fid ASC";
            respond(DB::all($sql, [$fdatum]));

        case 'LIST_COMMANDS':
            respond(['commands' => Commands::MAP, 'count' => count(Commands::MAP)]);

        default:
            respondError('Unknown command', 400, ['cmd' => $cmd]);
    }
} catch (Throwable $e) {
    error_log('API ERROR: ' . $e->getMessage() . ' @' . $e->getFile() . ':' . $e->getLine());
    respondError('Internal error', 500);
}
