import express from "express";
import http from "http";
import { WebSocketServer } from "ws";
import mysql from "mysql2/promise";
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import path from "path";
import fs from "fs";
import { fileURLToPath } from "url";
import { exec } from "child_process";
import { AccessToken } from "livekit-server-sdk";

import crypto from "crypto";

// -------------------------------------------------------------
// SICHERHEITSNETZ: fängt Fehler ab, die sonst den kompletten
// Server-Prozess crashen würden (z.B. ein setInterval mit async-Code
// ohne eigenes try/catch, dessen Promise abgelehnt wird - das crasht
// in neueren Node-Versionen standardmäßig den GESAMTEN Prozess, auch
// wenn nur EIN Spieler/EINE Aktion betroffen war). Loggt den Fehler
// nur, statt den Server (und damit alle Verbindungen) mitzureißen
// -------------------------------------------------------------
process.on("unhandledRejection", (reason) => {
    console.error("[Sicherheitsnetz] Unbehandelte Promise-Ablehnung:", reason);
});
process.on("uncaughtException", (err) => {
    console.error("[Sicherheitsnetz] Unbehandelte Ausnahme:", err);
});

// Liest den Secret aus der Umgebungsvariable JWT_SECRET (empfohlen für den echten
// Betrieb, siehe Hinweis unten). Ist keine gesetzt, wird beim Start EINMALIG ein
// zufälliger Wert erzeugt - sicherer als ein fest einprogrammierter Wert, aber
// ACHTUNG: bereits ausgestellte Logins werden dann bei jedem Neustart ungültig,
// da sich der Secret jedes Mal ändert. Für den Produktivbetrieb daher unbedingt
// eine eigene JWT_SECRET-Umgebungsvariable setzen (z.B. via .env oder PM2-Config),
// z.B. mit: node -e "console.log(require('crypto').randomBytes(48).toString('hex'))"
const JWT_SECRET = process.env.JWT_SECRET || crypto.randomBytes(48).toString("hex");
if (!process.env.JWT_SECRET) {
    console.warn("⚠️  Keine JWT_SECRET-Umgebungsvariable gesetzt - es wird ein zufälliger, nur für diesen Prozess gültiger Wert verwendet. Alle Logins werden beim nächsten Neustart ungültig. Für den Dauerbetrieb bitte JWT_SECRET fest setzen (siehe Kommentar im Code).");
}

// -------------------------------------------------------------
// SPRACHCHAT (LiveKit) - Zugangsdaten deines LiveKit-Servers (Server 2)
// -------------------------------------------------------------
const LIVEKIT_URL = "wss://voice.borderville.de";
const LIVEKIT_API_KEY = "APIoWRgTHrJaBrn";
const LIVEKIT_API_SECRET = "ffJS1Ke4OPupb7iZDUm5wWqs4wfdR1sLUKHHtCJrN4GA";

// -------------------------------------------------------------
// EINSTELLUNGEN (aus der DB, per Admin-Oberfläche änderbar)
// -------------------------------------------------------------
let gameSettings = {};

// -------------------------------------------------------------
// SEITEN-KONFIGURATION (Text-Einstellungen wie Webseiten-Titel -
// getrennt von gameSettings, weil das immer in Zahlen umwandelt)
// -------------------------------------------------------------
let siteConfig = {};
const SITE_CONFIG_DEFAULTS = {
    site_title: "GTA Multiplayer"
};

async function loadSiteConfig() {
    siteConfig = {};
    const [rows] = await db.query("SELECT * FROM site_config");
    for (const row of rows) siteConfig[row.config_key] = row.config_value;
    console.log("Seiten-Konfiguration geladen:", Object.keys(siteConfig).length);
}

function getSiteConfig(key) {
    if (siteConfig[key] !== undefined) return siteConfig[key];
    return SITE_CONFIG_DEFAULTS[key];
}

// -------------------------------------------------------------
// DISCORD-WEBHOOKS (News, Changelog, Neustart-Ankündigungen)
// -------------------------------------------------------------
let discordWebhooks = {}; // hook_key -> webhook_url

async function loadDiscordWebhooks() {
    discordWebhooks = {};
    const [rows] = await db.query("SELECT * FROM discord_webhooks");
    for (const row of rows) discordWebhooks[row.hook_key] = row.webhook_url;
    console.log("Discord-Webhooks geladen:", Object.keys(discordWebhooks).length);
}

async function sendDiscordWebhook(key, embed) {
    const url = discordWebhooks[key];
    if (!url) return; // kein Webhook für diesen Kanal eingerichtet

    try {
        const res = await fetch(url, {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ embeds: [embed] })
        });
        if (!res.ok) {
            console.error(`Discord-Webhook (${key}) fehlgeschlagen: HTTP ${res.status}`);
        }
    } catch (err) {
        console.error(`Discord-Webhook (${key}) fehlgeschlagen:`, err.message);
    }
}

// -------------------------------------------------------------
// DISCORD: Online-Status - bearbeitet immer dieselbe Nachricht,
// statt bei jeder Änderung eine neue zu posten
// -------------------------------------------------------------
let onlineStatusMessageId = null;
let lastOnlineStatusUpdate = 0;

async function updateDiscordOnlineStatus() {
    const url = discordWebhooks["online_status"];
    if (!url) return;

    const count = playersOnline.size;
    const names = [...playersOnline.values()].map(p => p.username).sort();
    const embed = {
        title: "🟢 Server-Status",
        description: `**${count}** Spieler online` + (names.length ? `\n${names.map(n => `• ${n}`).join("\n")}` : ""),
        color: count > 0 ? 0x2c7a3d : 0x555555,
        timestamp: new Date().toISOString(),
        footer: { text: "Letzte Aktualisierung" }
    };

    try {
        if (onlineStatusMessageId) {
            const editRes = await fetch(`${url}/messages/${onlineStatusMessageId}`, {
                method: "PATCH",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({ embeds: [embed] })
            });
            if (editRes.ok) return;
            onlineStatusMessageId = null; // Nachricht wurde wohl gelöscht - unten neu erstellen
        }

        const createRes = await fetch(`${url}?wait=true`, {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ embeds: [embed] })
        });
        if (createRes.ok) {
            const created = await createRes.json();
            onlineStatusMessageId = created.id;
        } else {
            console.error(`Discord Online-Status fehlgeschlagen: HTTP ${createRes.status}`);
        }
    } catch (err) {
        console.error("Discord Online-Status fehlgeschlagen:", err.message);
    }
}

// Bei Verbindungen/Trennungen aktualisieren, aber gedrosselt (max. alle 15s),
// damit viele gleichzeitige Logins/Logouts Discord nicht zuspammen
function maybeUpdateDiscordOnlineStatus() {
    const now = Date.now();
    if (now - lastOnlineStatusUpdate < 15000) return;
    lastOnlineStatusUpdate = now;
    updateDiscordOnlineStatus();
}


const SETTINGS_DEFAULTS = {
    salary_interval_minutes: 60,
    hunger_decay_per_tick: 0.002,
    thirst_decay_per_tick: 0.004,
    health_regen_per_tick: 0.01,
    health_decay_per_tick: 0.03,
    shop_owner_cut_percent: 20,
    gas_owner_cut_percent: 20,
    tow_fee: 50,
    car_collision_damage_factor: 3,
    pedestrian_damage_factor: 9,
    insurance_fee_interval_minutes: 60,
    insurance_fee_amount: 20,
    insurance_repair_discount_percent: 80,
    tax_interval_minutes: 60,
    tax_percent: 2,
    tuning_cost_per_level: 800,
    tuning_bonus_per_level_percent: 8,
    tuning_max_level: 5,
    tuning_paint_cost: 300,
    npc_traffic_enabled: 1,
    clothing_price: 150,
    trailer_price: 800,
    plate_price: 300,
    mechanic_fee_per_percent: 4,
    house_rent_interval_minutes: 60,
    max_inventory_weight: 50,
    cargo_drop_enabled: 1,
    cargo_drop_interval_minutes: 10,
    cargo_drop_chance_percent: 40,
    cargo_drop_expiry_minutes: 5
};

function getSetting(key) {
    if (gameSettings[key] !== undefined) return Number(gameSettings[key]);
    return SETTINGS_DEFAULTS[key];
}

// -------------------------------------------------------------
// KOPFGELD-SYSTEM
// -------------------------------------------------------------
const bounties = new Map(); // playerId -> amount

// -------------------------------------------------------------
// ZUFALLS-EVENTS: Boden-Funde (z.B. LKW verliert Ladung auf der Straße)
// -------------------------------------------------------------
const groundDrops = new Map(); // dropId -> { id, world, x, y, itemId, amount, label }
let dropIdCounter = 1;

function getGroundDropsForWorld(world) {
    const list = [];
    for (const [, d] of groundDrops) {
        if (d.world === world) list.push({ id: d.id, x: d.x, y: d.y, label: d.label });
    }
    return list;
}

// -------------------------------------------------------------
// STRASSENSPERREN: Polizei kann an ihrer Position eine Sperre
// aufstellen, die Fußgänger UND Fahrzeuge blockiert - für
// Verfolgungsjagden/Straßensperren-Taktiken. Nur in Erinnerung
// (kein DB-Eintrag nötig, verschwinden beim Server-Neustart)
// -------------------------------------------------------------
const roadblocks = new Map(); // id -> { world, x, y, placedBy }
let nextRoadblockId = 1;
const ROADBLOCK_RADIUS = 30; // px, ungefähr eine Kachel

function broadcastRoadblockUpdate(world) {
    const list = [...roadblocks.values()].filter(r => r.world === world);
    const msg = JSON.stringify({ type: "roadblock_update", roadblocks: list });
    for (const [, p] of playersOnline) {
        if (p.state.world === world) p.ws.send(msg);
    }
}

function isBlockedByRoadblock(world, x, y) {
    for (const [, r] of roadblocks) {
        if (r.world !== world) continue;
        if (Math.hypot(x - r.x, y - r.y) < ROADBLOCK_RADIUS) return true;
    }
    return false;
}

function getRoadblocksForWorld(world) {
    return [...roadblocks.values()].filter(r => r.world === world);
}

function broadcastGroundDropUpdate(world) {
    const msg = JSON.stringify({ type: "ground_drop_update", drops: getGroundDropsForWorld(world) });
    for (const [, p] of playersOnline) {
        if (p.state.world === world) p.ws.send(msg);
    }
}

// -------------------------------------------------------------
// Schickt allen Spielern, die sich gerade in "world" befinden, eine
// frische map_data-Nachricht - wird nach jeder neuen/gelöschten
// Standort-Erstellung aufgerufen (z.B. übers Admin-Panel oder /setpoint
// im Spiel), damit neue Punkte SOFORT sichtbar sind, ohne dass die
// Welt neu betreten werden muss
// -------------------------------------------------------------
// -------------------------------------------------------------
// LÖSCH-PROTOKOLL: merkt sich vor jeder Admin-Löschung die komplette
// Zeile, damit sie über /api/admin/undo_last_delete rückgängig
// gemacht werden kann (z.B. falls aus Versehen ein Standort gelöscht
// wurde). Generisch für alle Tabellen - loggt einfach die Zeile als
// JSON, unabhängig von der genauen Spaltenstruktur
// -------------------------------------------------------------
async function logDeletionForUndo(tableName, idColumn, idValue) {
    try {
        const [rows] = await db.query(`SELECT * FROM ${tableName} WHERE ${idColumn}=?`, [idValue]);
        if (rows.length > 0) {
            await db.query(
                "INSERT INTO admin_deletion_log (table_name, row_data) VALUES (?, ?)",
                [tableName, JSON.stringify(rows[0])]
            );
        }
    } catch (err) {
        // Protokollieren darf die eigentliche Löschung nie verhindern -
        // lieber ohne Undo-Möglichkeit löschen als gar nicht löschen können
        console.error(`[Löschprotokoll] Fehler beim Protokollieren von ${tableName}:`, err.message);
    }
}

function broadcastMapDataToWorld(world) {
    const worldMap = maps[world];
    if (!worldMap) return;

    for (const [, p] of playersOnline) {
        if (p.state.world !== world) continue;

        p.ws.send(JSON.stringify({
            type: "map_data",
            tiles: worldMap.tiles,
            tileRot: worldMap.tileRot || null,
            doors: worldMap.doors || [],
            objects: worldMap.objects || [],
            shops: getShopsForWorld(world),
            garages: getGaragesForWorld(world),
            jobcenters: getJobsForWorld(world),
            gasStations: getGasStationsForWorld(world),
            repairShops: getRepairShopsForWorld(world),
            jobPoints: getJobPointsForPlayer(world, p.state.jobRankId),
            houses: getHousesForWorld(world),
            taxiStands: getTaxiStandsForWorld(world),
            hospitals: getHospitalsForWorld(world),
            prisons: getPrisonsForWorld(world),
            territoryZones: getZonesForWorld(world),
            impoundLots: getImpoundLotsForWorld(world),
            fireStations: getFireStationsForWorld(world),
            harvestSpots: getHarvestSpotsForWorld(world),
            clothingShops: getClothingShopsForWorld(world),
            insuranceOffices: getInsuranceOfficesForWorld(world),
            plateOffices: getPlateOfficesForWorld(world),
            trailerShops: getTrailerShopsForWorld(world),
            highwayLinks: getHighwayLinksForWorld(world),
            blackMarketSpots: getBlackMarketSpotsForWorld(world),
roadblocks: getRoadblocksForWorld(world),
            groundDrops: getGroundDropsForWorld(world),
            processSpots: getProcessSpotsForWorld(world),
            dealerSpots: getActiveDealerSpotsForWorld(world),
            fires: getFiresForWorld(world),
            atms: worldMap.atms || [],
            spawn: worldMap.spawn
        }));
    }
}

async function trySpawnCargoDrop() {
    if (!getSetting("cargo_drop_enabled")) return;

    // Nur in Welten mit tatsächlich anwesenden Spielern spawnen, keine leeren Straßen bemühen
    const activeWorlds = new Set();
    for (const [, p] of playersOnline) activeWorlds.add(p.state.world);
    if (activeWorlds.size === 0) return;

    const worldList = [...activeWorlds];
    const world = worldList[Math.floor(Math.random() * worldList.length)];
    const map = maps[world];
    if (!map || !map.tiles || !map.tiles[0]) return;

    // Zufällige Straßen-Kachel suchen
    const cols = map.tiles[0].length;
    const rows = map.tiles.length;
    let spot = null;

    for (let i = 0; i < 40; i++) {
        const tx = Math.floor(Math.random() * cols);
        const ty = Math.floor(Math.random() * rows);
        const tileId = map.tiles[ty][tx];
        const cfg = tileConfig[tileId];
        if (cfg && !cfg.collision && cfg.isRoad) {
            spot = { x: tx * 32 + 16, y: ty * 32 + 16 };
            break;
        }
    }
    if (!spot) return; // keine Straßen-Kacheln markiert

    const [itemRows] = await db.query("SELECT id, name FROM items ORDER BY RAND() LIMIT 1");
    if (itemRows.length === 0) return;
    const item = itemRows[0];

    const dropId = dropIdCounter++;
    const amount = 1 + Math.floor(Math.random() * 3);
    groundDrops.set(dropId, {
        id: dropId, world, x: spot.x, y: spot.y,
        itemId: item.id, amount, label: item.name
    });

    broadcastGroundDropUpdate(world);
    broadcastToAll(`🚛 Ein LKW hat Ladung verloren! "${item.name}" liegt irgendwo auf der Straße in "${world}" (schau aufs 📦-Symbol auf der Karte).`, true);

    const expiryMs = getSetting("cargo_drop_expiry_minutes") * 60 * 1000;
    setTimeout(() => {
        if (groundDrops.has(dropId)) {
            groundDrops.delete(dropId);
            broadcastGroundDropUpdate(world);
        }
    }, expiryMs);
}

// Achievement-Titel (nur die, die einen title_text gesetzt haben)
const achievementTitles = new Map(); // achievement id -> title_text

async function loadAchievementTitles() {
    achievementTitles.clear();
    const [rows] = await db.query("SELECT id, title_text FROM achievements WHERE title_text IS NOT NULL AND title_text != ''");
    for (const row of rows) achievementTitles.set(row.id, row.title_text);
    console.log("Achievement-Titel geladen:", achievementTitles.size);
}

async function loadBounties() {
    bounties.clear();
    const [rows] = await db.query("SELECT * FROM bounties WHERE amount > 0");
    for (const row of rows) bounties.set(row.target_id, row.amount);
    console.log("Kopfgelder geladen:", bounties.size);
}

// -------------------------------------------------------------
// MOBILTELEFON: ANRUFE (nutzt dieselbe LiveKit-Infrastruktur wie der
// Nahbereichs-Sprachchat, aber ein privater Zwei-Personen-Raum statt
// dem Welt-Raum - funktioniert unabhängig von Entfernung/Welt)
// -------------------------------------------------------------
const pendingCalls = new Map(); // callId -> { callerId, targetId, room, timeoutHandle }
let callIdCounter = 1;
const CALL_RING_TIMEOUT_MS = 25000;

// -------------------------------------------------------------
// SERVER-EVENTS (Admin-auslösbar, z.B. Doppel-XP-Wochenende, Rabatt-Aktion)
// -------------------------------------------------------------
const activeEvents = {
    doubleXp: { active: false, endsAt: 0, label: "Doppel-XP" },
    discount: { active: false, endsAt: 0, percent: 0, label: "Rabatt-Aktion" }
};

function isEventActive(type) {
    const ev = activeEvents[type];
    if (!ev || !ev.active) return false;
    if (Date.now() > ev.endsAt) {
        ev.active = false;
        return false;
    }
    return true;
}

function getActiveDiscountPercent() {
    return isEventActive("discount") ? activeEvents.discount.percent : 0;
}

function broadcastEventUpdate() {
    const msg = JSON.stringify({ type: "event_update", events: activeEvents });
    for (const [, p] of playersOnline) {
        try { p.ws.send(msg); } catch {}
    }
}

const APP_VERSION = "1.0.0"; // hier bei jedem Release manuell hochzählen

// Erlaubte Skins (Emoji-Avatare) - "none" = einfaches farbiges Rechteck
const ALLOWED_SKINS = ["none", "🧑", "👮", "👷", "🤠", "🧙", "🤖", "👽", "🐱", "🐶", "🦊", "🥷", "🧛"];

// Gemalte Skins (Katalog, wie bei Kleidung) - Alternative zu den festen Emojis oben
const skinItems = new Map(); // id -> { id, name, image }

async function loadSkinItems() {
    skinItems.clear();
    const [rows] = await db.query("SELECT * FROM skin_items");
    for (const row of rows) {
        skinItems.set(row.id, { id: row.id, name: row.name, image: row.image_data });
    }
    console.log("Skin-Katalog geladen:", skinItems.size);
}

// Item-IDs, die als "trägt gerade etwas" (📦-Symbol) gelten - erweitere diese Liste,
// wenn du neue Job-Punkt-Items einführst
const JOB_CARRY_ITEMS = ["package", "fish", "crop", "trash"];

// WICHTIG: alle folgenden Konstanten müssen HIER GANZ OBEN stehen, nicht irgendwo
// mitten in der Datei - der asynchrone Start-Ablauf weiter unten läuft schon sehr
// früh los und darf niemals auf eine Konstante zugreifen, die textuell erst später
// im File deklariert wird (ReferenceError: "before initialization")
const NPC_ID_OFFSET = 9000000;
const NPC_COUNT_PER_WORLD = 6;
const ZONE_CAPTURE_RADIUS = 150;
const TAXI_FARE = 50;
const HOUSE_TEMPLATE_MAP = "haus_innen";

// -------------------------------------------------------------
// TAG/NACHT-ZYKLUS + WETTER
// -------------------------------------------------------------
let gameHour = 8; // Start morgens
const DAY_LENGTH_MINUTES = 24; // 24 echte Minuten = 1 kompletter Spieltag

let currentWeather = "clear"; // "clear" | "rain" | "fog" | "snow"

function pickWeather() {
    const roll = Math.random();
    if (roll < 0.2) currentWeather = "rain";
    else if (roll < 0.3) currentWeather = "fog";
    else if (roll < 0.38) currentWeather = "snow";
    else currentWeather = "clear";
    console.log("Wetter geändert:", currentWeather);
}
pickWeather();
setInterval(pickWeather, 20 * 60 * 1000); // alle 20 Minuten neu würfeln - vorher änderte sich das Wetter nie nach dem Serverstart

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// Zugangsdaten kommen bevorzugt aus Umgebungsvariablen (empfohlen für den
// echten Betrieb) - die Werte danach sind nur ein Fallback, damit der Server
// auch ohne gesetzte Umgebungsvariablen weiterläuft wie bisher. Ganz früh
// platziert, da sowohl die Backups weiter unten als auch der DB-Pool sie brauchen.
const DB_HOST = process.env.DB_HOST || "156.67.28.205";
const DB_USER = process.env.DB_USER || "game";
const DB_PASSWORD = process.env.DB_PASSWORD || "tito13101";
const DB_NAME = process.env.DB_NAME || "gamegta";
const DB_PORT = process.env.DB_PORT || "3406";

// -------------------------------------------------------------
// AUTOMATISCHE DB-BACKUPS
// -------------------------------------------------------------
const BACKUP_DIR = path.join(__dirname, "backups");
const BACKUP_INTERVAL_HOURS = 6;
const MAX_BACKUPS = 20;

if (!fs.existsSync(BACKUP_DIR)) fs.mkdirSync(BACKUP_DIR);

function runBackup() {
    const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
    const outFile = path.join(BACKUP_DIR, `backup_${timestamp}.sql`);
    const cmd = `mysqldump -h ${DB_HOST} -P ${DB_PORT} -u ${DB_USER} ${DB_NAME} > "${outFile}"`;

    exec(cmd, { env: { ...process.env, MYSQL_PWD: DB_PASSWORD } }, (err) => {
        if (err) {
            console.error("[Backup] Fehlgeschlagen:", err.message);
            return;
        }
        console.log("[Backup] Erstellt:", outFile);
        cleanupOldBackups();
    });
}

function cleanupOldBackups() {
    const files = fs.readdirSync(BACKUP_DIR)
        .filter(f => f.startsWith("backup_") && f.endsWith(".sql"))
        .sort();

    while (files.length > MAX_BACKUPS) {
        const oldest = files.shift();
        try { fs.unlinkSync(path.join(BACKUP_DIR, oldest)); } catch {}
    }
}

runBackup(); // einmal direkt beim Serverstart
setInterval(runBackup, BACKUP_INTERVAL_HOURS * 60 * 60 * 1000);

// -------------------------------------------------------------
// TILE CONFIG / OBJECT CONFIG
// (Laden erfolgt weiter unten aus der Datenbank, nachdem die DB-Verbindung steht)
// -------------------------------------------------------------
let tileConfig = {};
let objectConfig = {};

// -------------------------------------------------------------
// MAPS LADEN
// -------------------------------------------------------------
const maps = {};

// (Laden erfolgt weiter unten aus der Datenbank, nachdem die DB-Verbindung steht)

// -------------------------------------------------------------
// MYSQL VERBINDUNG
// -------------------------------------------------------------
export const db = await mysql.createPool({
    host: DB_HOST,
    user: DB_USER,
    password: DB_PASSWORD,
    database: DB_NAME,
    port: DB_PORT,
    connectionLimit: 10,
    decimalNumbers: true // WICHTIG: DECIMAL-Spalten (money, bank, ...) als Number statt String liefern
});

// -------------------------------------------------------------
// MAPS (jetzt aus der Datenbank statt aus maps/*.json)
// -------------------------------------------------------------
async function loadMaps() {
    const [rows] = await db.query("SELECT name, data FROM maps");
    for (const key of Object.keys(maps)) delete maps[key];

    for (const row of rows) {
        try {
            maps[row.name] = JSON.parse(row.data);
        } catch {
            console.error("Ungültige Map-Daten in DB für:", row.name);
        }
    }

    console.log("Maps geladen:", Object.keys(maps));
}

async function loadGameSettings() {
    const [rows] = await db.query("SELECT * FROM game_settings");
    gameSettings = {};
    for (const row of rows) gameSettings[row.setting_key] = row.setting_value;
    console.log("Einstellungen geladen:", gameSettings);
}

async function loadTileConfig() {
    const [rows] = await db.query("SELECT * FROM tile_config");
    tileConfig = {};
    for (const row of rows) {
        tileConfig[row.id] = { name: row.name, color: row.color, collision: !!row.collision, image: row.image_data || null, pvpSafe: !!row.pvp_safe, isRoad: !!row.is_road };
    }
    console.log("Tile-Config geladen:", Object.keys(tileConfig).length);
}

async function loadObjectConfig() {
    const [rows] = await db.query("SELECT * FROM object_config");
    objectConfig = {};
    for (const row of rows) {
        objectConfig[row.type] = {
            name: row.name,
            color: row.color,
            width: row.width,
            height: row.height,
            collision: !!row.collision,
            interactive: !!row.interactive,
            action: row.action || undefined,
            glowsAtNight: !!row.glows_at_night,
            image: row.image_data || null
        };
    }
    console.log("Object-Config geladen:", Object.keys(objectConfig).length);
}

// -------------------------------------------------------------
// EXPRESS SERVER
// -------------------------------------------------------------
const app = express();
app.use(express.json({ limit: "50mb" }));
app.use(express.static(__dirname));

// -------------------------------------------------------------
// ADMIN-SCHUTZ: bewusst GANZ VORNE registriert, noch vor der ersten
// /api/admin/*-Route - eine spätere Registrierung hätte zur Folge,
// dass alle Routen, die vorher stehen, komplett ungeschützt wären
// (das war lange Zeit tatsächlich der Fall und wurde hiermit behoben)
// -------------------------------------------------------------
// -------------------------------------------------------------
// BERECHTIGUNGS- UND GRUPPEN-SYSTEM (Team-Verwaltung)
// -------------------------------------------------------------
// Fester Katalog möglicher Berechtigungen - bewusst nicht frei in der DB
// definierbar, damit keine Tippfehler unbemerkt eine Lücke aufreißen können
const PERMISSION_CATALOG = [
    { key: "manage_players", label: "Spieler-Inventare & Konten bearbeiten" },
    { key: "manage_bans", label: "Sperren/Kicken/Freischalten" },
    { key: "manage_maps", label: "Map-Editor" },
    { key: "manage_shops_items", label: "Items & Shops" },
    { key: "manage_vehicles", label: "Fahrzeug-Modelle" },
    { key: "manage_jobs", label: "Jobs & Ränge" },
    { key: "manage_tiles_objects", label: "Tiles & Objekte" },
    { key: "manage_locations", label: "Karten-Punkte (Orte & Inhalte)" },
    { key: "manage_content", label: "News & Changelog" },
    { key: "manage_logs", label: "Logs einsehen" },
    { key: "manage_radio", label: "Radiosender" },
    { key: "manage_drugs", label: "Drogensorten" },
    { key: "manage_settings", label: "Server-Einstellungen" },
    { key: "manage_events", label: "Server-Events" },
    { key: "manage_clothing", label: "Kleidung & Skins" },
    { key: "manage_discord", label: "Discord-Webhooks" },
    { key: "manage_maintenance", label: "Wartungsmodus" },
    { key: "manage_beta_access", label: "Beta-Zugang verwalten" },
    { key: "server_restart", label: "Server-Neustart" },
    { key: "manage_admin_groups", label: "Berechtigungsgruppen selbst verwalten (vorsichtig vergeben!)" },
    { key: "manage_tickets", label: "Support-Tickets bearbeiten" },
    { key: "view_metrics", label: "Server-Metriken einsehen" }
];
const PERMISSION_KEYS = PERMISSION_CATALOG.map(p => p.key);

const permissionGroups = new Map(); // id -> { id, name, color, rank, permissions: Set<string> }

async function loadPermissionGroups() {
    permissionGroups.clear();
    const [groupRows] = await db.query("SELECT * FROM permission_groups ORDER BY `rank` DESC");
    const [permRows] = await db.query("SELECT * FROM group_permissions");

    for (const g of groupRows) {
        permissionGroups.set(g.id, { id: g.id, name: g.name, color: g.color, rank: g.rank, permissions: new Set() });
    }
    for (const gp of permRows) {
        const group = permissionGroups.get(gp.group_id);
        if (group) group.permissions.add(gp.permission_key);
    }
    console.log("Berechtigungsgruppen geladen:", permissionGroups.size);
}

// Super-Admins (is_admin=1) haben immer alles - das Gruppen-System ist eine
// zusätzliche, feinere Ebene für Team-Mitglieder ohne vollen Admin-Status
function hasPermission(player, key) {
    if (!player) return false;
    if (player.isAdmin) return true;
    const groupId = player.permissionGroupId;
    if (!groupId) return false;
    const group = permissionGroups.get(groupId);
    return !!group && group.permissions.has(key);
}

// Für HTTP-Endpunkte, die nur eine playerId (nicht das volle Player-Objekt)
// zur Hand haben - liest live aus der DB, damit Rechte-Änderungen sofort wirken
async function playerHasPermission(playerId, key) {
    const [rows] = await db.query("SELECT is_admin, permission_group_id FROM players WHERE id=?", [playerId]);
    if (rows.length === 0) return false;
    return hasPermission({ isAdmin: !!rows[0].is_admin, permissionGroupId: rows[0].permission_group_id || null }, key);
}

// Benachrichtigt live per WS: den Ticket-Ersteller (falls online) und alle
// Online-Mitarbeiter mit der Berechtigung "manage_tickets" - damit niemand
// die Seite neu laden muss, um eine neue Nachricht/ein neues Ticket zu sehen
async function notifyTicketWatchers(ticketId, eventType) {
    const [rows] = await db.query("SELECT player_id FROM tickets WHERE id=?", [ticketId]);
    if (rows.length === 0) return;
    const ownerId = rows[0].player_id;

    const payload = JSON.stringify({ type: "ticket_update", ticketId: Number(ticketId), eventType });

    for (const [pid, p] of playersOnline) {
        const isOwner = pid === ownerId;
        const isStaff = hasPermission({ isAdmin: p.isAdmin, permissionGroupId: p.state.permissionGroupId }, "manage_tickets");
        if (isOwner || isStaff) {
            try { p.ws.send(payload); } catch {}
        }
    }
}

function requirePermission(key) {
    return async (req, res, next) => {
        const authHeader = req.headers.authorization || "";
        const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : null;
        if (!token) return res.status(401).json({ ok: false, error: "Nicht eingeloggt" });

        try {
            const payload = jwt.verify(token, JWT_SECRET);

            // Aktuellen Stand live aus der DB lesen statt dem Token zu vertrauen -
            // sonst würden Rechte-Änderungen erst nach erneutem Login wirken
            const [rows] = await db.query("SELECT is_admin, permission_group_id FROM players WHERE id=?", [payload.id]);
            if (rows.length === 0) return res.status(401).json({ ok: false, error: "Account nicht gefunden" });

            const fakePlayer = { isAdmin: !!rows[0].is_admin, permissionGroupId: rows[0].permission_group_id || null };
            if (!hasPermission(fakePlayer, key)) {
                return res.status(403).json({ ok: false, error: "Keine Berechtigung dafür." });
            }
            req.player = payload;
            next();
        } catch {
            res.status(401).json({ ok: false, error: "Ungültiger oder abgelaufener Token" });
        }
    };
}

// -------------------------------------------------------------
// PFAD → BERECHTIGUNG: ordnet jeden /api/admin/*-Bereich einer der
// PERMISSION_CATALOG-Berechtigungen zu. Voll-Admins (is_admin=1) kommen
// immer überall rein; alle anderen brauchen die passende Gruppen-Berechtigung.
// -------------------------------------------------------------
const ADMIN_PATH_PERMISSIONS = [
    { prefix: "/api/admin/players", keys: ["manage_players", "manage_bans"] },
    { prefix: "/api/admin/car_configs", keys: ["manage_vehicles"] },
    { prefix: "/api/admin/job_vehicles", keys: ["manage_vehicles"] },
    { prefix: "/api/admin/jobs", keys: ["manage_jobs"] },
    { prefix: "/api/admin/job_ranks", keys: ["manage_jobs"] },
    { prefix: "/api/admin/job_points", keys: ["manage_jobs"] },
    { prefix: "/api/admin/items", keys: ["manage_shops_items"] },
    { prefix: "/api/admin/shops", keys: ["manage_shops_items"] },
    { prefix: "/api/admin/shop_items", keys: ["manage_shops_items"] },
    { prefix: "/api/admin/tile_config", keys: ["manage_tiles_objects"] },
    { prefix: "/api/admin/object_config", keys: ["manage_tiles_objects"] },
    { prefix: "/api/admin/clothing_shops", keys: ["manage_locations"] },
    { prefix: "/api/admin/insurance_offices", keys: ["manage_locations"] },
    { prefix: "/api/admin/plate_offices", keys: ["manage_locations"] },
    { prefix: "/api/admin/trailer_shops", keys: ["manage_locations"] },
    { prefix: "/api/admin/highway_links", keys: ["manage_locations"] },
    { prefix: "/api/admin/black_market_spots", keys: ["manage_locations"] },
    { prefix: "/api/admin/fire_stations", keys: ["manage_locations"] },
    { prefix: "/api/admin/garages", keys: ["manage_locations"] },
    { prefix: "/api/admin/gas_stations", keys: ["manage_locations"] },
    { prefix: "/api/admin/hospitals", keys: ["manage_locations"] },
    { prefix: "/api/admin/houses", keys: ["manage_locations"] },
    { prefix: "/api/admin/impound_lots", keys: ["manage_locations"] },
    { prefix: "/api/admin/jobcenters", keys: ["manage_locations"] },
    { prefix: "/api/admin/prisons", keys: ["manage_locations"] },
    { prefix: "/api/admin/repair_shops", keys: ["manage_locations"] },
    { prefix: "/api/admin/taxi_stands", keys: ["manage_locations"] },
    { prefix: "/api/admin/territory_zones", keys: ["manage_locations"] },
    { prefix: "/api/admin/gates", keys: ["manage_locations"] },
    { prefix: "/api/admin/drug_dealer_spots", keys: ["manage_locations"] },
    { prefix: "/api/admin/drug_harvest_spots", keys: ["manage_locations"] },
    { prefix: "/api/admin/drug_process_spots", keys: ["manage_locations"] },
    { prefix: "/api/admin/news", keys: ["manage_content"] },
    { prefix: "/api/admin/changelog", keys: ["manage_content"] },
    { prefix: "/api/admin/wishes", keys: ["manage_content"] },
    { prefix: "/api/admin/polls", keys: ["manage_content"] },
    { prefix: "/api/admin/remarks", keys: ["manage_content"] },
    { prefix: "/api/admin/site_config", keys: ["manage_content", "manage_settings"] },
    { prefix: "/api/admin/logs", keys: ["manage_logs"] },
    { prefix: "/api/admin/backups", keys: ["manage_logs"] },
    { prefix: "/api/admin/radio_stations", keys: ["manage_radio"] },
    { prefix: "/api/admin/drug_types", keys: ["manage_drugs"] },
    { prefix: "/api/admin/settings", keys: ["manage_settings"] },
    { prefix: "/api/admin/events", keys: ["manage_events"] },
    { prefix: "/api/admin/clothing_items", keys: ["manage_clothing"] },
    { prefix: "/api/admin/skin_items", keys: ["manage_clothing"] },
    { prefix: "/api/admin/admin_look", keys: ["manage_settings"] },
    { prefix: "/api/admin/discord_webhooks", keys: ["manage_discord"] },
    { prefix: "/api/admin/maintenance_mode", keys: ["manage_maintenance"] },
    { prefix: "/api/admin/beta_mode", keys: ["manage_beta_access"] },
    { prefix: "/api/admin/set_beta_tester", keys: ["manage_beta_access"] },
    { prefix: "/api/admin/auto_restart", keys: ["server_restart"] },
    { prefix: "/api/admin/restart_server", keys: ["server_restart"] },
    { prefix: "/api/admin/tickets", keys: ["manage_tickets"] },
    { prefix: "/api/admin/restart_status", keys: ["server_restart"] }
];

function findAdminPathPermissionKeys(path) {
    const match = ADMIN_PATH_PERMISSIONS.find(p => path.startsWith(p.prefix));
    return match ? match.keys : null;
}

async function requireAdmin(req, res, next) {
    const authHeader = req.headers.authorization || "";
    const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : null;

    if (!token) {
        return res.status(401).json({ ok: false, error: "Nicht eingeloggt" });
    }

    try {
        const payload = jwt.verify(token, JWT_SECRET);
        if (payload.isAdmin) {
            req.player = payload;
            return next();
        }

        // Kein Voll-Admin - anhand des URL-Pfads prüfen, ob eine passende
        // Gruppen-Berechtigung existiert (Team-Mitglieder mit eingeschränktem Zugriff)
        const neededKeys = findAdminPathPermissionKeys(req.path);
        if (!neededKeys) {
            return res.status(403).json({ ok: false, error: "Kein Admin-Zugriff" });
        }

        // Live aus der DB prüfen statt dem Token zu vertrauen, damit
        // Rechte-Änderungen sofort wirken, nicht erst nach erneutem Login
        const [rows] = await db.query("SELECT is_admin, permission_group_id FROM players WHERE id=?", [payload.id]);
        if (rows.length === 0) {
            return res.status(401).json({ ok: false, error: "Account nicht gefunden" });
        }

        const fakePlayer = { isAdmin: !!rows[0].is_admin, permissionGroupId: rows[0].permission_group_id || null };
        const allowed = neededKeys.some(key => hasPermission(fakePlayer, key));
        if (!allowed) {
            return res.status(403).json({ ok: false, error: "Keine Berechtigung für diesen Bereich." });
        }

        req.player = payload;
        next();
    } catch {
        res.status(401).json({ ok: false, error: "Ungültiger oder abgelaufener Token" });
    }
}

// -------------------------------------------------------------
// BERECHTIGUNGSGRUPPEN-VERWALTUNG - bewusst VOR der generellen
// requireAdmin-Absicherung platziert: diese Routen prüfen sich mit
// requirePermission() selbst vollständig ab (eigene Token-Prüfung +
// DB-Abfrage), damit auch Team-Mitglieder ohne vollen Admin-Status
// (aber mit dem Recht "manage_admin_groups") hier reinkommen. Würden
// sie hinter der isAdmin-only-Middleware stehen, kämen Nicht-Admins
// nie bis zur eigentlichen, feineren Prüfung durch.
// -------------------------------------------------------------
// -------------------------------------------------------------
// SUPPORT-TICKETS: Admin-Seite (alle Tickets) - bewusst mit eigener
// requirePermission()-Absicherung statt der allgemeinen requireAdmin-
// Middleware, damit Team-Mitglieder mit "manage_tickets" auch ohne
// Voll-Admin-Status reinkommen (siehe Erklärung bei den Berechtigungsgruppen)
// -------------------------------------------------------------
app.get("/api/admin/tickets", requirePermission("manage_tickets"), async (req, res) => {
    const statusFilter = req.query.status;
    let sql = `
        SELECT t.*, p.username,
            (SELECT COUNT(*) FROM ticket_messages WHERE ticket_id = t.id) AS message_count
        FROM tickets t JOIN players p ON p.id = t.player_id
    `;
    const params = [];
    if (statusFilter && statusFilter !== "all") {
        sql += " WHERE t.status = ?";
        params.push(statusFilter);
    }
    sql += " ORDER BY t.updated_at DESC LIMIT 200";

    const [rows] = await db.query(sql, params);
    res.json({ ok: true, tickets: rows });
});

app.post("/api/admin/tickets/:id/status", requirePermission("manage_tickets"), async (req, res) => {
    const { status } = req.body;
    if (!["open", "in_progress", "closed"].includes(status)) {
        return res.json({ ok: false, error: "Ungültiger Status" });
    }
    await db.query("UPDATE tickets SET status=?, updated_at=NOW() WHERE id=?", [status, req.params.id]);
    notifyTicketWatchers(req.params.id, "status_change");
    res.json({ ok: true });
});

app.get("/api/admin/permission_catalog", requirePermission("manage_admin_groups"), async (req, res) => {
    res.json({ ok: true, catalog: PERMISSION_CATALOG });
});

app.get("/api/admin/permission_groups", requirePermission("manage_admin_groups"), async (req, res) => {
    const groups = [...permissionGroups.values()].map(g => ({
        id: g.id, name: g.name, color: g.color, rank: g.rank, permissions: [...g.permissions]
    }));
    res.json({ ok: true, groups });
});

app.post("/api/admin/permission_groups", requirePermission("manage_admin_groups"), async (req, res) => {
    const { id, name, color, rank, permissions } = req.body;
    if (!name) return res.json({ ok: false, error: "Name erforderlich" });

    const cleanPermissions = (Array.isArray(permissions) ? permissions : []).filter(p => PERMISSION_KEYS.includes(p));
    const cleanColor = /^#[0-9a-fA-F]{6}$/.test(color) ? color : "#6fbf73";
    const cleanRank = Math.max(0, Math.floor(Number(rank) || 0));

    let groupId = id ? Number(id) : null;
    if (groupId) {
        await db.query("UPDATE permission_groups SET name=?, color=?, `rank`=? WHERE id=?", [name, cleanColor, cleanRank, groupId]);
        await db.query("DELETE FROM group_permissions WHERE group_id=?", [groupId]);
    } else {
        const [result] = await db.query("INSERT INTO permission_groups (name, color, `rank`) VALUES (?, ?, ?)", [name, cleanColor, cleanRank]);
        groupId = result.insertId;
    }

    for (const permKey of cleanPermissions) {
        await db.query("INSERT INTO group_permissions (group_id, permission_key) VALUES (?, ?)", [groupId, permKey]);
    }

    await loadPermissionGroups();
    res.json({ ok: true, id: groupId });
});

// -------------------------------------------------------------
// LÖSCHUNG RÜCKGÄNGIG MACHEN: stellt die zuletzt protokollierte
// Löschung wieder her (egal aus welcher Tabelle)
// -------------------------------------------------------------
app.get("/api/admin/last_deletions", requirePermission("manage_admin_groups"), async (req, res) => {
    const [rows] = await db.query(
        "SELECT id, table_name, row_data, deleted_at FROM admin_deletion_log ORDER BY id DESC LIMIT 10"
    );
    res.json({ ok: true, deletions: rows });
});

app.post("/api/admin/undo_last_delete", requirePermission("manage_admin_groups"), async (req, res) => {
    const { logId } = req.body; // optional: gezielt ein bestimmtes Protokoll rückgängig machen, sonst das jüngste

    const [rows] = logId
        ? await db.query("SELECT * FROM admin_deletion_log WHERE id=?", [logId])
        : await db.query("SELECT * FROM admin_deletion_log ORDER BY id DESC LIMIT 1");

    if (rows.length === 0) {
        return res.json({ ok: false, error: "Keine Löschung zum Rückgängigmachen vorhanden." });
    }

    const entry = rows[0];
    const data = typeof entry.row_data === "string" ? JSON.parse(entry.row_data) : entry.row_data;
    const columns = Object.keys(data);
    const placeholders = columns.map(() => "?").join(", ");
    const values = columns.map(c => data[c]);

    try {
        await db.query(
            `INSERT INTO ${entry.table_name} (${columns.join(", ")}) VALUES (${placeholders})`,
            values
        );
        await db.query("DELETE FROM admin_deletion_log WHERE id=?", [entry.id]);
        res.json({ ok: true, table: entry.table_name, restored: data });
    } catch (err) {
        console.error("Fehler beim Wiederherstellen:", err);
        res.json({ ok: false, error: "Wiederherstellen fehlgeschlagen: " + (err.sqlMessage || err.message) });
    }
});

app.delete("/api/admin/permission_groups/:id", requirePermission("manage_admin_groups"), async (req, res) => {
    await db.query("UPDATE players SET permission_group_id=NULL WHERE permission_group_id=?", [req.params.id]);
    await db.query("DELETE FROM group_permissions WHERE group_id=?", [req.params.id]);
    await logDeletionForUndo("permission_groups", "id", req.params.id);
    await db.query("DELETE FROM permission_groups WHERE id=?", [req.params.id]);
    await loadPermissionGroups();
    res.json({ ok: true });
});

app.get("/api/admin/player_group_search", requirePermission("manage_admin_groups"), async (req, res) => {
    const q = String(req.query.q || "").trim();

    const [rows] = q
        ? await db.query(
            "SELECT id, username, is_admin, permission_group_id FROM players WHERE username LIKE ? ORDER BY username LIMIT 20",
            [`%${q}%`]
        )
        : await db.query(
            "SELECT id, username, is_admin, permission_group_id FROM players ORDER BY username LIMIT 500"
        );

    res.json({ ok: true, players: rows });
});

app.post("/api/admin/assign_player_group", requirePermission("manage_admin_groups"), async (req, res) => {
    const { playerId, groupId } = req.body;
    if (!playerId) return res.json({ ok: false, error: "Spieler erforderlich" });

    const finalGroupId = groupId ? Number(groupId) : null;
    await db.query("UPDATE players SET permission_group_id=? WHERE id=?", [finalGroupId, playerId]);

    const online = playersOnline.get(Number(playerId));
    if (online) online.permissionGroupId = finalGroupId;

    res.json({ ok: true });
});

app.get("/api/admin/logs", requirePermission("manage_logs"), async (req, res) => {
    const category = req.query.category;
    let query = "SELECT * FROM logs";
    const params = [];
    if (category) {
        query += " WHERE category=?";
        params.push(category);
    }
    query += " ORDER BY created_at DESC LIMIT 300";
    const [rows] = await db.query(query, params);
    res.json({ ok: true, logs: rows });
});

app.use("/api/admin", requireAdmin);

const server = http.createServer(app);
const wss = new WebSocketServer({ server });
const playersOnline = new Map();

// -------------------------------------------------------------
// AUTO-KONFIGURATIONEN (Modelle: Größe, Farbe, Fahrverhalten) AUS DB
// -------------------------------------------------------------
let carConfigs = {}; // model -> { width, height, color, maxSpeed, accel, brake, friction, turnSpeed }

// -------------------------------------------------------------
// INVENTAR-GEWICHT: jeder Gegenstand hat ein Gewicht, jeder Spieler
// eine maximale Tragfähigkeit - verhindert unbegrenztes Horten
// -------------------------------------------------------------
let itemWeights = {}; // itemId -> Gewicht (Zahl)

async function loadItemWeights() {
    const [rows] = await db.query("SELECT id, weight FROM items");
    itemWeights = {};
    for (const r of rows) itemWeights[r.id] = Number(r.weight) || 0;
    console.log("Item-Gewichte geladen:", Object.keys(itemWeights).length);
}

function getMaxCarryWeight() {
    return getSetting("max_inventory_weight") || 50;
}

function getInventoryWeight(inventory) {
    let total = 0;
    for (const entry of inventory) {
        total += (itemWeights[entry.id] || 0) * entry.amount;
    }
    return Math.round(total * 100) / 100;
}

// Prüft, ob noch Platz für die gewünschte Menge ist. Gibt zurück, wie
// viele Einheiten tatsächlich noch reinpassen (kann weniger sein als
// gewünscht, oder 0) - so können Aufrufer selbst entscheiden, ob sie
// bei Teilmengen abbrechen oder nur so viel wie möglich geben wollen
function getCarryableAmount(inventory, itemId, desiredAmount) {
    const perUnit = itemWeights[itemId] || 0;
    if (perUnit <= 0) return desiredAmount; // Gegenstände ohne Gewicht sind unbegrenzt tragbar

    const currentWeight = getInventoryWeight(inventory);
    const remainingCapacity = getMaxCarryWeight() - currentWeight;
    if (remainingCapacity <= 0) return 0;

    const maxUnitsByWeight = Math.floor(remainingCapacity / perUnit);
    return Math.max(0, Math.min(desiredAmount, maxUnitsByWeight));
}

async function loadCarConfigs() {
    const [rows] = await db.query("SELECT * FROM car_configs");
    carConfigs = {};
    for (const r of rows) {
        carConfigs[r.model] = {
            width: r.width,
            height: r.height,
            color: r.color,
            maxSpeed: r.max_speed,
            accel: r.accel,
            brake: r.brake,
            friction: r.friction,
            turnSpeed: r.turn_speed,
            tankSize: r.tank_size,
            consumption: r.consumption,
            image: r.image_data || null,
            isTrailerModel: !!r.is_trailer_model,
            trailerPrice: r.trailer_price || null
        };
    }
    console.log("Auto-Configs geladen:", Object.keys(carConfigs));
}

// -------------------------------------------------------------
// AUTOS (RAM-Cache, aus DB geladen)
// -------------------------------------------------------------
const cars = new Map(); // carId -> { id, ownerId, model, world, x, y, angle, speed, driverId, throttle, steer }

async function loadCars() {
    cars.clear();
    const [rows] = await db.query("SELECT * FROM cars WHERE is_stored = 0");
    for (const row of rows) {
        cars.set(row.id, {
            id: row.id,
            ownerId: row.owner_id,
            model: row.model,
            world: row.world,
            x: row.x,
            y: row.y,
            angle: row.angle,
            speed: 0,
            driverId: null,
            throttle: 0,
            steer: 0,
            fuel: row.fuel ?? 50,
            headlights: false,
            leftBlinker: false,
            rightBlinker: false,
            brakeLight: false,
            hazard: false,
            health: row.health ?? 100,
            trunk: JSON.parse(row.trunk || "[]"),
            jobId: row.job_id || null,
            emergency: !!row.emergency,
            odometer: row.odometer || 0,
            insured: !!row.insured,
            tuningSpeed: row.tuning_speed || 0,
            tuningAccel: row.tuning_accel || 0,
            tuningBrake: row.tuning_brake || 0,
            paintColor: row.paint_color || null,
            isTrailer: !!row.is_trailer,
            towedByCarId: null,
            loadedOnTrailerId: null,
            plate: row.plate || null
        });
    }
    console.log("Autos geladen:", cars.size);
}

// -------------------------------------------------------------
// GEBÜNDELTE FAHRZEUG-BROADCASTS: die Fahrzeug-Physik läuft alle 50ms
// (20x/Sekunde) und markiert dabei praktisch IMMER "geändert", sobald
// auch nur ein einziger NPC unterwegs ist (NPCs bewegen sich ja quasi
// dauernd) - das hat bisher 20x pro Sekunde einen kompletten
// Fahrzeug-Broadcast an ALLE Spieler ausgelöst, permanent, unabhängig
// davon ob überhaupt jemand fährt. Genau wie bei den Spieler-Bewegungen
// wird das jetzt gebündelt: die Physik-Simulation selbst bleibt bei
// vollen 20Hz für flüssige Bewegung, aber gesendet wird nur noch max.
// 10x pro Sekunde.
// -------------------------------------------------------------
let carsBroadcastPending = false;
function scheduleCarsBroadcast() {
    carsBroadcastPending = true;
}
setInterval(() => {
    if (carsBroadcastPending) {
        carsBroadcastPending = false;
        sendCarsToAll();
    }
}, 100);

function sendCarsToAll() {
    const list = [];
    for (const [, c] of cars) {
        const cfg = carConfigs[c.model] || carConfigs.sedan || {};
        list.push({
            id: c.id,
            model: c.model,
            world: c.world,
            x: c.x,
            y: c.y,
            angle: c.angle,
            driverId: c.driverId,
            speedKmh: Math.round(Math.abs(c.speed || 0) * 18),
            fuel: Math.round((c.fuel ?? 0) * 10) / 10,
            tankSize: cfg.tankSize || 50,
            headlights: !!c.headlights,
            leftBlinker: !!c.leftBlinker,
            rightBlinker: !!c.rightBlinker,
            brakeLight: !!c.brakeLight,
            hazard: !!c.hazard,
            health: Math.round(c.health ?? 100),
            passengerId: c.passengerId || null,
            jobId: c.jobId || null,
            emergency: !!c.emergency,
            odometer: Math.round((c.odometer || 0) * 10) / 10,
            insured: !!c.insured,
            ownerId: c.ownerId || null,
            tuningSpeed: c.tuningSpeed || 0,
            tuningAccel: c.tuningAccel || 0,
            tuningBrake: c.tuningBrake || 0,
            paintColor: c.paintColor || null,
            isTrailer: !!c.isTrailer,
            towedByCarId: c.towedByCarId || null,
            loadedOnTrailerId: c.loadedOnTrailerId || null,
            plate: c.plate || null
        });
    }
    const msg = JSON.stringify({ type: "cars", cars: list });
    for (const [, p] of playersOnline) p.ws.send(msg);
}

// -------------------------------------------------------------
// SHOPS (RAM-Cache, aus DB geladen)
// -------------------------------------------------------------
const shops = new Map(); // shopId -> { id, name, world, x, y, items: [{id, name, price, type, model, restore}] }

async function loadShops() {
    shops.clear();
    const [shopRows] = await db.query("SELECT * FROM shops");

    for (const s of shopRows) {
        shops.set(s.id, {
            id: s.id, name: s.name, world: s.world, x: s.x, y: s.y, items: [],
            owner_id: s.owner_id, purchase_price: s.purchase_price
        });
    }

    const [itemRows] = await db.query(`
        SELECT si.shop_id, si.price, i.id AS item_id, i.name, i.type, i.model, i.restore
        FROM shop_items si
        JOIN items i ON i.id = si.item_id
    `);

    for (const row of itemRows) {
        const shop = shops.get(row.shop_id);
        if (!shop) continue;
        shop.items.push({
            id: row.item_id,
            name: row.name,
            price: row.price,
            type: row.type,
            model: row.model,
            restore: row.restore
        });
    }

    console.log("Shops geladen:", shops.size);
}

function getShopsForWorld(world) {
    const list = [];
    for (const [, s] of shops) {
        if (s.world === world) list.push({ id: s.id, x: s.x, y: s.y });
    }
    return list;
}

function findShopNear(world, x, y) {
    for (const [, s] of shops) {
        if (s.world !== world) continue;
        if (Math.abs(s.x * 32 - x) < 32 && Math.abs(s.y * 32 - y) < 32) return s;
    }
    return null;
}

// -------------------------------------------------------------
// KLEIDUNGSLÄDEN
// -------------------------------------------------------------
const clothingShops = new Map(); // id -> { id, name, world, x, y }

// Kleidungs-Katalog (von Admins bemalte Kleidungsstücke, je Slot eine Liste)
const clothingItems = new Map(); // id -> { id, slot, name, color, image }

async function loadClothingItems() {
    clothingItems.clear();
    const [rows] = await db.query("SELECT * FROM clothing_items");
    for (const row of rows) {
        clothingItems.set(row.id, {
            id: row.id, slot: row.slot, name: row.name,
            color: row.color, price: row.price, image: row.image_data || null
        });
    }
    console.log("Kleidungs-Katalog geladen:", clothingItems.size);
}

function getClothingCatalogBySlot(slot) {
    const list = [];
    for (const [, item] of clothingItems) {
        if (item.slot === slot) list.push(item);
    }
    return list;
}

// Löst einen getragenen Kleidungs-Slot zu Farbe+Textur auf; fällt auf die
// einfache Flächenfarbe zurück, falls nichts angezogen ist (oder das Teil
// inzwischen vom Admin gelöscht wurde)
function resolveClothingSlot(itemId, fallbackColor) {
    if (itemId) {
        const item = clothingItems.get(itemId);
        if (item) return { color: item.color, image: item.image || null };
    }
    return { color: fallbackColor, image: null };
}


async function loadClothingShops() {
    clothingShops.clear();
    const [rows] = await db.query("SELECT * FROM clothing_shops");
    for (const s of rows) clothingShops.set(s.id, s);
    console.log("Kleidungsläden geladen:", clothingShops.size);
}

function getClothingShopsForWorld(world) {
    const list = [];
    for (const [, s] of clothingShops) {
        if (s.world === world) list.push({ id: s.id, x: s.x, y: s.y, name: s.name });
    }
    return list;
}

function findClothingShopNear(world, x, y) {
    for (const [, s] of clothingShops) {
        if (s.world !== world) continue;
        if (Math.abs(s.x * 32 - x) < 32 && Math.abs(s.y * 32 - y) < 32) return s;
    }
    return null;
}

// -------------------------------------------------------------
// VERSICHERUNGSBÜROS
// -------------------------------------------------------------
const insuranceOffices = new Map();

async function loadInsuranceOffices() {
    insuranceOffices.clear();
    const [rows] = await db.query("SELECT * FROM insurance_offices");
    for (const o of rows) insuranceOffices.set(o.id, o);
    console.log("Versicherungsbüros geladen:", insuranceOffices.size);
}

function getInsuranceOfficesForWorld(world) {
    const list = [];
    for (const [, o] of insuranceOffices) {
        if (o.world === world) list.push({ id: o.id, x: o.x, y: o.y, name: o.name });
    }
    return list;
}

function findInsuranceOfficeNear(world, x, y) {
    for (const [, o] of insuranceOffices) {
        if (o.world !== world) continue;
        if (Math.abs(o.x * 32 - x) < 32 && Math.abs(o.y * 32 - y) < 32) return o;
    }
    return null;
}

// -------------------------------------------------------------
// KFZ-ZULASSUNGSSTELLEN (Nummernschilder)
// -------------------------------------------------------------
const plateOffices = new Map();

async function loadPlateOffices() {
    plateOffices.clear();
    const [rows] = await db.query("SELECT * FROM plate_offices");
    for (const o of rows) plateOffices.set(o.id, o);
    console.log("Zulassungsstellen geladen:", plateOffices.size);
}

function getPlateOfficesForWorld(world) {
    const list = [];
    for (const [, o] of plateOffices) {
        if (o.world === world) list.push({ id: o.id, x: o.x, y: o.y, name: o.name });
    }
    return list;
}

function findPlateOfficeNear(world, x, y) {
    for (const [, o] of plateOffices) {
        if (o.world !== world) continue;
        if (Math.abs(o.x * 32 - x) < 32 && Math.abs(o.y * 32 - y) < 32) return o;
    }
    return null;
}

// -------------------------------------------------------------
// BÖRSE: Kurse schwanken periodisch (Random Walk mit Firmen-eigener
// Volatilität), Verlauf wird für ein Chart mitgeschrieben
// -------------------------------------------------------------
const stocks = new Map();

async function loadStocks() {
    stocks.clear();
    const [rows] = await db.query("SELECT * FROM stocks");
    for (const s of rows) stocks.set(s.id, s);
    console.log("Aktien geladen:", stocks.size);
}

setInterval(async () => {
    for (const [, s] of stocks) {
        const changePct = (Math.random() * 2 - 1) * Number(s.volatility);
        let newPrice = Number(s.price) * (1 + changePct / 100);
        newPrice = Math.max(1, Math.round(newPrice * 100) / 100);

        s.price = newPrice;
        await db.query("UPDATE stocks SET price=? WHERE id=?", [newPrice, s.id]);
        await db.query("INSERT INTO stock_price_history (stock_id, price) VALUES (?, ?)", [s.id, newPrice]);
    }

    // Alte Verlaufsdaten aufräumen (mehr als 7 Tage alt), damit die Tabelle nicht unbegrenzt wächst
    await db.query("DELETE FROM stock_price_history WHERE recorded_at < DATE_SUB(NOW(), INTERVAL 7 DAY)");
}, 5 * 60000); // alle 5 Minuten ein neuer Kurs

// -------------------------------------------------------------
// AUTOBAHN-VERBINDUNGEN: verbindet zwei Punkte auf zwei (auch
// unterschiedlichen) Karten miteinander - wird automatisch beim
// Herannahen ausgelöst (zu Fuß oder mit Auto, keine Taste nötig)
// -------------------------------------------------------------
const highwayLinks = new Map();

async function loadHighwayLinks() {
    highwayLinks.clear();
    const [rows] = await db.query("SELECT * FROM highway_links");
    for (const l of rows) highwayLinks.set(l.id, l);
    console.log("Autobahn-Verbindungen geladen:", highwayLinks.size);
}

function getHighwayLinksForWorld(world) {
    const list = [];
    for (const [, l] of highwayLinks) {
        if (l.world_a === world) list.push({ id: l.id, x: l.x_a, y: l.y_a, name: l.name });
        if (l.world_b === world) list.push({ id: l.id, x: l.x_b, y: l.y_b, name: l.name });
    }
    return list;
}

// Findet eine Verbindung in der Nähe UND liefert direkt das passende Ziel
// (die jeweils ANDERE Seite der Verbindung) mit zurück
function findHighwayLinkNear(world, x, y) {
    for (const [, l] of highwayLinks) {
        if (l.world_a === world && Math.abs(l.x_a * 32 - x) < 40 && Math.abs(l.y_a * 32 - y) < 40) {
            return { targetWorld: l.world_b, targetX: l.x_b * 32, targetY: l.y_b * 32 };
        }
        if (l.world_b === world && Math.abs(l.x_b * 32 - x) < 40 && Math.abs(l.y_b * 32 - y) < 40) {
            return { targetWorld: l.world_a, targetX: l.x_a * 32, targetY: l.y_a * 32 };
        }
    }
    return null;
}

// -------------------------------------------------------------
// SCHWARZMARKT (HEHLER): Diebesgut wird hier gegen Aufpreis verkauft,
// mit Entdeckungsrisiko bei jedem Verkauf
// -------------------------------------------------------------
const blackMarketSpots = new Map();

async function loadBlackMarketSpots() {
    blackMarketSpots.clear();
    const [rows] = await db.query("SELECT * FROM black_market_spots");
    for (const s of rows) blackMarketSpots.set(s.id, s);
    console.log("Schwarzmarkt-Standorte geladen:", blackMarketSpots.size);
}

function getBlackMarketSpotsForWorld(world) {
    const list = [];
    for (const [, s] of blackMarketSpots) {
        if (s.world === world) list.push({ id: s.id, x: s.x, y: s.y, name: s.name });
    }
    return list;
}

function findBlackMarketSpotNear(world, x, y) {
    for (const [, s] of blackMarketSpots) {
        if (s.world !== world) continue;
        if (Math.abs(s.x * 32 - x) < 32 && Math.abs(s.y * 32 - y) < 32) return s;
    }
    return null;
}

// -------------------------------------------------------------
// ANHÄNGER-SHOPS
// -------------------------------------------------------------
const trailerShops = new Map();

async function loadTrailerShops() {
    trailerShops.clear();
    const [rows] = await db.query("SELECT * FROM trailer_shops");
    for (const o of rows) trailerShops.set(o.id, o);
    console.log("Anhänger-Shops geladen:", trailerShops.size);
}

function getTrailerShopsForWorld(world) {
    const list = [];
    for (const [, o] of trailerShops) {
        if (o.world === world) list.push({ id: o.id, x: o.x, y: o.y, name: o.name });
    }
    return list;
}

function findTrailerShopNear(world, x, y) {
    for (const [, o] of trailerShops) {
        if (o.world !== world) continue;
        if (Math.abs(o.x * 32 - x) < 32 && Math.abs(o.y * 32 - y) < 32) return o;
    }
    return null;
}

async function spawnTrailerForPlayer(p, playerId, model) {
    const [result] = await db.query(
        "INSERT INTO cars (owner_id, model, world, x, y, angle, fuel, health, is_trailer) VALUES (?, ?, ?, ?, ?, 0, 0, 100, 1)",
        [playerId, model, p.state.world, p.state.x + 40, p.state.y]
    );
    cars.set(result.insertId, {
        id: result.insertId, ownerId: playerId, model, world: p.state.world,
        x: p.state.x + 40, y: p.state.y, angle: 0, speed: 0,
        driverId: null, throttle: 0, steer: 0, fuel: 0,
        headlights: false, leftBlinker: false, rightBlinker: false,
        brakeLight: false, hazard: false, health: 100, trunk: [],
        passengerId: null, isTrailer: true, towedByCarId: null
    });
    sendCarsToAll();
}

// -------------------------------------------------------------
// GARAGEN (RAM-Cache, aus DB geladen)
// -------------------------------------------------------------
const garages = new Map(); // garageId -> { id, name, world, x, y }

async function loadGarages() {
    garages.clear();
    const [rows] = await db.query("SELECT * FROM garages");
    for (const g of rows) {
        garages.set(g.id, g);
    }
    console.log("Garagen geladen:", garages.size);
}

function getGaragesForWorld(world) {
    const list = [];
    for (const [, g] of garages) {
        if (g.world === world) list.push({ id: g.id, x: g.x, y: g.y });
    }
    return list;
}

function findGarageNear(world, x, y) {
    for (const [, g] of garages) {
        if (g.world !== world) continue;
        if (Math.abs(g.x * 32 - x) < 32 && Math.abs(g.y * 32 - y) < 32) return g;
    }
    return null;
}

// -------------------------------------------------------------
// JOBSYSTEM (RAM-Cache, aus DB geladen)
// -------------------------------------------------------------
const jobs = new Map();       // jobId -> { id, name }
const jobRanks = new Map();   // rankId -> { id, jobId, level, title, salary }
const jobcenters = new Map(); // jobcenterId -> { id, name, world, x, y }

async function loadJobs() {
    jobs.clear();
    jobRanks.clear();

    const [jobRows] = await db.query("SELECT * FROM jobs");
    for (const j of jobRows) {
        jobs.set(j.id, {
            id: j.id, name: j.name, type: j.type || "generic", protected: !!j.protected,
            uniformShirtId: j.uniform_shirt_id || null,
            uniformPantsId: j.uniform_pants_id || null,
            uniformShoesId: j.uniform_shoes_id || null,
            uniformHelmetId: j.uniform_helmet_id || null
        });
    }

    const [rankRows] = await db.query("SELECT * FROM job_ranks ORDER BY job_id, level ASC");
    for (const r of rankRows) {
        jobRanks.set(r.id, { id: r.id, jobId: r.job_id, level: r.level, title: r.title, salary: r.salary });
    }

    console.log("Jobs geladen:", jobs.size, "- Ränge:", jobRanks.size);
}

async function loadJobcenters() {
    jobcenters.clear();
    const [rows] = await db.query("SELECT * FROM jobcenters");
    for (const j of rows) {
        jobcenters.set(j.id, j);
    }
    console.log("Jobcenter geladen:", jobcenters.size);
}

function getJobsForWorld(world) {
    const list = [];
    for (const [, j] of jobcenters) {
        if (j.world === world) list.push({ id: j.id, x: j.x, y: j.y });
    }
    return list;
}

function findJobcenterNear(world, x, y) {
    for (const [, j] of jobcenters) {
        if (j.world !== world) continue;
        if (Math.abs(j.x * 32 - x) < 32 && Math.abs(j.y * 32 - y) < 32) return j;
    }
    return null;
}

// Alle Jobs inkl. ihrer Ränge, sortiert - fürs Jobcenter-Fenster im Client
function getJobsWithRanks() {
    const list = [];
    for (const [, j] of jobs) {
        const ranks = [...jobRanks.values()]
            .filter(r => r.jobId === j.id)
            .sort((a, b) => a.level - b.level);
        list.push({ id: j.id, name: j.name, ranks });
    }
    return list;
}

// -------------------------------------------------------------
// TANKSTELLEN (RAM-Cache, aus DB geladen)
// -------------------------------------------------------------
const gasStations = new Map(); // id -> { id, name, world, x, y, price }

async function loadGasStations() {
    gasStations.clear();
    const [rows] = await db.query("SELECT * FROM gas_stations");
    for (const g of rows) {
        gasStations.set(g.id, g);
    }
    console.log("Tankstellen geladen:", gasStations.size);
}

function getGasStationsForWorld(world) {
    const list = [];
    for (const [, g] of gasStations) {
        if (g.world === world) list.push({ id: g.id, x: g.x, y: g.y, price: g.price });
    }
    return list;
}

function findGasStationNear(world, x, y) {
    for (const [, g] of gasStations) {
        if (g.world !== world) continue;
        if (Math.abs(g.x * 32 - x) < 32 && Math.abs(g.y * 32 - y) < 32) return g;
    }
    return null;
}

// -------------------------------------------------------------
// REPARATUR-WERKSTÄTTEN (RAM-Cache, aus DB geladen)
// -------------------------------------------------------------
const repairShops = new Map(); // id -> { id, name, world, x, y, price_per_point }

async function loadRepairShops() {
    repairShops.clear();
    const [rows] = await db.query("SELECT * FROM repair_shops");
    for (const r of rows) {
        repairShops.set(r.id, r);
    }
    console.log("Werkstätten geladen:", repairShops.size);
}

function getRepairShopsForWorld(world) {
    const list = [];
    for (const [, r] of repairShops) {
        if (r.world === world) list.push({ id: r.id, x: r.x, y: r.y, pricePerPoint: r.price_per_point });
    }
    return list;
}

function findRepairShopNear(world, x, y) {
    for (const [, r] of repairShops) {
        if (r.world !== world) continue;
        if (Math.abs(r.x * 32 - x) < 32 && Math.abs(r.y * 32 - y) < 32) return r;
    }
    return null;
}

// -------------------------------------------------------------
// TAXI-SYSTEM
// -------------------------------------------------------------
const taxiStands = new Map(); // id -> { id, name, world, x, y }
const taxiWaiting = new Map(); // playerId -> { requestedAt }

async function loadTaxiStands() {
    taxiStands.clear();
    const [rows] = await db.query("SELECT * FROM taxi_stands");
    for (const t of rows) taxiStands.set(t.id, t);
    console.log("Taxi-Stände geladen:", taxiStands.size);
}

function getTaxiStandsForWorld(world) {
    const list = [];
    for (const [, t] of taxiStands) {
        if (t.world === world) list.push({ id: t.id, x: t.x, y: t.y, name: t.name });
    }
    return list;
}

function findTaxiStandNear(world, x, y) {
    for (const [, t] of taxiStands) {
        if (t.world !== world) continue;
        if (Math.abs(t.x * 32 - x) < 32 && Math.abs(t.y * 32 - y) < 32) return t;
    }
    return null;
}

// -------------------------------------------------------------
// KRANKENHÄUSER (Respawn-Punkte bei Tod)
// -------------------------------------------------------------
const hospitals = new Map();

async function loadHospitals() {
    hospitals.clear();
    const [rows] = await db.query("SELECT * FROM hospitals");
    for (const h of rows) hospitals.set(h.id, h);
    console.log("Krankenhäuser geladen:", hospitals.size);
}

function getHospitalsForWorld(world) {
    const list = [];
    for (const [, h] of hospitals) {
        if (h.world === world) list.push({ id: h.id, x: h.x, y: h.y, name: h.name });
    }
    return list;
}

function getAnyHospital() {
    // Bevorzugt eins in "stadt", sonst irgendeins, sonst Fallback-Koordinaten
    for (const [, h] of hospitals) {
        if (h.world === "stadt") return h;
    }
    const first = [...hospitals.values()][0];
    return first || { world: "stadt", x: 100, y: 100 };
}

// -------------------------------------------------------------
// GEFÄNGNISSE (Ziel-Zellen bei Festnahme)
// -------------------------------------------------------------
const prisons = new Map();

async function loadPrisons() {
    prisons.clear();
    const [rows] = await db.query("SELECT * FROM prisons");
    for (const p of rows) prisons.set(p.id, p);
    console.log("Gefängnisse geladen:", prisons.size);
}

function getPrisonsForWorld(world) {
    const list = [];
    for (const [, p] of prisons) {
        if (p.world === world) list.push({ id: p.id, x: p.x, y: p.y, name: p.name });
    }
    return list;
}

function getAnyPrison() {
    for (const [, p] of prisons) {
        if (p.world === "stadt") return p;
    }
    const first = [...prisons.values()][0];
    return first || { world: "stadt", x: 150, y: 100 };
}

function findPrisonNear(world, x, y) {
    for (const [, pr] of prisons) {
        if (pr.world !== world) continue;
        if (Math.abs(pr.x * 32 - x) < 60 && Math.abs(pr.y * 32 - y) < 60) return pr;
    }
    return null;
}

// -------------------------------------------------------------
// TOR-SCHLÜSSELSYSTEM
// -------------------------------------------------------------
const gates = new Map(); // id -> { id, name, world, x, y, owner_id }

async function loadGates() {
    gates.clear();
    const [rows] = await db.query("SELECT * FROM gates");
    for (const g of rows) gates.set(g.id, g);
    console.log("Tore mit Besitzer geladen:", gates.size);
}

function findGateByPosition(world, x, y) {
    for (const [, g] of gates) {
        if (g.world === world && g.x === x && g.y === y) return g;
    }
    return null;
}

// Prüft, ob der Spieler auf der für dieses Tor gesperrten Seite steht
// (obj.oneWaySide: "top" | "bottom" | "left" | "right" | undefined)
function isOnBlockedSide(obj, playerX, playerY) {
    if (!obj.oneWaySide) return false;
    const cfg = objectConfig[obj.type];
    if (!cfg) return false;

    const centerX = obj.x + cfg.width / 2;
    const centerY = obj.y - (cfg.height - 32) + cfg.height / 2;

    switch (obj.oneWaySide) {
        case "top": return playerY < centerY;
        case "bottom": return playerY > centerY;
        case "left": return playerX < centerX;
        case "right": return playerX > centerX;
        default: return false;
    }
}

// Findet das nächste "Tor"-Objekt (interaktiv, action=toggle_gate) in Interaktions-Reichweite
function findGateObjectNear(world, px, py) {
    const map = maps[world];
    if (!map || !map.objects) return null;

    return map.objects.find(o => {
        const cfg = objectConfig[o.type];
        if (!cfg || cfg.action !== "toggle_gate") return false;
        const ox = o.x;
        const oy = o.y - (cfg.height - 32);
        const margin = 48; // +16px extra Reichweite (Wände in der Nähe, Auto-Größe)
        return px > ox - margin && px < ox + cfg.width + margin &&
               py > oy - margin && py < oy + cfg.height + margin;
    }) || null;
}

// Öffnet/schließt das nächste Tor-Objekt in Reichweite - genutzt sowohl von der
// generischen E-Interaktion als auch vom dedizierten Q-Kurzbefehl.
async function toggleNearestGate(player, playerId, silent) {
    const map = maps[player.state.world];
    if (!map) return false;

    const obj = findGateObjectNear(player.state.world, player.state.x, player.state.y);
    if (!obj) {
        if (!silent) player.ws.send(JSON.stringify({ type: "shop_error", msg: "Kein Tor in der Nähe." }));
        return false;
    }

    if (isOnBlockedSide(obj, player.state.x, player.state.y)) {
        if (!silent) player.ws.send(JSON.stringify({ type: "shop_error", msg: "🚫 Von dieser Seite kannst du das Tor nicht bedienen." }));
        return false;
    }

    const gate = findGateByPosition(player.state.world, obj.x, obj.y);

    if (gate && (gate.owner_id || gate.required_job_id)) {
        let hasAccess = gate.owner_id === playerId;

        if (!hasAccess && gate.required_job_id) {
            const rank = jobRanks.get(player.state.jobRankId);
            if (rank && rank.jobId === gate.required_job_id) hasAccess = true;
        }

        if (!hasAccess) {
            const [rows] = await db.query(
                "SELECT 1 FROM gate_keys WHERE gate_id=? AND player_id=?",
                [gate.id, playerId]
            );
            hasAccess = rows.length > 0;
        }

        if (!hasAccess) {
            const jobDef = gate.required_job_id ? jobs.get(gate.required_job_id) : null;
            player.ws.send(JSON.stringify({
                type: "shop_error",
                msg: jobDef && !gate.owner_id
                    ? `🔒 Nur Mitarbeiter von "${jobDef.name}" können dieses Tor bedienen.`
                    : "🔒 Für dieses Tor brauchst du einen Schlüssel."
            }));
            return false;
        }
    }

    obj.open = !obj.open;
    console.log(`[Tor] ${player.username} schaltet Objekt bei (${obj.x},${obj.y}) Größe ${objectConfig[obj.type].width}x${objectConfig[obj.type].height} auf ${obj.open ? "OFFEN" : "ZU"}`);
    broadcastGateToggle(player.state.world, map.objects.indexOf(obj), obj.open);
    player.ws.send(JSON.stringify({
        type: "shop_info",
        msg: obj.open ? "Tor/Tür geöffnet." : "Tor/Tür geschlossen."
    }));
    return true;
}

// -------------------------------------------------------------
// ABSCHLEPPSYSTEM
// -------------------------------------------------------------
const impoundLots = new Map();

async function loadImpoundLots() {
    impoundLots.clear();
    const [rows] = await db.query("SELECT * FROM impound_lots");
    for (const l of rows) impoundLots.set(l.id, l);
    console.log("Abschlepphöfe geladen:", impoundLots.size);
}

function getImpoundLotsForWorld(world) {
    const list = [];
    for (const [, l] of impoundLots) {
        if (l.world === world) list.push({ id: l.id, x: l.x, y: l.y, name: l.name });
    }
    return list;
}

function findImpoundLotNear(world, x, y) {
    for (const [, l] of impoundLots) {
        if (l.world !== world) continue;
        if (Math.abs(l.x * 32 - x) < 60 && Math.abs(l.y * 32 - y) < 60) return l;
    }
    return null;
}

function isTowJobRank(jobRankId) {
    const rank = jobRanks.get(jobRankId);
    if (!rank) return false;
    const job = jobs.get(rank.jobId);
    return !!job && job.type === "tow";
}

function isFireJobRank(jobRankId) {
    const rank = jobRanks.get(jobRankId);
    if (!rank) return false;
    const job = jobs.get(rank.jobId);
    return !!job && job.type === "fire";
}

// -------------------------------------------------------------
// DROGEN-SYSTEM
// -------------------------------------------------------------
const drugTypes = new Map();       // id -> { id, name, raw_item_id, product_item_id, base_price }
const drugHarvestSpots = new Map();
const drugProcessSpots = new Map();
const drugDealerSpots = new Map(); // Pool möglicher Verkaufsorte je Sorte
const activeDealerSpot = new Map(); // drug_id -> { spotId, world, x, y, price }

async function loadDrugTypes() {
    drugTypes.clear();
    const [rows] = await db.query("SELECT * FROM drug_types");
    for (const d of rows) drugTypes.set(d.id, d);
    console.log("Drogensorten geladen:", drugTypes.size);
}

async function loadDrugHarvestSpots() {
    drugHarvestSpots.clear();
    const [rows] = await db.query("SELECT * FROM drug_harvest_spots");
    for (const s of rows) drugHarvestSpots.set(s.id, s);
    console.log("Anbaustellen geladen:", drugHarvestSpots.size);
}

async function loadDrugProcessSpots() {
    drugProcessSpots.clear();
    const [rows] = await db.query("SELECT * FROM drug_process_spots");
    for (const s of rows) drugProcessSpots.set(s.id, s);
    console.log("Labore geladen:", drugProcessSpots.size);
}

async function loadDrugDealerSpots() {
    drugDealerSpots.clear();
    const [rows] = await db.query("SELECT * FROM drug_dealer_spots");
    for (const s of rows) drugDealerSpots.set(s.id, s);
    console.log("Verkaufsorte (Pool) geladen:", drugDealerSpots.size);

    // Für jede Drogensorte einen zufälligen aktiven Verkaufsort auswählen
    for (const [drugId, drug] of drugTypes) {
        rotateDealerSpot(drugId, drug);
    }
}

function rotateDealerSpot(drugId, drugDef) {
    const candidates = [...drugDealerSpots.values()].filter(s => s.drug_id === drugId);
    if (candidates.length === 0) {
        activeDealerSpot.delete(drugId);
        return;
    }

    const spot = candidates[Math.floor(Math.random() * candidates.length)];
    const basePrice = drugDef.base_price;
    const price = Math.round(basePrice * (0.7 + Math.random() * 0.8)); // 70%-150% vom Basispreis

    const oldSpot = activeDealerSpot.get(drugId);
    activeDealerSpot.set(drugId, { spotId: spot.id, world: spot.world, x: spot.x, y: spot.y, name: spot.name, price });

    // Betroffene Welten (alt + neu) über die Änderung informieren
    if (oldSpot) broadcastDealerUpdate(oldSpot.world);
    broadcastDealerUpdate(spot.world);
}

function getActiveDealerSpotsForWorld(world) {
    const list = [];
    for (const [drugId, spot] of activeDealerSpot) {
        if (spot.world !== world) continue;
        const drug = drugTypes.get(drugId);
        list.push({ drugId, drugName: drug?.name || drugId, x: spot.x, y: spot.y, price: spot.price });
    }
    return list;
}

function broadcastDealerUpdate(world) {
    const msg = JSON.stringify({ type: "dealer_update", spots: getActiveDealerSpotsForWorld(world) });
    for (const [, p] of playersOnline) {
        if (p.state.world === world) p.ws.send(msg);
    }
}

function getHarvestSpotsForWorld(world) {
    const list = [];
    for (const [, s] of drugHarvestSpots) {
        if (s.world === world) list.push({ id: s.id, x: s.x, y: s.y, name: s.name, drugId: s.drug_id });
    }
    return list;
}

function getProcessSpotsForWorld(world) {
    const list = [];
    for (const [, s] of drugProcessSpots) {
        if (s.world === world) list.push({ id: s.id, x: s.x, y: s.y, name: s.name, drugId: s.drug_id });
    }
    return list;
}

function findHarvestSpotNear(world, x, y) {
    for (const [, s] of drugHarvestSpots) {
        if (s.world !== world) continue;
        if (Math.abs(s.x * 32 - x) < 40 && Math.abs(s.y * 32 - y) < 40) return s;
    }
    return null;
}

function findProcessSpotNear(world, x, y) {
    for (const [, s] of drugProcessSpots) {
        if (s.world !== world) continue;
        if (Math.abs(s.x * 32 - x) < 40 && Math.abs(s.y * 32 - y) < 40) return s;
    }
    return null;
}

function findActiveDealerNear(world, x, y) {
    for (const [drugId, spot] of activeDealerSpot) {
        if (spot.world !== world) continue;
        if (Math.abs(spot.x * 32 - x) < 50 && Math.abs(spot.y * 32 - y) < 50) return { drugId, spot };
    }
    return null;
}

// -------------------------------------------------------------
// FEUERWEHR-SYSTEM
// -------------------------------------------------------------
const fireStations = new Map();

async function loadFireStations() {
    fireStations.clear();
    const [rows] = await db.query("SELECT * FROM fire_stations");
    for (const s of rows) fireStations.set(s.id, s);
    console.log("Feuerwachen geladen:", fireStations.size);
}

function getFireStationsForWorld(world) {
    const list = [];
    for (const [, s] of fireStations) {
        if (s.world === world) list.push({ id: s.id, x: s.x, y: s.y, name: s.name });
    }
    return list;
}

// Aktive Feuer (rein im RAM, nicht in der DB - wie NPC-Verkehr)
const activeFires = new Map(); // id -> { id, world, x, y, intensity, startedAt }
let fireIdCounter = 1;
const FIRE_BURNOUT_MS = 3 * 60 * 1000; // brennt nach 3 Minuten von selbst aus
const FIRE_DAMAGE_RADIUS = 70;

function spawnFire(world, x, y) {
    const id = fireIdCounter++;
    const fire = { id, world, x, y, intensity: 100, startedAt: Date.now() };
    activeFires.set(id, fire);

    broadcastFireUpdate(world);
    broadcastFireAlert(world, x, y);

    setTimeout(() => {
        if (activeFires.has(id)) {
            activeFires.delete(id);
            broadcastFireUpdate(world);
        }
    }, FIRE_BURNOUT_MS);

    return fire;
}

function getFiresForWorld(world) {
    const list = [];
    for (const [, f] of activeFires) {
        if (f.world === world) list.push({ id: f.id, x: f.x, y: f.y, intensity: f.intensity });
    }
    return list;
}

function broadcastFireUpdate(world) {
    const msg = JSON.stringify({ type: "fire_update", fires: getFiresForWorld(world) });
    for (const [, p] of playersOnline) {
        if (p.state.world === world) p.ws.send(msg);
    }
}

function broadcastFireAlert(world, x, y) {
    const msg = JSON.stringify({ type: "fire_alert", world, x: Math.round(x), y: Math.round(y) });
    for (const [, p] of playersOnline) {
        if (isFireJobRank(p.state.jobRankId)) p.ws.send(msg);
    }
}

// -------------------------------------------------------------
// TERRITORIUM (Banden-Zonen)
// -------------------------------------------------------------
const territoryZones = new Map();

async function loadTerritoryZones() {
    territoryZones.clear();
    const [rows] = await db.query(`
        SELECT tz.*, g.name AS gang_name, g.tag AS gang_tag, g.color AS gang_color
        FROM territory_zones tz
        LEFT JOIN gangs g ON g.id = tz.owner_gang_id
    `);
    for (const z of rows) territoryZones.set(z.id, z);
    console.log("Territoriums-Zonen geladen:", territoryZones.size);
}

function getZonesForWorld(world) {
    const list = [];
    for (const [, z] of territoryZones) {
        if (z.world === world) {
            list.push({
                id: z.id, x: z.x, y: z.y, name: z.name,
                ownerGangId: z.owner_gang_id,
                ownerTag: z.gang_tag,
                ownerColor: z.gang_color
            });
        }
    }
    return list;
}

function findZoneNear(world, x, y) {
    for (const [, z] of territoryZones) {
        if (z.world !== world) continue;
        if (Math.hypot(z.x * 32 - x, z.y * 32 - y) <= ZONE_CAPTURE_RADIUS) return z;
    }
    return null;
}

function broadcastZoneUpdate(world) {
    const msg = JSON.stringify({ type: "zone_update", zones: getZonesForWorld(world) });
    for (const [, p] of playersOnline) {
        if (p.state.world === world) p.ws.send(msg);
    }
}

function broadcastGateToggle(world, objectIndex, open) {
    const msg = JSON.stringify({ type: "gate_toggle", objectIndex, open });
    for (const [, p] of playersOnline) {
        if (p.state.world === world) p.ws.send(msg);
    }
}

function isTaxiJobRank(jobRankId) {
    const rank = jobRanks.get(jobRankId);
    if (!rank) return false;
    const job = jobs.get(rank.jobId);
    return !!job && job.type === "taxi";
}

function isPoliceJobRank(jobRankId) {
    const rank = jobRanks.get(jobRankId);
    if (!rank) return false;
    const job = jobs.get(rank.jobId);
    return !!job && job.type === "police";
}

function isMedicJobRank(jobRankId) {
    const rank = jobRanks.get(jobRankId);
    if (!rank) return false;
    const job = jobs.get(rank.jobId);
    return !!job && job.type === "medic";
}

function isMechanicJobRank(jobRankId) {
    const rank = jobRanks.get(jobRankId);
    if (!rank) return false;
    const job = jobs.get(rank.jobId);
    return !!job && job.type === "mechanic";
}

function hasTabletAccess(jobRankId) {
    const rank = jobRanks.get(jobRankId);
    if (!rank) return false;
    const job = jobs.get(rank.jobId);
    return !!job && ["police", "medic", "fire"].includes(job.type);
}

// Diagnose-Variante: liefert zusätzlich eine Begründung für die Chat-Fehlermeldung,
// damit man sofort sieht, WAS der Server als aktuellen Job erkennt
function tabletAccessDenyReason(jobRankId) {
    const rank = jobRanks.get(jobRankId);
    if (!rank) return "Du hast aktuell keinen Job (kein Rang zugewiesen).";
    const job = jobs.get(rank.jobId);
    if (!job) return "Dein Job-Rang verweist auf keinen gültigen Job mehr.";
    return `Dein aktueller Job "${job.name}" hat den Typ "${job.type}" - das Tablet ist nur für police/medic/fire freigegeben. Prüfe in der Job-Verwaltung, ob dieser Job wirklich den Typ "police" hat.`;
}

function broadcastMedicAlert(alert) {
    const msg = JSON.stringify({ type: "medic_alert", ...alert });
    for (const [, p] of playersOnline) {
        if (isMedicJobRank(p.state.jobRankId)) p.ws.send(msg);
    }
}

// -------------------------------------------------------------
// FMS-STATUS + LEITSTELLE: standardisierte Funkmeldesystem-Status
// (wie im echten BOS-Funk) für Polizei/Rettungsdienst/Feuerwehr, plus
// eine Leitstellen-Übersicht, die alle Einsatzkräfte live sieht und
// Einsätze verteilen kann
// -------------------------------------------------------------
const FMS_STATUS_LABELS = {
    0: "Notruf",
    1: "Einsatzbereit Funk",
    2: "Einsatzbereit Wache",
    3: "Anfahrt zur Einsatzstelle",
    4: "Ankunft am Einsatzort",
    5: "Sprechwunsch",
    6: "Nicht einsatzbereit",
    7: "Einsatz erledigt, Rückfahrt",
    8: "Sprechwunsch Leitstelle"
};

function getJobTypeOf(jobRankId) {
    const rank = jobRanks.get(jobRankId);
    if (!rank) return null;
    const job = jobs.get(rank.jobId);
    return job ? job.type : null;
}

// Wer gerade das Leitstellen-Fenster offen hat -> bekommt Status-Änderungen live
const leitstelleViewers = new Set();

function getLeitstelleUnitsSnapshot() {
    const units = [];
    for (const [id, p] of playersOnline) {
        const jobType = getJobTypeOf(p.state.jobRankId);
        if (!p.state.onDuty || !["police", "medic", "fire"].includes(jobType)) continue;
        units.push({
            playerId: id,
            username: p.username,
            jobType,
            fmsStatus: p.state.fmsStatus ?? 2,
            world: p.state.world,
            x: Math.round(p.state.x),
            y: Math.round(p.state.y)
        });
    }
    return units;
}

function broadcastLeitstelleUpdate() {
    if (leitstelleViewers.size === 0) return;
    const msg = JSON.stringify({ type: "leitstelle_units", units: getLeitstelleUnitsSnapshot() });
    for (const viewerId of leitstelleViewers) {
        const viewer = playersOnline.get(viewerId);
        if (viewer) viewer.ws.send(msg);
    }
}

// -------------------------------------------------------------
// BANDEN/FRAKTIONEN
// -------------------------------------------------------------
async function getPlayerGang(playerId) {
    const [rows] = await db.query(
        `SELECT g.id, g.name, g.tag, g.color, g.bank, g.leader_id, gm.role
         FROM gang_members gm JOIN gangs g ON g.id = gm.gang_id
         WHERE gm.player_id = ?`,
        [playerId]
    );
    return rows[0] || null;
}

async function refreshPlayerGangInfo(playerId) {
    const p = playersOnline.get(playerId);
    if (!p) return;

    const gang = await getPlayerGang(playerId);
    p.state.gangTag = gang ? gang.tag : null;
    p.state.gangColor = gang ? gang.color : null;
}

async function refreshGangMembersGangInfo(gangId) {
    const [members] = await db.query("SELECT player_id FROM gang_members WHERE gang_id=?", [gangId]);
    for (const m of members) {
        await refreshPlayerGangInfo(m.player_id);
    }
}

function findAtmNear(world, x, y) {
    const map = maps[world];
    if (!map || !map.atms) return null;
    return map.atms.find(a => Math.abs(a.x * 32 - x) < 32 && Math.abs(a.y * 32 - y) < 32) || null;
}

function broadcastCrimeAlert(alert) {
    const msg = JSON.stringify({ type: "crime_alert", ...alert });
    for (const [, p] of playersOnline) {
        if (isPoliceJobRank(p.state.jobRankId)) p.ws.send(msg);
    }
}

// -------------------------------------------------------------
// AUTOMATISCHES SCHLIESSEN VON STANDORT-FENSTERN: merkt sich, welches
// Shop-artige Fenster ein Spieler gerade offen hat und wo - wird bei
// jeder Bewegung geprüft. Zu weit weggelaufen oder gefesselt worden ->
// Fenster wird clientseitig automatisch geschlossen.
// -------------------------------------------------------------
const WINDOW_CLOSE_DISTANCE = 80; // px - etwas großzügiger als die Interaktions-Reichweite

function trackOpenLocationWindow(p, windowType) {
    p.openWindowType = windowType;
    p.openWindowX = p.state.x;
    p.openWindowY = p.state.y;
    p.openWindowWorld = p.state.world;
}

function clearOpenWindowTracking(p) {
    p.openWindowType = null;
}

function closeOpenWindowIfAny(p, reason) {
    if (!p.openWindowType) return;
    p.ws.send(JSON.stringify({ type: "close_window", windowType: p.openWindowType, reason }));
    p.openWindowType = null;
}

function checkOpenWindowDistance(p) {
    if (!p.openWindowType) return;
    if (p.state.world !== p.openWindowWorld) {
        closeOpenWindowIfAny(p, "moved");
        return;
    }
    const dist = Math.hypot(p.state.x - p.openWindowX, p.state.y - p.openWindowY);
    if (dist > WINDOW_CLOSE_DISTANCE) {
        closeOpenWindowIfAny(p, "moved");
    }
}

// -------------------------------------------------------------
// AUTOBAHN-TELEPORT: bringt einen Spieler (zu Fuß oder im Auto) auf
// die andere Seite einer Autobahn-Verbindung, inkl. Mitnahme des
// Fahrzeugs (falls er gerade fährt) über die Kartengrenze hinweg
// -------------------------------------------------------------
function teleportPlayerViaHighway(p, playerId, link) {
    p.state.world = link.targetWorld;
    p.state.x = link.targetX;
    p.state.y = link.targetY;
    p.positionDirty = true;

    if (p.drivingCarId) {
        const car = cars.get(p.drivingCarId);
        if (car) {
            car.world = link.targetWorld;
            car.x = link.targetX;
            car.y = link.targetY;
        }
    }

    db.query("UPDATE players SET world=?, x=?, y=? WHERE id=?", [p.state.world, p.state.x, p.state.y, playerId]).catch(() => {});

    const targetMap = maps[link.targetWorld];
    p.ws.send(JSON.stringify({
        type: "map_data",
        tiles: targetMap.tiles,
        tileRot: targetMap.tileRot || null,
        doors: targetMap.doors || [],
        objects: targetMap.objects || [],
        shops: getShopsForWorld(link.targetWorld),
        garages: getGaragesForWorld(link.targetWorld),
        jobcenters: getJobsForWorld(link.targetWorld),
        gasStations: getGasStationsForWorld(link.targetWorld),
        repairShops: getRepairShopsForWorld(link.targetWorld),
        jobPoints: getJobPointsForPlayer(link.targetWorld, p.state.jobRankId),
        houses: getHousesForWorld(link.targetWorld),
        taxiStands: getTaxiStandsForWorld(link.targetWorld),
        hospitals: getHospitalsForWorld(link.targetWorld),
        prisons: getPrisonsForWorld(link.targetWorld),
        territoryZones: getZonesForWorld(link.targetWorld),
        impoundLots: getImpoundLotsForWorld(link.targetWorld),
        fireStations: getFireStationsForWorld(link.targetWorld),
        harvestSpots: getHarvestSpotsForWorld(link.targetWorld),
        clothingShops: getClothingShopsForWorld(link.targetWorld),
        insuranceOffices: getInsuranceOfficesForWorld(link.targetWorld),
        plateOffices: getPlateOfficesForWorld(link.targetWorld),
        trailerShops: getTrailerShopsForWorld(link.targetWorld),
highwayLinks: getHighwayLinksForWorld(link.targetWorld),
blackMarketSpots: getBlackMarketSpotsForWorld(link.targetWorld),
roadblocks: getRoadblocksForWorld(link.targetWorld),
        highwayLinks: getHighwayLinksForWorld(link.targetWorld),
blackMarketSpots: getBlackMarketSpotsForWorld(link.targetWorld),
roadblocks: getRoadblocksForWorld(link.targetWorld),
        groundDrops: getGroundDropsForWorld(link.targetWorld),
        processSpots: getProcessSpotsForWorld(link.targetWorld),
        dealerSpots: getActiveDealerSpotsForWorld(link.targetWorld),
        fires: getFiresForWorld(link.targetWorld),
        atms: targetMap.atms || [],
        spawn: targetMap.spawn
    }));

    sendStateToAll();
    sendCarsToAll();
}

function broadcastToAll(text, system) {
    const msg = JSON.stringify({ type: "chat_message", system: !!system, text });
    for (const [, p] of playersOnline) {
        try { p.ws.send(msg); } catch {}
    }
}

function addSystemLine(p, text) {
    p.ws.send(JSON.stringify({ type: "chat_message", system: true, text }));
}

// -------------------------------------------------------------
// INVENTAR/GELD-HILFSFUNKTIONEN (funktionieren auch bei Offline-Spielern,
// z.B. fürs Auktionshaus)
// -------------------------------------------------------------
// bypassWeightCheck: für Fälle, wo der Gegenstand schon "gehört" (z.B.
// gewonnene Auktion, schon bezahlt) - Ablehnen wäre dort unfair, das
// Geld ist ja bereits weg. Gibt zurück, wie viele Einheiten tatsächlich
// hinzugefügt wurden (kann bei Gewichtsprüfung weniger als angefragt sein).
async function addItemToInventory(playerId, itemId, amount, bypassWeightCheck) {
    const online = playersOnline.get(playerId);
    let inv;

    if (online) {
        inv = online.state.inventory;
    } else {
        const [rows] = await db.query("SELECT inventory FROM players WHERE id=?", [playerId]);
        if (rows.length === 0) return 0;
        inv = JSON.parse(rows[0].inventory || "[]");
    }

    let actualAmount = amount;
    if (!bypassWeightCheck) {
        actualAmount = getCarryableAmount(inv, itemId, amount);
        if (actualAmount <= 0) {
            if (online) online.ws.send(JSON.stringify({ type: "shop_error", msg: "Inventar zu schwer - kann nichts mehr aufnehmen." }));
            return 0;
        }
    }

    const existing = inv.find(i => i.id === itemId);
    if (existing) existing.amount += actualAmount;
    else inv.push({ id: itemId, amount: actualAmount });

    await db.query("UPDATE players SET inventory=? WHERE id=?", [JSON.stringify(inv), playerId]);

    if (online) {
        online.state.inventory = inv;
        sendStateToAll();
    }
    return actualAmount;
}

async function removeItemFromInventory(playerId, itemId, amount) {
    const online = playersOnline.get(playerId);
    let inv;

    if (online) {
        inv = online.state.inventory;
    } else {
        const [rows] = await db.query("SELECT inventory FROM players WHERE id=?", [playerId]);
        if (rows.length === 0) return false;
        inv = JSON.parse(rows[0].inventory || "[]");
    }

    const existing = inv.find(i => i.id === itemId);
    if (!existing || existing.amount < amount) return false;

    existing.amount -= amount;
    const newInv = inv.filter(i => i.amount > 0);

    await db.query("UPDATE players SET inventory=? WHERE id=?", [JSON.stringify(newInv), playerId]);

    if (online) {
        online.state.inventory = newInv;
        sendStateToAll();
    }
    return true;
}

async function getPlayerMoney(playerId) {
    const online = playersOnline.get(playerId);
    if (online) return online.state.money;
    const [rows] = await db.query("SELECT money FROM players WHERE id=?", [playerId]);
    return rows.length ? rows[0].money : 0;
}

async function adjustMoney(playerId, delta) {
    const online = playersOnline.get(playerId);
    if (online) {
        online.state.money = round2(Math.max(0, Number(online.state.money) + Number(delta)));
        await db.query("UPDATE players SET money=? WHERE id=?", [online.state.money, playerId]);
        sendStateToAll();
    } else {
        await db.query("UPDATE players SET money = GREATEST(0, money + ?) WHERE id=?", [delta, playerId]);
    }
}

async function adjustBank(playerId, delta) {
    const online = playersOnline.get(playerId);
    if (online) {
        online.state.bank = round2(Math.max(0, Number(online.state.bank) + Number(delta)));
        await db.query("UPDATE players SET bank=? WHERE id=?", [online.state.bank, playerId]);
        sendStateToAll();
    } else {
        await db.query("UPDATE players SET bank = GREATEST(0, bank + ?) WHERE id=?", [delta, playerId]);
    }
}

// -------------------------------------------------------------
// LEVEL/XP-SYSTEM
// -------------------------------------------------------------
function levelForXp(xp) {
    return Math.floor(xp / 500) + 1;
}

async function awardXp(playerId, p, amount, reason) {
    if (!p) return;
    const oldLevel = p.state.level || 1;

    if (isEventActive("doubleXp")) {
        amount = amount * 2;
        reason = reason ? `${reason}, Doppel-XP` : "Doppel-XP";
    }

    p.state.xp = (p.state.xp || 0) + amount;
    p.state.level = levelForXp(p.state.xp);

    await db.query("UPDATE players SET xp=?, level=? WHERE id=?", [p.state.xp, p.state.level, playerId]);
    sendStateToAll();

    p.ws.send(JSON.stringify({ type: "shop_info", msg: `+${amount} XP${reason ? " (" + reason + ")" : ""}` }));

    if (p.state.level > oldLevel) {
        p.ws.send(JSON.stringify({ type: "shop_info", msg: `🎉 Level ${p.state.level} erreicht!` }));
        broadcastToAll(`⭐ ${p.username} hat Level ${p.state.level} erreicht!`, true);
    }
}

// -------------------------------------------------------------
// ACHIEVEMENTS
// -------------------------------------------------------------
async function unlockAchievement(playerId, p, keyName) {
    if (!p) return;

    const [achRows] = await db.query("SELECT * FROM achievements WHERE key_name=?", [keyName]);
    if (achRows.length === 0) return;
    const ach = achRows[0];

    const [already] = await db.query(
        "SELECT 1 FROM player_achievements WHERE player_id=? AND achievement_id=?",
        [playerId, ach.id]
    );
    if (already.length > 0) return;

    await db.query(
        "INSERT INTO player_achievements (player_id, achievement_id) VALUES (?, ?)",
        [playerId, ach.id]
    );

    p.ws.send(JSON.stringify({ type: "shop_info", msg: `🏆 Achievement freigeschaltet: ${ach.name} - ${ach.description}` }));
    await awardXp(playerId, p, ach.xp_reward, ach.name);
}

async function checkAchievements(playerId, p) {
    if (!p) return;

    if (p.state.jobRankId) await unlockAchievement(playerId, p, "first_job");
    if ((p.state.deliveryCount || 0) >= 10) await unlockAchievement(playerId, p, "deliveries_10");
    if ((p.state.deliveryCount || 0) >= 50) await unlockAchievement(playerId, p, "deliveries_50");
    if ((p.state.robberyCount || 0) >= 1) await unlockAchievement(playerId, p, "first_robbery");
    if ((p.state.robberyCount || 0) >= 10) await unlockAchievement(playerId, p, "robberies_10");
    if ((p.state.money || 0) + (p.state.bank || 0) >= 50000) await unlockAchievement(playerId, p, "rich_50k");
}

// -------------------------------------------------------------
// LOG-SYSTEM
// -------------------------------------------------------------
async function logEvent(category, actor, message) {
    try {
        await db.query(
            "INSERT INTO logs (category, actor, message) VALUES (?, ?, ?)",
            [category, actor || null, message]
        );
    } catch (err) {
        console.error("Log-Fehler:", err);
    }
}

async function killPlayer(playerId, p, killerName, killerId) {
    p.ws.send(JSON.stringify({ type: "player_died", killerName: killerName || null }));

    // Kopfgeld auszahlen - nur bei einem echten PvP-Kill (killerId gesetzt), nicht bei
    // Unfällen (Auto, Feuer, Ertrinken usw.), sonst wäre Selbstmord-Farming möglich
    if (killerId && bounties.has(playerId)) {
        const bountyAmount = bounties.get(playerId);
        bounties.delete(playerId);
        await db.query("DELETE FROM bounties WHERE target_id=?", [playerId]);

        const killer = playersOnline.get(killerId);
        if (killer) {
            await adjustMoney(killerId, bountyAmount);
            killer.ws.send(JSON.stringify({ type: "shop_info", msg: `💀 Kopfgeld kassiert: +${bountyAmount}$ für ${p.username}!` }));
            broadcastToAll(`💰 ${killer.username} hat das Kopfgeld auf ${p.username} kassiert (${bountyAmount}$)!`, true);
            await logEvent("bounty", killer.username, `Kopfgeld auf ${p.username} kassiert: ${bountyAmount}$`);
        }
    }

    broadcastMedicAlert({
        crimeType: "Notfall - Person verletzt",
        world: p.state.world,
        x: Math.round(p.state.x),
        y: Math.round(p.state.y)
    });

    // Auto ggf. verlassen
    if (p.drivingCarId) {
        const c = cars.get(p.drivingCarId);
        if (c) c.driverId = null;
        p.drivingCarId = null;
    }
    if (p.passengerCarId) {
        const c = cars.get(p.passengerCarId);
        if (c) c.passengerId = null;
        p.passengerCarId = null;
    }

    setTimeout(async () => {
        const hospital = getAnyHospital();

        p.state.world = hospital.world;
        p.state.x = hospital.x * 32 + 16;
        p.state.y = hospital.y * 32 + 16;
        p.state.health = 50;

        // kleine "Arztrechnung"
        const fine = Math.min(p.state.money, Math.round(p.state.money * 0.1));
        p.state.money -= fine;

        await db.query(
            "UPDATE players SET world=?, x=?, y=?, health=?, money=? WHERE id=?",
            [p.state.world, p.state.x, p.state.y, p.state.health, p.state.money, playerId]
        );

        sendStateToAll();
        sendCarsToAll();

        const map = maps[p.state.world];
        if (map) {
            p.ws.send(JSON.stringify({
                type: "map_data",
                tiles: map.tiles,
                tileRot: map.tileRot || null,
                doors: map.doors || [],
                objects: map.objects || [],
                shops: getShopsForWorld(p.state.world),
                garages: getGaragesForWorld(p.state.world),
                jobcenters: getJobsForWorld(p.state.world),
                gasStations: getGasStationsForWorld(p.state.world),
                repairShops: getRepairShopsForWorld(p.state.world),
                jobPoints: getJobPointsForPlayer(p.state.world, p.state.jobRankId),
                houses: getHousesForWorld(p.state.world),
                taxiStands: getTaxiStandsForWorld(p.state.world),
                hospitals: getHospitalsForWorld(p.state.world),
                prisons: getPrisonsForWorld(p.state.world),
                territoryZones: getZonesForWorld(p.state.world),
                impoundLots: getImpoundLotsForWorld(p.state.world),
                fireStations: getFireStationsForWorld(p.state.world),
                harvestSpots: getHarvestSpotsForWorld(p.state.world),
                clothingShops: getClothingShopsForWorld(p.state.world),
                insuranceOffices: getInsuranceOfficesForWorld(p.state.world),
                plateOffices: getPlateOfficesForWorld(p.state.world),
                trailerShops: getTrailerShopsForWorld(p.state.world),
highwayLinks: getHighwayLinksForWorld(p.state.world),
blackMarketSpots: getBlackMarketSpotsForWorld(p.state.world),
roadblocks: getRoadblocksForWorld(p.state.world),
                groundDrops: getGroundDropsForWorld(p.state.world),
                processSpots: getProcessSpotsForWorld(p.state.world),
                dealerSpots: getActiveDealerSpotsForWorld(p.state.world),
                fires: getFiresForWorld(p.state.world),
                atms: map.atms || [],
                spawn: map.spawn
            }));
        }

        p.ws.send(JSON.stringify({
            type: "shop_info",
            msg: `Im Krankenhaus wiederbelebt. Arztrechnung: -${fine}$`
        }));
    }, 3000);
}

let activeRestartInterval = null;

// -------------------------------------------------------------
// AUTOMATISCHER TÄGLICHER NEUSTART: prüft jede Minute, ob die
// eingestellte Uhrzeit erreicht ist - mit Vorwarnung wie beim
// manuellen Neustart, damit sich Spieler in Sicherheit bringen können
// -------------------------------------------------------------
let lastAutoRestartDate = null; // verhindert Mehrfachauslösung innerhalb derselben Minute/desselben Tages

setInterval(() => {
    if (getSiteConfig("auto_restart_enabled") !== "1") return;

    const configuredTime = getSiteConfig("auto_restart_time"); // Format "HH:MM"
    if (!configuredTime) return;

    const now = new Date();
    const nowHHMM = String(now.getHours()).padStart(2, "0") + ":" + String(now.getMinutes()).padStart(2, "0");
    const today = now.toISOString().slice(0, 10);

    if (nowHHMM === configuredTime && lastAutoRestartDate !== today) {
        lastAutoRestartDate = today;
        if (!activeRestartInterval) {
            triggerServerRestartCountdown(60, "Automatischer Neustart (geplant)");
        }
    }
}, 30000); // alle 30 Sekunden prüfen, reicht für minutengenaue Auslösung

function triggerServerRestartCountdown(seconds, triggeredBy) {
    let remaining = seconds;

    broadcastToAll(`⚠️ Server-Neustart in ${seconds} Sekunden angekündigt! Bitte bring dich in Sicherheit (Auto parken, aus Gebäuden raus).`, true);

    sendDiscordWebhook("restart", {
        title: "⚠️ Server-Neustart angekündigt",
        description: `Neustart in ${seconds} Sekunden, ausgelöst von ${triggeredBy}.`,
        color: 0xc77a1f,
        timestamp: new Date().toISOString()
    });

    const warnAt = new Set([120, 60, 30, 20, 10, 5, 4, 3, 2, 1].filter(s => s < seconds));

    activeRestartInterval = setInterval(async () => {
        remaining--;

        if (warnAt.has(remaining)) {
            broadcastToAll(`⚠️ Server-Neustart in ${remaining}s!`, true);
        }

        if (remaining <= 0) {
            clearInterval(activeRestartInterval);
            activeRestartInterval = null;

            const restartMsg = JSON.stringify({ type: "server_restart_now" });
            for (const [, pl] of playersOnline) {
                try { pl.ws.send(restartMsg); } catch {}
            }

            console.log(`[Admin] Server-Neustart von ${triggeredBy} ausgelöst.`);
            await logEvent("server", triggeredBy, "Server-Neustart ausgelöst");

            setTimeout(() => {
                for (const [, pl] of playersOnline) {
                    try { pl.ws.close(); } catch {}
                }
                process.exit(0);
            }, 1200);
        }
    }, 1000);
}

function cancelServerRestartCountdown() {
    if (!activeRestartInterval) return false;
    clearInterval(activeRestartInterval);
    activeRestartInterval = null;
    broadcastToAll("✅ Server-Neustart wurde abgebrochen.", true);
    return true;
}

// -------------------------------------------------------------
// JOB-PUNKTE (Ziele für Leute in einem bestimmten Job)
// -------------------------------------------------------------
const jobPoints = new Map();

async function loadJobPoints() {
    jobPoints.clear();
    const [rows] = await db.query("SELECT * FROM job_points");
    for (const jp of rows) {
        jobPoints.set(jp.id, jp);
    }
    console.log("Job-Punkte geladen:", jobPoints.size);
}

// Nur die Punkte des Jobs, den DIESER Spieler gerade ausübt
function getJobPointsForPlayer(world, jobRankId) {
    const rank = jobRanks.get(jobRankId);
    if (!rank) return [];

    const list = [];
    for (const [, jp] of jobPoints) {
        if (jp.world === world && jp.job_id === rank.jobId) {
            list.push({ id: jp.id, x: jp.x, y: jp.y, name: jp.name, kind: jp.kind, reward: jp.reward });
        }
    }
    return list;
}

function findJobPointNear(world, x, y) {
    for (const [, jp] of jobPoints) {
        if (jp.world !== world) continue;
        if (Math.abs(jp.x * 32 - x) < 32 && Math.abs(jp.y * 32 - y) < 32) return jp;
    }
    return null;
}

// -------------------------------------------------------------
// HÄUSER (mit Schlüssel-System)
// -------------------------------------------------------------
const houses = new Map();

async function loadHouses() {
    houses.clear();
    const [rows] = await db.query("SELECT * FROM houses");
    for (const h of rows) {
        houses.set(h.id, h);
    }
    console.log("Häuser geladen:", houses.size);
}

function getHousesForWorld(world) {
    const list = [];
    for (const [, h] of houses) {
        if (h.world === world) {
            list.push({
                id: h.id, x: h.x, y: h.y, name: h.name, price: h.price, owned: !!h.owner_id,
                ownerId: h.owner_id || null, rentPrice: h.rent_price || null, renterId: h.renter_id || null
            });
        }
    }
    return list;
}

function findHouseNear(world, x, y) {
    for (const [, h] of houses) {
        if (h.world !== world) continue;
        if (Math.abs(h.x * 32 - x) < 32 && Math.abs(h.y * 32 - y) < 32) return h;
    }
    return null;
}

// Innenraum: alle Häuser teilen sich dieselbe Zimmer-Vorlage (Map-Name "haus_innen",
// im Map-Editor normal anlegen), aber jedes Haus bekommt seine eigene "Welt"
// (house_<id>), damit Spieler in verschiedenen Häusern sich nicht sehen.

function ensureHouseInteriorMap(houseId) {
    const key = "house_" + houseId;
    if (!maps[HOUSE_TEMPLATE_MAP]) return null; // Vorlage existiert noch nicht
    maps[key] = maps[HOUSE_TEMPLATE_MAP]; // immer aktuellen Stand der Vorlage verwenden
    return key;
}

function isHouseInteriorWorld(world) {
    return typeof world === "string" && world.startsWith("house_");
}

await loadGameSettings();
await loadSiteConfig();
await loadDiscordWebhooks();
await loadBounties();
await loadAchievementTitles();
await loadClothingShops();
await loadInsuranceOffices();
await loadPlateOffices();
await loadTrailerShops();
await loadHighwayLinks();
await loadStocks();
await loadBlackMarketSpots();
await loadPermissionGroups();
await loadClothingItems();
await loadSkinItems();
await loadTileConfig();
await loadObjectConfig();
await loadMaps();
await loadCarConfigs();
await loadItemWeights();
await loadCars();
await loadShops();
await loadGarages();
await loadJobs();
await loadJobcenters();
await loadGasStations();
await loadRepairShops();
await loadJobPoints();
await loadHouses();
await loadTaxiStands();
await loadHospitals();
await loadPrisons();
await loadTerritoryZones();
await loadGates();
await loadImpoundLots();
await loadFireStations();
await loadDrugTypes();
await loadDrugHarvestSpots();
await loadDrugProcessSpots();
await loadDrugDealerSpots();
// REGISTER
// -------------------------------------------------------------
app.post("/api/register", async (req, res) => {
    const { username, password, applicationText } = req.body;

    if (!applicationText || applicationText.trim().length < 20) {
        return res.json({ ok: false, error: "Bitte kurz begründen, warum du mitspielen möchtest (mind. 20 Zeichen)." });
    }

    const hash = await bcrypt.hash(password, 10);

    try {
        await db.query(
            "INSERT INTO players (username, password_hash, world, x, y, approved, application_text) VALUES (?, ?, 'stadt', 112, 144, 0, ?)",
            [username, hash, applicationText.trim().slice(0, 2000)]
        );
        res.json({ ok: true, pendingApproval: true });
    } catch {
        res.json({ ok: false, error: "Username vergeben" });
    }
});

// -------------------------------------------------------------
// LOGIN
// -------------------------------------------------------------
app.post("/api/login", async (req, res) => {
    const { username, password } = req.body;

    const [rows] = await db.query(
        "SELECT * FROM players WHERE username = ?",
        [username]
    );

    if (rows.length === 0)
        return res.json({ ok: false, error: "User nicht gefunden" });

    const player = rows[0];

    if (!await bcrypt.compare(password, player.password_hash))
        return res.json({ ok: false, error: "Passwort falsch" });

    if (!player.approved) {
        return res.json({ ok: false, error: "Account wartet noch auf Freischaltung durch einen Admin." });
    }

    if (player.banned) {
        return res.json({ ok: false, error: "Dieser Account wurde gesperrt." });
    }

    const token = jwt.sign(
        { id: player.id, username: player.username, isAdmin: !!player.is_admin, isBetaTester: !!player.is_beta_tester, permissionGroupId: player.permission_group_id || null },
        JWT_SECRET
    );

    res.json({ ok: true, token, isAdmin: !!player.is_admin, isBetaTester: !!player.is_beta_tester, username: player.username });
});

// -------------------------------------------------------------
// MAP SPEICHERN (Editor)
// -------------------------------------------------------------
app.post("/api/save_map", requirePermission("manage_maps"), async (req, res) => {
    const { name, data } = req.body;

    if (!name || !data) {
        return res.json({ ok: false, error: "Name oder Daten fehlen" });
    }

    try {
        const json = JSON.stringify(data);
        await db.query(
            "INSERT INTO maps (name, data) VALUES (?, ?) ON DUPLICATE KEY UPDATE data=?",
            [name, json, json]
        );
        maps[name] = data; // In-Memory-Cache direkt aktualisieren, kein Reload nötig

        // Alle Spieler, die sich gerade in dieser Welt befinden, live aktualisieren
        for (const [, p] of playersOnline) {
            if (p.state.world !== name) continue;

            p.ws.send(JSON.stringify({
                type: "map_data",
                tiles: data.tiles,
                tileRot: data.tileRot || null,
                doors: data.doors || [],
                objects: data.objects || [],
                shops: getShopsForWorld(name),
                garages: getGaragesForWorld(name),
                jobcenters: getJobsForWorld(name),
                gasStations: getGasStationsForWorld(name),
                repairShops: getRepairShopsForWorld(name),
                jobPoints: getJobPointsForPlayer(name, p.state.jobRankId),
                houses: getHousesForWorld(name),
                taxiStands: getTaxiStandsForWorld(name),
                hospitals: getHospitalsForWorld(name),
                prisons: getPrisonsForWorld(name),
                territoryZones: getZonesForWorld(name),
                impoundLots: getImpoundLotsForWorld(name),
                fireStations: getFireStationsForWorld(name),
                harvestSpots: getHarvestSpotsForWorld(name),
                clothingShops: getClothingShopsForWorld(name),
                insuranceOffices: getInsuranceOfficesForWorld(name),
                plateOffices: getPlateOfficesForWorld(name),
                trailerShops: getTrailerShopsForWorld(name),
highwayLinks: getHighwayLinksForWorld(name),
blackMarketSpots: getBlackMarketSpotsForWorld(name),
roadblocks: getRoadblocksForWorld(name),
                groundDrops: getGroundDropsForWorld(name),
                processSpots: getProcessSpotsForWorld(name),
                dealerSpots: getActiveDealerSpotsForWorld(name),
                fires: getFiresForWorld(name),
                atms: data.atms || [],
                spawn: data.spawn
            }));
        }

        res.json({ ok: true });
    } catch (err) {
        console.error(err);
        res.json({ ok: false, error: "Speichern fehlgeschlagen" });
    }
});

app.get("/api/get_maps", (req, res) => {
    res.json({ ok: true, maps });
});

// -------------------------------------------------------------
// 3D-OBJEKTE (aus dem experimentellen editor3d.html) - pro Welt
// gespeichert, öffentlich lesbar (der 3D-Testclient braucht keinen
// Login für den Boden/Objekte), Speichern nur mit manage_maps-Recht
// -------------------------------------------------------------
app.get("/api/get_3d_objects", async (req, res) => {
    const world = req.query.world;
    if (!world) return res.json({ ok: false, error: "world fehlt" });

    const [rows] = await db.query("SELECT * FROM map_objects_3d WHERE world=?", [world]);
    res.json({ ok: true, objects: rows });
});

app.post("/api/save_3d_objects", requirePermission("manage_maps"), async (req, res) => {
    const { world, objects } = req.body;
    if (!world || !Array.isArray(objects)) {
        return res.json({ ok: false, error: "world oder objects fehlen" });
    }

    try {
        // Einfachster verlässlicher Ansatz für den Prototyp: alle
        // bisherigen Objekte dieser Welt ersetzen, statt einzeln
        // abzugleichen (spart komplexe Diff-Logik, ist bei den
        // überschaubaren Objektmengen einer Karte unproblematisch)
        await db.query("DELETE FROM map_objects_3d WHERE world=?", [world]);

        for (const o of objects) {
            await db.query(
                `INSERT INTO map_objects_3d (world, type, x, y, z, rotation_y, scale, blueprint, portal_id, linked_to, label_text)
                 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
                [world, o.type, o.x, o.y, o.z, o.rotationY || 0, o.scale || 1,
                 // "[]" statt null, falls die blueprint-Spalte als NOT NULL
                 // angelegt wurde (normale Palette-Objekte wie Haus/Baum/
                 // Portal haben keinen eigenen Bauplan)
                 o.blueprint ? JSON.stringify(o.blueprint) : "[]",
                 o.portalId || null, o.linkedTo || null, o.labelText || null]
            );
        }

        res.json({ ok: true, count: objects.length });
    } catch (err) {
        console.error("Fehler beim Speichern der 3D-Objekte:", err);
        res.json({ ok: false, error: err.sqlMessage || err.message || "Speichern fehlgeschlagen" });
    }
});

// -------------------------------------------------------------
// 3D-OBJEKTE IN EINE ANDERE WELT KOPIEREN: fügt neue Zeilen hinzu,
// löscht/ersetzt NICHTS in der Zielwelt (anders als save_3d_objects)
// -------------------------------------------------------------
app.post("/api/copy_3d_objects", requirePermission("manage_maps"), async (req, res) => {
    const { targetWorld, objects } = req.body;
    if (!targetWorld || !Array.isArray(objects) || objects.length === 0) {
        return res.json({ ok: false, error: "targetWorld oder objects fehlen" });
    }

    try {
        for (const o of objects) {
            await db.query(
                `INSERT INTO map_objects_3d (world, type, x, y, z, rotation_y, scale, blueprint, portal_id, linked_to, label_text)
                 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
                [targetWorld, o.type, o.x, o.y, o.z, o.rotationY || 0, o.scale || 1,
                 o.blueprint ? JSON.stringify(o.blueprint) : "[]",
                 null, null, o.labelText || null] // Portal-Verknüpfungen bewusst NICHT mitkopiert - Ziel-IDs wären in der neuen Welt ungültig
            );
        }
        res.json({ ok: true, count: objects.length });
    } catch (err) {
        console.error("Fehler beim Kopieren der 3D-Objekte:", err);
        res.json({ ok: false, error: err.sqlMessage || err.message || "Kopieren fehlgeschlagen" });
    }
});

// -------------------------------------------------------------
// 3D-AUTO-MODELLE: verknüpft ein selbstgebautes Objekt (aus dem
// Auto-Baukasten in editor3d.html) mit einem echten car_configs.model
// -Namen - der 3D-Client nutzt das dann für ALLE Fahrzeuge dieses
// Modells statt des CesiumMilkTruck-Platzhalters
// -------------------------------------------------------------
app.get("/api/car_models_3d", async (req, res) => {
    const [rows] = await db.query("SELECT model, blueprint FROM car_models_3d");
    const result = {};
    for (const row of rows) result[row.model] = typeof row.blueprint === "string" ? JSON.parse(row.blueprint) : row.blueprint;
    res.json({ ok: true, models: result });
});

app.post("/api/admin/car_models_3d", requirePermission("manage_maps"), async (req, res) => {
    const { model, blueprint } = req.body;
    if (!model || !Array.isArray(blueprint)) {
        return res.json({ ok: false, error: "model oder blueprint fehlen" });
    }

    try {
        await db.query(
            "INSERT INTO car_models_3d (model, blueprint) VALUES (?, ?) ON DUPLICATE KEY UPDATE blueprint=?",
            [model, JSON.stringify(blueprint), JSON.stringify(blueprint)]
        );
        res.json({ ok: true });
    } catch (err) {
        console.error("Fehler beim Speichern des 3D-Auto-Modells:", err);
        res.json({ ok: false, error: "Speichern fehlgeschlagen" });
    }
});

// -------------------------------------------------------------
// NEWS: öffentlich lesbar
// -------------------------------------------------------------
// -------------------------------------------------------------
// -------------------------------------------------------------
// TILE/OBJECT CONFIG: öffentlich, ersetzt die alten tiles.json/objectConfig.json
// -------------------------------------------------------------
app.get("/api/tile_config", (req, res) => {
    res.json(tileConfig);
});

app.get("/api/object_config", (req, res) => {
    res.json(objectConfig);
});

// -------------------------------------------------------------
// STATUS: öffentlich, für Online-Spieleranzahl (z.B. auf der Startseite)
// -------------------------------------------------------------
app.get("/api/status", async (req, res) => {
    const [rows] = await db.query("SELECT version FROM changelog ORDER BY created_at DESC LIMIT 1");
    const version = rows.length > 0 ? rows[0].version : APP_VERSION;
    res.json({ ok: true, online: playersOnline.size, version });
});

app.get("/api/site_config", async (req, res) => {
    res.json({ ok: true, title: getSiteConfig("site_title") });
});

app.post("/api/admin/site_config", async (req, res) => {
    const { title } = req.body;
    const finalTitle = String(title || "").trim().slice(0, 100) || SITE_CONFIG_DEFAULTS.site_title;

    await db.query(
        "INSERT INTO site_config (config_key, config_value) VALUES ('site_title', ?) ON DUPLICATE KEY UPDATE config_value=?",
        [finalTitle, finalTitle]
    );
    await loadSiteConfig();
    res.json({ ok: true });
});

app.get("/api/admin/admin_look", async (req, res) => {
    res.json({
        ok: true,
        shirtId: getSiteConfig("admin_shirt_id") || "",
        pantsId: getSiteConfig("admin_pants_id") || "",
        shoesId: getSiteConfig("admin_shoes_id") || "",
        helmetId: getSiteConfig("admin_helmet_id") || "",
        skinId: getSiteConfig("admin_skin_id") || ""
    });
});

app.post("/api/admin/admin_look", async (req, res) => {
    const { shirtId, pantsId, shoesId, helmetId, skinId } = req.body;
    const fields = {
        admin_shirt_id: shirtId, admin_pants_id: pantsId,
        admin_shoes_id: shoesId, admin_helmet_id: helmetId, admin_skin_id: skinId
    };

    for (const [key, value] of Object.entries(fields)) {
        const clean = String(value || "").trim();
        if (clean) {
            await db.query(
                "INSERT INTO site_config (config_key, config_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE config_value=?",
                [key, clean, clean]
            );
        } else {
            await db.query("DELETE FROM site_config WHERE config_key=?", [key]);
        }
    }
    await loadSiteConfig();
    res.json({ ok: true });
});

app.get("/api/admin/discord_webhooks", async (req, res) => {
    const [rows] = await db.query("SELECT * FROM discord_webhooks");
    const result = {};
    for (const row of rows) result[row.hook_key] = row.webhook_url;
    res.json({ ok: true, webhooks: result });
});

app.post("/api/admin/discord_webhooks", async (req, res) => {
    const { key, url } = req.body;
    const allowedKeys = ["news", "changelog", "restart", "maintenance", "online_status", "tickets", "wishes", "surveys"];
    if (!allowedKeys.includes(key)) {
        return res.json({ ok: false, error: "Ungültiger Kanal-Typ." });
    }
    const cleanUrl = String(url || "").trim();
    if (cleanUrl && !cleanUrl.startsWith("https://discord.com/api/webhooks/") && !cleanUrl.startsWith("https://discordapp.com/api/webhooks/")) {
        return res.json({ ok: false, error: "Das sieht nicht nach einer gültigen Discord-Webhook-URL aus." });
    }

    if (cleanUrl) {
        await db.query(
            "INSERT INTO discord_webhooks (hook_key, webhook_url) VALUES (?, ?) ON DUPLICATE KEY UPDATE webhook_url=?",
            [key, cleanUrl, cleanUrl]
        );
    } else {
        await db.query("DELETE FROM discord_webhooks WHERE hook_key=?", [key]);
    }
    await loadDiscordWebhooks();
    res.json({ ok: true });
});

app.get("/api/news", async (req, res) => {
    const [rows] = await db.query(
        "SELECT id, title, content, author, created_at FROM news ORDER BY created_at DESC LIMIT 50"
    );
    res.json({ ok: true, news: rows });
});

// -------------------------------------------------------------
// CHANGELOG: öffentlich lesbar
// -------------------------------------------------------------
app.get("/api/changelog", async (req, res) => {
    const [rows] = await db.query(
        "SELECT id, version, title, content, created_at FROM changelog ORDER BY created_at DESC LIMIT 50"
    );
    res.json({ ok: true, changelog: rows });
});

// -------------------------------------------------------------
// ADMIN-SCHUTZ: nur eingeloggte Admins dürfen /api/admin/* nutzen
// -------------------------------------------------------------
// -------------------------------------------------------------
// SERVER-NEUSTART über die Web-Oberfläche (Übersicht-Seite) - unabhängig
// vom Admin-Dienst im Spiel, da hier schon der JWT-Login als Admin reicht
// -------------------------------------------------------------
app.post("/api/admin/restart_server", async (req, res) => {
    const { cancel, seconds } = req.body;

    if (cancel) {
        const wasActive = cancelServerRestartCountdown();
        return res.json({ ok: true, cancelled: wasActive });
    }

    if (activeRestartInterval) {
        return res.json({ ok: false, error: "Es läuft bereits ein Neustart-Countdown." });
    }

    const finalSeconds = Math.max(5, Math.min(600, Math.floor(Number(seconds) || 60)));
    triggerServerRestartCountdown(finalSeconds, req.player.username);
    res.json({ ok: true, seconds: finalSeconds });
});

app.get("/api/admin/restart_status", async (req, res) => {
    res.json({ ok: true, active: !!activeRestartInterval });
});

// -------------------------------------------------------------
// WARTUNGSMODUS: blockiert neue Logins für Nicht-Admins
// -------------------------------------------------------------
app.get("/api/admin/maintenance_mode", async (req, res) => {
    res.json({ ok: true, active: getSiteConfig("maintenance_mode") === "1" });
});

app.post("/api/admin/maintenance_mode", async (req, res) => {
    const { active } = req.body;
    const value = active ? "1" : "0";

    await db.query(
        "INSERT INTO site_config (config_key, config_value) VALUES ('maintenance_mode', ?) ON DUPLICATE KEY UPDATE config_value=?",
        [value, value]
    );
    await loadSiteConfig();

    if (active) {
        broadcastToAll("🔧 Der Server geht in den Wartungsmodus - neue Logins sind vorübergehend gesperrt.", true);
        sendDiscordWebhook("maintenance", {
            title: "🔧 Wartungsmodus aktiviert",
            description: `Ausgelöst von ${req.player.username}. Neue Spiel-Logins sind vorübergehend gesperrt.`,
            color: 0xc77a1f,
            timestamp: new Date().toISOString()
        });
    } else {
        broadcastToAll("✅ Wartungsmodus beendet - Logins sind wieder möglich.", true);
        sendDiscordWebhook("maintenance", {
            title: "✅ Wartungsmodus beendet",
            description: `Ausgelöst von ${req.player.username}. Logins sind wieder möglich.`,
            color: 0x2c7a3d,
            timestamp: new Date().toISOString()
        });
    }

    res.json({ ok: true, active });
});

// -------------------------------------------------------------
// BETA-MODUS: wenn aktiv, dürfen nur Admins + als Beta-Tester
// markierte Spieler sich einloggen - alle anderen bekommen eine
// klare Fehlermeldung statt ins Spiel zu kommen
// -------------------------------------------------------------
app.get("/api/admin/beta_mode", async (req, res) => {
    res.json({ ok: true, active: getSiteConfig("beta_mode_enabled") === "1" });
});

app.post("/api/admin/beta_mode", async (req, res) => {
    const { active } = req.body;
    const value = active ? "1" : "0";

    await db.query(
        "INSERT INTO site_config (config_key, config_value) VALUES ('beta_mode_enabled', ?) ON DUPLICATE KEY UPDATE config_value=?",
        [value, value]
    );
    await loadSiteConfig();
    res.json({ ok: true, active });
});

app.get("/api/admin/beta_testers", async (req, res) => {
    const [rows] = await db.query("SELECT id, username FROM players WHERE is_beta_tester=1 ORDER BY username");
    res.json({ ok: true, testers: rows });
});

app.post("/api/admin/set_beta_tester", async (req, res) => {
    const { username, isBetaTester } = req.body;
    if (!username) return res.json({ ok: false, error: "username fehlt" });

    const [result] = await db.query(
        "UPDATE players SET is_beta_tester=? WHERE username=?",
        [isBetaTester ? 1 : 0, username]
    );
    if (result.affectedRows === 0) {
        return res.json({ ok: false, error: "Spieler nicht gefunden" });
    }
    res.json({ ok: true });
});


app.get("/api/admin/auto_restart", async (req, res) => {
    res.json({
        ok: true,
        enabled: getSiteConfig("auto_restart_enabled") === "1",
        time: getSiteConfig("auto_restart_time") || "04:00"
    });
});

app.post("/api/admin/auto_restart", async (req, res) => {
    const { enabled, time } = req.body;

    if (time && !/^([01]\d|2[0-3]):([0-5]\d)$/.test(time)) {
        return res.json({ ok: false, error: "Ungültige Uhrzeit (Format HH:MM erwartet)" });
    }

    const enabledValue = enabled ? "1" : "0";
    await db.query(
        "INSERT INTO site_config (config_key, config_value) VALUES ('auto_restart_enabled', ?) ON DUPLICATE KEY UPDATE config_value=?",
        [enabledValue, enabledValue]
    );
    if (time) {
        await db.query(
            "INSERT INTO site_config (config_key, config_value) VALUES ('auto_restart_time', ?) ON DUPLICATE KEY UPDATE config_value=?",
            [time, time]
        );
    }
    await loadSiteConfig();

    res.json({ ok: true, enabled: !!enabled, time: getSiteConfig("auto_restart_time") });
});

// -------------------------------------------------------------
// AUTH-SCHUTZ: für Endpunkte, die JEDER eingeloggte Spieler nutzen darf
// (nicht nur Admins) - z.B. das eigene Profil
// -------------------------------------------------------------
function requireAuth(req, res, next) {
    const authHeader = req.headers.authorization || "";
    const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : null;

    if (!token) {
        return res.status(401).json({ ok: false, error: "Nicht eingeloggt" });
    }

    try {
        req.player = jwt.verify(token, JWT_SECRET);
        next();
    } catch {
        res.status(401).json({ ok: false, error: "Ungültiger oder abgelaufener Token" });
    }
}

// Leichter Endpunkt, mit dem Client-Seiten (Übersicht, admin_*.html) prüfen
// können, welche Admin-Bereiche für den eingeloggten Spieler sichtbar sein
// sollen - Voll-Admins bekommen alle Schlüssel, Team-Mitglieder nur ihre Gruppe
// -------------------------------------------------------------
// BÖRSE: öffentliche Kursliste + Verlauf, Portfolio nur eingeloggt
// -------------------------------------------------------------
app.get("/api/stocks", async (req, res) => {
    res.json({ ok: true, stocks: [...stocks.values()] });
});

app.get("/api/stocks/:id/history", async (req, res) => {
    const [rows] = await db.query(
        "SELECT price, recorded_at FROM stock_price_history WHERE stock_id=? ORDER BY recorded_at ASC LIMIT 500",
        [req.params.id]
    );
    res.json({ ok: true, history: rows });
});

app.get("/api/stocks/portfolio", requireAuth, async (req, res) => {
    const [rows] = await db.query(`
        SELECT ps.stock_id, ps.shares, s.symbol, s.name, s.price
        FROM player_stocks ps JOIN stocks s ON s.id = ps.stock_id
        WHERE ps.player_id=? AND ps.shares > 0
    `, [req.player.id]);
    res.json({ ok: true, portfolio: rows });
});

app.get("/api/my_permissions", requireAuth, async (req, res) => {
    const [rows] = await db.query("SELECT is_admin, permission_group_id FROM players WHERE id=?", [req.player.id]);
    if (rows.length === 0) return res.json({ ok: false, error: "Account nicht gefunden" });

    const isAdmin = !!rows[0].is_admin;
    const permissions = isAdmin
        ? PERMISSION_KEYS
        : [...(permissionGroups.get(rows[0].permission_group_id)?.permissions || [])];

    res.json({ ok: true, isAdmin, permissions });
});

// -------------------------------------------------------------
// PROFIL (jeder eingeloggte Spieler für sich selbst)
// -------------------------------------------------------------
// -------------------------------------------------------------
// SPRACHCHAT: LiveKit-Zugangstoken erstellen
// -------------------------------------------------------------
app.get("/api/voice/token", requireAuth, async (req, res) => {
    if (LIVEKIT_API_KEY.startsWith("TODO")) {
        return res.json({ ok: false, error: "LiveKit ist serverseitig noch nicht konfiguriert (LIVEKIT_API_KEY/SECRET in server.js eintragen)." });
    }

    const online = playersOnline.get(req.player.id);
    const world = online ? online.state.world : "lobby";

    // Ein Raum pro Spielwelt - alle in derselben Welt sind im selben Sprachraum,
    // die Lautstärke-Abschwächung nach Entfernung übernimmt der Client
    const roomName = "voice_" + world.replace(/[^a-zA-Z0-9_]/g, "_");

    try {
        const at = new AccessToken(LIVEKIT_API_KEY, LIVEKIT_API_SECRET, {
            identity: String(req.player.id),
            name: req.player.username
        });
        at.addGrant({ roomJoin: true, room: roomName, canPublish: true, canSubscribe: true });

        const token = await at.toJwt();
        res.json({ ok: true, url: LIVEKIT_URL, token, room: roomName });
    } catch (err) {
        console.error("LiveKit-Token-Fehler:", err);
        res.json({ ok: false, error: "Token konnte nicht erstellt werden." });
    }
});

// -------------------------------------------------------------
// SUPPORT-TICKETS: Spieler-Seite (eigene Tickets)
// -------------------------------------------------------------
app.get("/api/tickets", requireAuth, async (req, res) => {
    const [rows] = await db.query(
        "SELECT * FROM tickets WHERE player_id=? ORDER BY updated_at DESC",
        [req.player.id]
    );
    res.json({ ok: true, tickets: rows });
});

app.get("/api/tickets/:id", requireAuth, async (req, res) => {
    const [ticketRows] = await db.query("SELECT * FROM tickets WHERE id=?", [req.params.id]);
    if (ticketRows.length === 0) return res.json({ ok: false, error: "Ticket nicht gefunden" });
    const ticket = ticketRows[0];

    // Eigene Tickets darf man sehen, fremde nur mit der Berechtigung
    if (ticket.player_id !== req.player.id && !(await playerHasPermission(req.player.id, "manage_tickets"))) {
        return res.status(403).json({ ok: false, error: "Kein Zugriff auf dieses Ticket" });
    }

    const [messages] = await db.query(
        "SELECT tm.*, p.username FROM ticket_messages tm JOIN players p ON p.id = tm.sender_id WHERE ticket_id=? ORDER BY created_at ASC",
        [req.params.id]
    );
    res.json({ ok: true, ticket, messages });
});

app.post("/api/tickets", requireAuth, async (req, res) => {
    const { subject, category, message } = req.body;
    if (!subject || !message) {
        return res.json({ ok: false, error: "Betreff und Nachricht erforderlich" });
    }
    const cleanCategory = String(category || "sonstiges").slice(0, 30);

    const [result] = await db.query(
        "INSERT INTO tickets (player_id, subject, category) VALUES (?, ?, ?)",
        [req.player.id, String(subject).slice(0, 200), cleanCategory]
    );
    await db.query(
        "INSERT INTO ticket_messages (ticket_id, sender_id, is_admin_reply, message) VALUES (?, ?, 0, ?)",
        [result.insertId, req.player.id, String(message).slice(0, 2000)]
    );

    notifyTicketWatchers(result.insertId, "new_ticket");
    sendDiscordWebhook("tickets", {
        title: "🎫 Neues Support-Ticket",
        description: `**${String(subject).slice(0, 200)}**\n${String(message).slice(0, 300)}`,
        color: 0x5b8def,
        fields: [
            { name: "Von", value: req.player.username, inline: true },
            { name: "Kategorie", value: cleanCategory, inline: true }
        ],
        timestamp: new Date().toISOString()
    });
    res.json({ ok: true, id: result.insertId });
});

app.post("/api/tickets/:id/reply", requireAuth, async (req, res) => {
    const { message } = req.body;
    if (!message) return res.json({ ok: false, error: "Nachricht erforderlich" });

    const [ticketRows] = await db.query("SELECT * FROM tickets WHERE id=?", [req.params.id]);
    if (ticketRows.length === 0) return res.json({ ok: false, error: "Ticket nicht gefunden" });
    const ticket = ticketRows[0];

    const isOwner = ticket.player_id === req.player.id;
    const isStaff = await playerHasPermission(req.player.id, "manage_tickets");
    if (!isOwner && !isStaff) return res.status(403).json({ ok: false, error: "Kein Zugriff auf dieses Ticket" });
    if (ticket.status === "closed" && !isStaff) {
        return res.json({ ok: false, error: "Dieses Ticket ist geschlossen." });
    }

    await db.query(
        "INSERT INTO ticket_messages (ticket_id, sender_id, is_admin_reply, message) VALUES (?, ?, ?, ?)",
        [req.params.id, req.player.id, isStaff && !isOwner ? 1 : 0, String(message).slice(0, 2000)]
    );
    await db.query(
        "UPDATE tickets SET status=?, updated_at=NOW() WHERE id=?",
        [isStaff && !isOwner ? "in_progress" : "open", req.params.id]
    );

    notifyTicketWatchers(req.params.id, "ticket_reply");
    res.json({ ok: true });
});

app.get("/api/me", requireAuth, async (req, res) => {
    const [rows] = await db.query(
        "SELECT id, username, money, bank, health, hunger, thirst, world, is_admin, color, skin, skin_item_id, playtime_seconds FROM players WHERE id=?",
        [req.player.id]
    );
    if (rows.length === 0) {
        return res.json({ ok: false, error: "Spieler nicht gefunden" });
    }

    // Falls der Spieler gerade online ist, aktuellste Live-Werte statt DB-Stand nehmen
    const online = playersOnline.get(req.player.id);
    const row = rows[0];

    res.json({
        ok: true,
        player: {
            id: row.id,
            username: row.username,
            money: online ? online.state.money : row.money,
            bank: online ? online.state.bank : row.bank,
            health: online ? online.state.health : row.health,
            hunger: online ? online.state.hunger : row.hunger,
            thirst: online ? online.state.thirst : row.thirst,
            world: online ? online.state.world : row.world,
            isAdmin: !!row.is_admin,
            online: !!online,
            color: (online ? online.state.color : row.color) || "#f1c40f",
            skin: (online ? online.state.skin : row.skin) || "none",
            skinItemId: online ? online.state.skinItemId : (row.skin_item_id || null),
            playtimeSeconds: online ? online.state.playtimeSeconds : (row.playtime_seconds || 0)
        }
    });
});

app.post("/api/me/password", requireAuth, async (req, res) => {
    const { currentPassword, newPassword } = req.body;

    if (!currentPassword || !newPassword || newPassword.length < 4) {
        return res.json({ ok: false, error: "Neues Passwort muss mind. 4 Zeichen haben" });
    }

    const [rows] = await db.query("SELECT password_hash FROM players WHERE id=?", [req.player.id]);
    if (rows.length === 0) return res.json({ ok: false, error: "Spieler nicht gefunden" });

    const valid = await bcrypt.compare(currentPassword, rows[0].password_hash);
    if (!valid) return res.json({ ok: false, error: "Aktuelles Passwort ist falsch" });

    const newHash = await bcrypt.hash(newPassword, 10);
    await db.query("UPDATE players SET password_hash=? WHERE id=?", [newHash, req.player.id]);

    res.json({ ok: true });
});

app.post("/api/me/color", requireAuth, async (req, res) => {
    const { color } = req.body;
    if (!color || !/^#[0-9a-fA-F]{6}$/.test(color)) {
        return res.json({ ok: false, error: "Ungültige Farbe (Format: #RRGGBB)" });
    }

    await db.query("UPDATE players SET color=? WHERE id=?", [color, req.player.id]);

    const p = playersOnline.get(req.player.id);
    if (p) {
        p.state.color = color;
        sendStateToAll();
    }

    res.json({ ok: true });
});

app.post("/api/me/skin", requireAuth, async (req, res) => {
    const { skin } = req.body;
    if (!ALLOWED_SKINS.includes(skin)) {
        return res.json({ ok: false, error: "Ungültiger Skin" });
    }

    await db.query("UPDATE players SET skin=? WHERE id=?", [skin, req.player.id]);

    const p = playersOnline.get(req.player.id);
    if (p) {
        p.state.skin = skin;
        sendStateToAll();
    }

    res.json({ ok: true });
});

app.get("/api/skins", (req, res) => {
    res.json({ ok: true, skins: ALLOWED_SKINS });
});

app.post("/api/me/skin_item", requireAuth, async (req, res) => {
    const skinId = req.body.skinId ? Number(req.body.skinId) : null;
    if (skinId && !skinItems.has(skinId)) {
        return res.json({ ok: false, error: "Ungültiger Skin" });
    }

    await db.query("UPDATE players SET skin_item_id=? WHERE id=?", [skinId, req.player.id]);

    const p = playersOnline.get(req.player.id);
    if (p) {
        p.state.skinItemId = skinId;
        sendStateToAll();
    }

    res.json({ ok: true });
});

app.get("/api/me/titles", requireAuth, async (req, res) => {
    const [rows] = await db.query(`
        SELECT a.id, a.name, a.title_text
        FROM player_achievements pa
        JOIN achievements a ON a.id = pa.achievement_id
        WHERE pa.player_id=? AND a.title_text IS NOT NULL AND a.title_text != ''
    `, [req.player.id]);

    const online = playersOnline.get(req.player.id);
    let activeId;
    if (online) {
        activeId = online.state.activeTitleAchievementId;
    } else {
        const [playerRows] = await db.query("SELECT active_title_achievement_id FROM players WHERE id=?", [req.player.id]);
        activeId = playerRows.length ? playerRows[0].active_title_achievement_id : null;
    }

    res.json({ ok: true, titles: rows, active: activeId || null });
});

app.post("/api/me/title", requireAuth, async (req, res) => {
    const achievementId = req.body.achievementId ? Number(req.body.achievementId) : null;

    if (achievementId) {
        const [unlocked] = await db.query(
            "SELECT 1 FROM player_achievements WHERE player_id=? AND achievement_id=?",
            [req.player.id, achievementId]
        );
        if (unlocked.length === 0 || !achievementTitles.has(achievementId)) {
            return res.json({ ok: false, error: "Diesen Titel hast du nicht freigeschaltet." });
        }
    }

    await db.query("UPDATE players SET active_title_achievement_id=? WHERE id=?", [achievementId, req.player.id]);

    const p = playersOnline.get(req.player.id);
    if (p) {
        p.state.activeTitleAchievementId = achievementId;
        sendStateToAll();
    }

    res.json({ ok: true });
});

app.get("/api/items", async (req, res) => {
    const [rows] = await db.query("SELECT id, name, type, damage, weapon_range, weight FROM items");
    res.json({ ok: true, items: rows });
});

app.get("/api/radio_stations", async (req, res) => {
    const [rows] = await db.query("SELECT id, name, url FROM radio_stations ORDER BY name");
    res.json({ ok: true, stations: rows });
});

// -------------------------------------------------------------
// AUKTIONSHAUS (funktioniert unabhängig davon, ob Käufer/Verkäufer online sind)
// -------------------------------------------------------------
app.get("/api/auctions", requireAuth, async (req, res) => {
    const [rows] = await db.query(`
        SELECT a.*, s.username AS seller_name, b.username AS bidder_name
        FROM auctions a
        JOIN players s ON s.id = a.seller_id
        LEFT JOIN players b ON b.id = a.current_bidder_id
        WHERE a.status='active'
        ORDER BY a.ends_at ASC
    `);
    res.json({ ok: true, auctions: rows });
});

app.get("/api/auctions/mine", requireAuth, async (req, res) => {
    const [rows] = await db.query(
        "SELECT * FROM auctions WHERE seller_id=? ORDER BY created_at DESC LIMIT 50",
        [req.player.id]
    );
    res.json({ ok: true, auctions: rows });
});

app.get("/api/me/inventory", requireAuth, async (req, res) => {
    const online = playersOnline.get(req.player.id);
    if (online) {
        return res.json({ ok: true, inventory: online.state.inventory });
    }
    const [rows] = await db.query("SELECT inventory FROM players WHERE id=?", [req.player.id]);
    res.json({ ok: true, inventory: rows.length ? JSON.parse(rows[0].inventory || "[]") : [] });
});

app.post("/api/auctions", requireAuth, async (req, res) => {
    const { itemId, amount, startingPrice, buyoutPrice, hours } = req.body;
    const amt = Math.max(1, Math.floor(Number(amount) || 1));
    const startPrice = Math.max(1, Math.floor(Number(startingPrice) || 1));
    const buyout = buyoutPrice ? Math.max(startPrice, Math.floor(Number(buyoutPrice))) : null;
    const dur = Math.min(72, Math.max(1, Math.floor(Number(hours) || 24)));

    if (!itemId) return res.json({ ok: false, error: "Item erforderlich" });

    const removed = await removeItemFromInventory(req.player.id, itemId, amt);
    if (!removed) return res.json({ ok: false, error: "Item nicht (in ausreichender Menge) im Inventar vorhanden" });

    const endsAt = new Date(Date.now() + dur * 3600 * 1000);
    const [result] = await db.query(
        "INSERT INTO auctions (seller_id, item_id, amount, starting_price, buyout_price, ends_at) VALUES (?, ?, ?, ?, ?, ?)",
        [req.player.id, itemId, amt, startPrice, buyout, endsAt]
    );

    res.json({ ok: true, id: result.insertId });
});

app.post("/api/auctions/:id/bid", requireAuth, async (req, res) => {
    const amount = Math.max(1, Math.floor(Number(req.body.amount) || 0));

    const [rows] = await db.query("SELECT * FROM auctions WHERE id=? AND status='active'", [req.params.id]);
    if (rows.length === 0) return res.json({ ok: false, error: "Angebot nicht gefunden oder beendet" });
    const auction = rows[0];

    if (auction.seller_id === req.player.id) {
        return res.json({ ok: false, error: "Du kannst nicht auf dein eigenes Angebot bieten" });
    }
    if (new Date(auction.ends_at) <= new Date()) {
        return res.json({ ok: false, error: "Angebot bereits abgelaufen" });
    }

    const minBid = auction.current_bid ? auction.current_bid + 1 : auction.starting_price;
    if (amount < minBid) {
        return res.json({ ok: false, error: `Gebot muss mindestens ${minBid}$ sein` });
    }

    const money = await getPlayerMoney(req.player.id);
    if (money < amount) return res.json({ ok: false, error: "Nicht genug Bargeld" });

    await adjustMoney(req.player.id, -amount);
    if (auction.current_bidder_id) {
        await adjustMoney(auction.current_bidder_id, auction.current_bid); // vorheriges Gebot zurückerstatten
    }

    await db.query("UPDATE auctions SET current_bid=?, current_bidder_id=? WHERE id=?",
        [amount, req.player.id, auction.id]);

    res.json({ ok: true });
});

app.post("/api/auctions/:id/buyout", requireAuth, async (req, res) => {
    const [rows] = await db.query("SELECT * FROM auctions WHERE id=? AND status='active'", [req.params.id]);
    if (rows.length === 0) return res.json({ ok: false, error: "Angebot nicht gefunden" });
    const auction = rows[0];

    if (!auction.buyout_price) return res.json({ ok: false, error: "Kein Sofortkauf-Preis vorhanden" });
    if (auction.seller_id === req.player.id) return res.json({ ok: false, error: "Du kannst nicht dein eigenes Angebot kaufen" });

    const money = await getPlayerMoney(req.player.id);
    if (money < auction.buyout_price) return res.json({ ok: false, error: "Nicht genug Bargeld" });

    await adjustMoney(req.player.id, -auction.buyout_price);
    if (auction.current_bidder_id) {
        await adjustMoney(auction.current_bidder_id, auction.current_bid); // vorheriges Gebot zurückerstatten
    }
    await adjustMoney(auction.seller_id, auction.buyout_price);
    await addItemToInventory(req.player.id, auction.item_id, auction.amount, true);

    await db.query("UPDATE auctions SET status='sold', current_bid=?, current_bidder_id=? WHERE id=?",
        [auction.buyout_price, req.player.id, auction.id]);

    res.json({ ok: true });
});

app.post("/api/auctions/:id/cancel", requireAuth, async (req, res) => {
    const [rows] = await db.query("SELECT * FROM auctions WHERE id=? AND status='active'", [req.params.id]);
    if (rows.length === 0) return res.json({ ok: false, error: "Nicht gefunden" });
    const auction = rows[0];

    if (auction.seller_id !== req.player.id) return res.json({ ok: false, error: "Nur der Verkäufer kann stornieren" });
    if (auction.current_bidder_id) return res.json({ ok: false, error: "Kann nicht storniert werden - es liegt bereits ein Gebot vor" });

    await addItemToInventory(req.player.id, auction.item_id, auction.amount, true);
    await db.query("UPDATE auctions SET status='cancelled' WHERE id=?", [auction.id]);

    res.json({ ok: true });
});

// -------------------------------------------------------------
// IMMOBILIENMARKT (Häuser zwischen Spielern - selbes Muster wie das Auktionshaus)
// -------------------------------------------------------------
app.get("/api/house_listings", requireAuth, async (req, res) => {
    const [rows] = await db.query(`
        SELECT hl.*, h.name AS house_name, h.world, h.x, h.y,
               s.username AS seller_name, b.username AS bidder_name
        FROM house_listings hl
        JOIN houses h ON h.id = hl.house_id
        JOIN players s ON s.id = hl.seller_id
        LEFT JOIN players b ON b.id = hl.current_bidder_id
        WHERE hl.status='active'
        ORDER BY hl.ends_at ASC
    `);
    res.json({ ok: true, listings: rows });
});

app.get("/api/house_listings/mine", requireAuth, async (req, res) => {
    const [rows] = await db.query(
        "SELECT * FROM house_listings WHERE seller_id=? ORDER BY created_at DESC LIMIT 50",
        [req.player.id]
    );
    res.json({ ok: true, listings: rows });
});

app.get("/api/me/houses", requireAuth, async (req, res) => {
    const [rows] = await db.query(
        "SELECT id, name, world, x, y FROM houses WHERE owner_id=?",
        [req.player.id]
    );
    res.json({ ok: true, houses: rows });
});

app.post("/api/house_listings", requireAuth, async (req, res) => {
    const { houseId, startingPrice, buyoutPrice, hours } = req.body;
    const startPrice = Math.max(1, Math.floor(Number(startingPrice) || 1));
    const buyout = buyoutPrice ? Math.max(startPrice, Math.floor(Number(buyoutPrice))) : null;
    const dur = Math.min(72, Math.max(1, Math.floor(Number(hours) || 24)));

    const [houseRows] = await db.query("SELECT * FROM houses WHERE id=?", [houseId]);
    if (houseRows.length === 0) return res.json({ ok: false, error: "Haus nicht gefunden" });
    const house = houseRows[0];

    if (house.owner_id !== req.player.id) {
        return res.json({ ok: false, error: "Das ist nicht dein Haus" });
    }

    const [existing] = await db.query(
        "SELECT 1 FROM house_listings WHERE house_id=? AND status='active'",
        [houseId]
    );
    if (existing.length > 0) {
        return res.json({ ok: false, error: "Dieses Haus ist schon im Angebot" });
    }

    const endsAt = new Date(Date.now() + dur * 3600 * 1000);
    const [result] = await db.query(
        "INSERT INTO house_listings (house_id, seller_id, starting_price, buyout_price, ends_at) VALUES (?, ?, ?, ?, ?)",
        [houseId, req.player.id, startPrice, buyout, endsAt]
    );

    res.json({ ok: true, id: result.insertId });
});

app.post("/api/house_listings/:id/bid", requireAuth, async (req, res) => {
    const amount = Math.max(1, Math.floor(Number(req.body.amount) || 0));

    const [rows] = await db.query("SELECT * FROM house_listings WHERE id=? AND status='active'", [req.params.id]);
    if (rows.length === 0) return res.json({ ok: false, error: "Angebot nicht gefunden oder beendet" });
    const listing = rows[0];

    if (listing.seller_id === req.player.id) {
        return res.json({ ok: false, error: "Du kannst nicht auf dein eigenes Haus bieten" });
    }
    if (new Date(listing.ends_at) <= new Date()) {
        return res.json({ ok: false, error: "Angebot bereits abgelaufen" });
    }

    const minBid = listing.current_bid ? listing.current_bid + 1 : listing.starting_price;
    if (amount < minBid) {
        return res.json({ ok: false, error: `Gebot muss mindestens ${minBid}$ sein` });
    }

    const money = await getPlayerMoney(req.player.id);
    if (money < amount) return res.json({ ok: false, error: "Nicht genug Bargeld" });

    await adjustMoney(req.player.id, -amount);
    if (listing.current_bidder_id) {
        await adjustMoney(listing.current_bidder_id, listing.current_bid); // vorheriges Gebot zurückerstatten
    }

    await db.query("UPDATE house_listings SET current_bid=?, current_bidder_id=? WHERE id=?",
        [amount, req.player.id, listing.id]);

    res.json({ ok: true });
});

app.post("/api/house_listings/:id/buyout", requireAuth, async (req, res) => {
    const [rows] = await db.query("SELECT * FROM house_listings WHERE id=? AND status='active'", [req.params.id]);
    if (rows.length === 0) return res.json({ ok: false, error: "Angebot nicht gefunden" });
    const listing = rows[0];

    if (!listing.buyout_price) return res.json({ ok: false, error: "Kein Sofortkauf-Preis vorhanden" });
    if (listing.seller_id === req.player.id) return res.json({ ok: false, error: "Du kannst nicht dein eigenes Haus kaufen" });

    const money = await getPlayerMoney(req.player.id);
    if (money < listing.buyout_price) return res.json({ ok: false, error: "Nicht genug Bargeld" });

    await adjustMoney(req.player.id, -listing.buyout_price);
    if (listing.current_bidder_id) {
        await adjustMoney(listing.current_bidder_id, listing.current_bid);
    }
    await adjustMoney(listing.seller_id, listing.buyout_price);

    // Besitzwechsel: neuer Eigentümer, alte Schlüssel werden zurückgesetzt
    await db.query("UPDATE houses SET owner_id=? WHERE id=?", [req.player.id, listing.house_id]);
    await db.query("DELETE FROM house_keys WHERE house_id=?", [listing.house_id]);
    await loadHouses();

    await db.query("UPDATE house_listings SET status='sold', current_bid=?, current_bidder_id=? WHERE id=?",
        [listing.buyout_price, req.player.id, listing.id]);

    res.json({ ok: true });
});

app.post("/api/house_listings/:id/cancel", requireAuth, async (req, res) => {
    const [rows] = await db.query("SELECT * FROM house_listings WHERE id=? AND status='active'", [req.params.id]);
    if (rows.length === 0) return res.json({ ok: false, error: "Nicht gefunden" });
    const listing = rows[0];

    if (listing.seller_id !== req.player.id) return res.json({ ok: false, error: "Nur der Verkäufer kann stornieren" });
    if (listing.current_bidder_id) return res.json({ ok: false, error: "Kann nicht storniert werden - es liegt bereits ein Gebot vor" });

    await db.query("UPDATE house_listings SET status='cancelled' WHERE id=?", [listing.id]);
    res.json({ ok: true });
});

app.get("/api/achievements/:username", async (req, res) => {
    const [playerRows] = await db.query("SELECT id FROM players WHERE username=? OR id=?", [req.params.username, req.params.username]);
    if (playerRows.length === 0) return res.json({ ok: false, error: "Spieler nicht gefunden" });

    const [rows] = await db.query(
        `SELECT a.key_name, a.name, a.description, a.xp_reward, pa.unlocked_at
         FROM player_achievements pa JOIN achievements a ON a.id = pa.achievement_id
         WHERE pa.player_id=? ORDER BY pa.unlocked_at DESC`,
        [playerRows[0].id]
    );
    const [allAch] = await db.query("SELECT key_name, name, description, xp_reward FROM achievements");

    res.json({ ok: true, unlocked: rows, all: allAch });
});

app.get("/api/leaderboard", async (req, res) => {
    const [richest] = await db.query(
        "SELECT username, (money + bank) AS total FROM players ORDER BY total DESC LIMIT 10"
    );
    const [deliveries] = await db.query(
        "SELECT username, delivery_count FROM players ORDER BY delivery_count DESC LIMIT 10"
    );
    const [levels] = await db.query(
        "SELECT username, level, xp FROM players ORDER BY level DESC, xp DESC LIMIT 10"
    );
    const [robberies] = await db.query(
        "SELECT username, robbery_count FROM players ORDER BY robbery_count DESC LIMIT 10"
    );
    const [playtime] = await db.query(
        "SELECT username, playtime_seconds FROM players ORDER BY playtime_seconds DESC LIMIT 10"
    );

    res.json({ ok: true, richest, deliveries, levels, robberies, playtime });
});


// -------------------------------------------------------------
// WÜNSCHE (jeder eingeloggte Spieler kann lesen/schreiben/abstimmen)
// -------------------------------------------------------------
app.get("/api/wishes", requireAuth, async (req, res) => {
    const [rows] = await db.query(
        "SELECT id, player_id, username, title, content, status, votes, created_at FROM wishes ORDER BY votes DESC, created_at DESC"
    );
    const [myVotes] = await db.query(
        "SELECT wish_id FROM wish_votes WHERE player_id=?",
        [req.player.id]
    );
    const votedIds = new Set(myVotes.map(v => v.wish_id));

    res.json({
        ok: true,
        wishes: rows.map(w => ({ ...w, hasVoted: votedIds.has(w.id) }))
    });
});

app.post("/api/wishes", requireAuth, async (req, res) => {
    const { title, content } = req.body;
    if (!title || !content) {
        return res.json({ ok: false, error: "Titel und Inhalt erforderlich" });
    }
    await db.query(
        "INSERT INTO wishes (player_id, username, title, content) VALUES (?, ?, ?, ?)",
        [req.player.id, req.player.username, title, content]
    );
    sendDiscordWebhook("wishes", {
        title: "💡 Neuer Wunsch/Vorschlag",
        description: `**${String(title).slice(0, 200)}**\n${String(content).slice(0, 300)}`,
        color: 0xf0c419,
        fields: [{ name: "Von", value: req.player.username, inline: true }],
        timestamp: new Date().toISOString()
    });
    res.json({ ok: true });
});

app.post("/api/wishes/:id/vote", requireAuth, async (req, res) => {
    const wishId = Number(req.params.id);

    const [existing] = await db.query(
        "SELECT 1 FROM wish_votes WHERE wish_id=? AND player_id=?",
        [wishId, req.player.id]
    );

    if (existing.length > 0) {
        await db.query("DELETE FROM wish_votes WHERE wish_id=? AND player_id=?", [wishId, req.player.id]);
        await db.query("UPDATE wishes SET votes = GREATEST(0, votes - 1) WHERE id=?", [wishId]);
        res.json({ ok: true, voted: false });
    } else {
        await db.query("INSERT INTO wish_votes (wish_id, player_id) VALUES (?, ?)", [wishId, req.player.id]);
        await db.query("UPDATE wishes SET votes = votes + 1 WHERE id=?", [wishId]);
        res.json({ ok: true, voted: true });
    }
});

// -------------------------------------------------------------
// UMFRAGEN: existierte bisher NUR im Client (umfragen.html rief
// /api/polls bzw. /api/admin/polls auf) - die Endpunkte selbst
// fehlten im Server komplett, Umfragen waren also nie funktionsfähig
// -------------------------------------------------------------
app.get("/api/polls", requireAuth, async (req, res) => {
    const [pollRows] = await db.query("SELECT * FROM polls ORDER BY id DESC");
    const [optionRows] = await db.query("SELECT * FROM poll_options ORDER BY id ASC");
    const [voteRows] = await db.query("SELECT * FROM poll_votes WHERE player_id=?", [req.player.id]);

    const polls = pollRows.map(p => ({
        id: p.id,
        question: p.question,
        createdBy: p.created_by,
        createdAt: p.created_at,
        closed: !!p.closed,
        myVote: voteRows.find(v => v.poll_id === p.id)?.option_id || null,
        options: optionRows.filter(o => o.poll_id === p.id).map(o => ({
            id: o.id, text: o.option_text, votes: o.votes
        }))
    }));

    res.json({ ok: true, polls });
});

app.post("/api/admin/polls", requirePermission("manage_content"), async (req, res) => {
    const { question, options } = req.body;
    if (!question || !Array.isArray(options) || options.length < 2) {
        return res.json({ ok: false, error: "Frage und mindestens 2 Optionen erforderlich" });
    }

    const [result] = await db.query(
        "INSERT INTO polls (question, created_by) VALUES (?, ?)",
        [String(question).slice(0, 300), req.player.username]
    );
    for (const opt of options) {
        await db.query(
            "INSERT INTO poll_options (poll_id, option_text) VALUES (?, ?)",
            [result.insertId, String(opt).slice(0, 150)]
        );
    }

    sendDiscordWebhook("surveys", {
        title: "📊 Neue Umfrage",
        description: `**${String(question).slice(0, 300)}**\n${options.map(o => `• ${o}`).join("\n")}`,
        color: 0x9b59b6,
        fields: [{ name: "Erstellt von", value: req.player.username, inline: true }],
        timestamp: new Date().toISOString()
    });

    res.json({ ok: true, id: result.insertId });
});

app.post("/api/polls/:id/vote", requireAuth, async (req, res) => {
    const pollId = Number(req.params.id);
    const optionId = Number(req.body.optionId);

    const [pollRows] = await db.query("SELECT * FROM polls WHERE id=?", [pollId]);
    if (pollRows.length === 0) return res.json({ ok: false, error: "Umfrage nicht gefunden" });
    if (pollRows[0].closed) return res.json({ ok: false, error: "Diese Umfrage ist bereits geschlossen." });

    const [optRows] = await db.query("SELECT * FROM poll_options WHERE id=? AND poll_id=?", [optionId, pollId]);
    if (optRows.length === 0) return res.json({ ok: false, error: "Ungültige Option" });

    const [existing] = await db.query(
        "SELECT * FROM poll_votes WHERE poll_id=? AND player_id=?",
        [pollId, req.player.id]
    );

    if (existing.length > 0) {
        if (existing[0].option_id === optionId) {
            return res.json({ ok: true }); // schon genau so abgestimmt, nichts zu tun
        }
        await db.query("UPDATE poll_options SET votes = GREATEST(0, votes - 1) WHERE id=?", [existing[0].option_id]);
        await db.query("UPDATE poll_votes SET option_id=? WHERE poll_id=? AND player_id=?", [optionId, pollId, req.player.id]);
    } else {
        await db.query("INSERT INTO poll_votes (poll_id, option_id, player_id) VALUES (?, ?, ?)", [pollId, optionId, req.player.id]);
    }
    await db.query("UPDATE poll_options SET votes = votes + 1 WHERE id=?", [optionId]);

    res.json({ ok: true });
});

app.post("/api/admin/polls/:id/close", requirePermission("manage_content"), async (req, res) => {
    await db.query("UPDATE polls SET closed=1 WHERE id=?", [req.params.id]);
    res.json({ ok: true });
});

app.delete("/api/admin/polls/:id", requirePermission("manage_content"), async (req, res) => {
    await logDeletionForUndo("polls", "id", req.params.id);
    await db.query("DELETE FROM poll_votes WHERE poll_id=?", [req.params.id]);
    await db.query("DELETE FROM poll_options WHERE poll_id=?", [req.params.id]);
    await db.query("DELETE FROM polls WHERE id=?", [req.params.id]);
    res.json({ ok: true });
});

// -------------------------------------------------------------
// ANMERKUNGEN (jeder eingeloggte Spieler kann schreiben, nur Admins lesen)
// -------------------------------------------------------------
app.post("/api/remarks", requireAuth, async (req, res) => {
    const { content } = req.body;
    if (!content) {
        return res.json({ ok: false, error: "Inhalt erforderlich" });
    }
    await db.query(
        "INSERT INTO remarks (player_id, username, content) VALUES (?, ?, ?)",
        [req.player.id, req.player.username, content]
    );
    res.json({ ok: true });
});

// -------------------------------------------------------------
// UMFRAGEN (jeder eingeloggte Spieler kann lesen/abstimmen)
// -------------------------------------------------------------
app.get("/api/polls", requireAuth, async (req, res) => {
    const [polls] = await db.query("SELECT * FROM polls ORDER BY created_at DESC LIMIT 50");
    const [options] = await db.query("SELECT * FROM poll_options");
    const [voteCounts] = await db.query(
        "SELECT poll_id, option_id, COUNT(*) AS votes FROM poll_votes GROUP BY poll_id, option_id"
    );
    const [myVotes] = await db.query(
        "SELECT poll_id, option_id FROM poll_votes WHERE player_id=?", [req.player.id]
    );
    const myVoteMap = new Map(myVotes.map(v => [v.poll_id, v.option_id]));

    const result = polls.map(poll => {
        const pollOptions = options
            .filter(o => o.poll_id === poll.id)
            .map(o => ({
                id: o.id,
                text: o.text,
                votes: voteCounts.find(v => v.poll_id === poll.id && v.option_id === o.id)?.votes || 0
            }));
        return {
            id: poll.id,
            question: poll.question,
            createdBy: poll.created_by,
            createdAt: poll.created_at,
            closed: !!poll.closed,
            options: pollOptions,
            myVote: myVoteMap.get(poll.id) || null
        };
    });

    res.json({ ok: true, polls: result });
});

app.post("/api/polls/:id/vote", requireAuth, async (req, res) => {
    const pollId = Number(req.params.id);
    const optionId = Number(req.body.optionId);

    const [pollRows] = await db.query("SELECT * FROM polls WHERE id=?", [pollId]);
    if (pollRows.length === 0) return res.json({ ok: false, error: "Umfrage nicht gefunden" });
    if (pollRows[0].closed) return res.json({ ok: false, error: "Umfrage ist geschlossen" });

    const [optRows] = await db.query("SELECT 1 FROM poll_options WHERE id=? AND poll_id=?", [optionId, pollId]);
    if (optRows.length === 0) return res.json({ ok: false, error: "Ungültige Option" });

    await db.query(
        `INSERT INTO poll_votes (poll_id, player_id, option_id) VALUES (?, ?, ?)
         ON DUPLICATE KEY UPDATE option_id=?`,
        [pollId, req.player.id, optionId, optionId]
    );

    res.json({ ok: true });
});

app.post("/api/admin/polls", async (req, res) => {
    const { question, options } = req.body;
    if (!question || !Array.isArray(options) || options.length < 2) {
        return res.json({ ok: false, error: "Frage und mindestens 2 Antwortoptionen erforderlich" });
    }

    const [result] = await db.query(
        "INSERT INTO polls (question, created_by) VALUES (?, ?)",
        [question, req.player.username]
    );
    const pollId = result.insertId;

    for (const opt of options) {
        const text = String(opt).trim().slice(0, 100);
        if (text) await db.query("INSERT INTO poll_options (poll_id, text) VALUES (?, ?)", [pollId, text]);
    }

    res.json({ ok: true, id: pollId });
});

app.post("/api/admin/polls/:id/close", async (req, res) => {
    await db.query("UPDATE polls SET closed=1 WHERE id=?", [req.params.id]);
    res.json({ ok: true });
});

app.delete("/api/admin/polls/:id", async (req, res) => {
    await db.query("DELETE FROM poll_votes WHERE poll_id=?", [req.params.id]);
    await db.query("DELETE FROM poll_options WHERE poll_id=?", [req.params.id]);
    await logDeletionForUndo("polls", "id", req.params.id);
    await db.query("DELETE FROM polls WHERE id=?", [req.params.id]);
    res.json({ ok: true });
});

// -------------------------------------------------------------
// ADMIN API: TILE-CONFIG
// -------------------------------------------------------------
app.get("/api/admin/backups", (req, res) => {
    const files = fs.readdirSync(BACKUP_DIR)
        .filter(f => f.endsWith(".sql"))
        .map(f => {
            const stat = fs.statSync(path.join(BACKUP_DIR, f));
            return { name: f, sizeKb: Math.round(stat.size / 1024), created: stat.mtime };
        })
        .sort((a, b) => new Date(b.created) - new Date(a.created));
    res.json({ ok: true, backups: files });
});

app.post("/api/admin/backups/run", (req, res) => {
    runBackup();
    res.json({ ok: true, msg: "Backup gestartet (läuft im Hintergrund)." });
});

app.get("/api/admin/events/status", (req, res) => {
    isEventActive("doubleXp");
    isEventActive("discount"); // triggert Ablauf-Check
    res.json({ ok: true, events: activeEvents });
});

app.post("/api/admin/events/start", async (req, res) => {
    const { type, durationMinutes, percent } = req.body;
    const dur = Math.max(1, Math.min(10080, Math.floor(Number(durationMinutes) || 60))); // max 1 Woche
    const endsAt = Date.now() + dur * 60 * 1000;

    if (type === "doubleXp") {
        activeEvents.doubleXp.active = true;
        activeEvents.doubleXp.endsAt = endsAt;
        broadcastToAll(`🎉 DOPPEL-XP AKTIVIERT für ${dur} Minuten! Alle XP-Belohnungen sind verdoppelt.`, true);
    } else if (type === "discount") {
        const pct = Math.max(1, Math.min(90, Math.floor(Number(percent) || 10)));
        activeEvents.discount.active = true;
        activeEvents.discount.endsAt = endsAt;
        activeEvents.discount.percent = pct;
        broadcastToAll(`💰 RABATT-AKTION AKTIVIERT: ${pct}% auf Shops & Tankstellen für ${dur} Minuten!`, true);
    } else {
        return res.json({ ok: false, error: "Unbekannter Event-Typ" });
    }

    await logEvent("server_event", "Admin", `Event "${type}" gestartet (${dur} Min${type === "discount" ? `, ${percent}%` : ""})`);
    broadcastEventUpdate();
    res.json({ ok: true, events: activeEvents });
});

app.post("/api/admin/events/stop", async (req, res) => {
    const { type } = req.body;
    if (!activeEvents[type]) return res.json({ ok: false, error: "Unbekannter Event-Typ" });

    activeEvents[type].active = false;
    broadcastToAll(`${activeEvents[type].label} wurde vorzeitig beendet.`, true);
    await logEvent("server_event", "Admin", `Event "${type}" vorzeitig beendet`);
    broadcastEventUpdate();

    res.json({ ok: true, events: activeEvents });
});

app.get("/api/admin/settings", async (req, res) => {
    const [rows] = await db.query("SELECT * FROM game_settings ORDER BY setting_key");
    res.json({ ok: true, settings: rows, defaults: SETTINGS_DEFAULTS });
});

app.post("/api/admin/settings", async (req, res) => {
    const { key, value } = req.body;
    if (!key || value === undefined) {
        return res.json({ ok: false, error: "Schlüssel und Wert erforderlich" });
    }
    await db.query(
        "INSERT INTO game_settings (setting_key, setting_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE setting_value=?",
        [key, value, value]
    );
    await loadGameSettings();

    if (key === "npc_traffic_enabled") syncNpcTrafficWithSetting();

    res.json({ ok: true });
});

app.get("/api/admin/tile_config", async (req, res) => {
    const [rows] = await db.query("SELECT * FROM tile_config ORDER BY id");
    res.json({ ok: true, tiles: rows });
});

app.post("/api/admin/tile_config", async (req, res) => {
    const { id, name, color, collision, pvpSafe, isRoad, imageData } = req.body;
    if (id === undefined || !name || !color) {
        return res.json({ ok: false, error: "ID, Name und Farbe erforderlich" });
    }
    // grobe Größenbremse gegen Missbrauch (Texturen sollen klein/pixelig bleiben)
    if (imageData && imageData.length > 200000) {
        return res.json({ ok: false, error: "Textur zu groß" });
    }

    // Falls imageData gar nicht mitgeschickt wurde (z.B. normales Speichern-Formular,
    // ohne Textur-Bereich anzufassen), bestehende Textur NICHT löschen
    let finalImageData = imageData;
    if (finalImageData === undefined) {
        const [existing] = await db.query("SELECT image_data FROM tile_config WHERE id=?", [id]);
        finalImageData = existing.length ? existing[0].image_data : null;
    }

    await db.query(
        `INSERT INTO tile_config (id, name, color, collision, pvp_safe, is_road, image_data) VALUES (?, ?, ?, ?, ?, ?, ?)
         ON DUPLICATE KEY UPDATE name=?, color=?, collision=?, pvp_safe=?, is_road=?, image_data=?`,
        [id, name, color, collision ? 1 : 0, pvpSafe ? 1 : 0, isRoad ? 1 : 0, finalImageData || null,
         name, color, collision ? 1 : 0, pvpSafe ? 1 : 0, isRoad ? 1 : 0, finalImageData || null]
    );
    await loadTileConfig();
    res.json({ ok: true });
});

app.delete("/api/admin/tile_config/:id", async (req, res) => {
    await logDeletionForUndo("tile_config", "id", req.params.id);
    await db.query("DELETE FROM tile_config WHERE id=?", [req.params.id]);
    await loadTileConfig();
    res.json({ ok: true });
});

// -------------------------------------------------------------
// ADMIN API: OBJECT-CONFIG
// -------------------------------------------------------------
app.get("/api/admin/object_config", async (req, res) => {
    const [rows] = await db.query("SELECT * FROM object_config ORDER BY type");
    res.json({ ok: true, objects: rows });
});

app.post("/api/admin/object_config", async (req, res) => {
    const { type, name, color, width, height, collision, interactive, action, glowsAtNight, imageData } = req.body;
    if (!type || !name || !color) {
        return res.json({ ok: false, error: "Typ, Name und Farbe erforderlich" });
    }
    if (imageData && imageData.length > 200000) {
        return res.json({ ok: false, error: "Textur zu groß" });
    }

    // Falls imageData gar nicht mitgeschickt wurde, bestehende Textur NICHT löschen
    let finalImageData = imageData;
    if (finalImageData === undefined) {
        const [existing] = await db.query("SELECT image_data FROM object_config WHERE type=?", [type]);
        finalImageData = existing.length ? existing[0].image_data : null;
    }

    await db.query(
        `INSERT INTO object_config (type, name, color, width, height, collision, interactive, action, glows_at_night, image_data)
         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
         ON DUPLICATE KEY UPDATE name=?, color=?, width=?, height=?, collision=?, interactive=?, action=?, glows_at_night=?, image_data=?`,
        [type, name, color, width || 32, height || 32, collision ? 1 : 0, interactive ? 1 : 0, action || null, glowsAtNight ? 1 : 0, finalImageData || null,
         name, color, width || 32, height || 32, collision ? 1 : 0, interactive ? 1 : 0, action || null, glowsAtNight ? 1 : 0, finalImageData || null]
    );
    await loadObjectConfig();
    res.json({ ok: true });
});

app.delete("/api/admin/object_config/:type", async (req, res) => {
    await logDeletionForUndo("object_config", "type", req.params.type);
    await db.query("DELETE FROM object_config WHERE type=?", [req.params.type]);
    await loadObjectConfig();
    res.json({ ok: true });
});

// -------------------------------------------------------------
// ADMIN API: ITEMS
// -------------------------------------------------------------
app.get("/api/admin/items", async (req, res) => {
    const [rows] = await db.query("SELECT * FROM items");
    res.json({ ok: true, items: rows });
});

app.post("/api/admin/items", async (req, res) => {
    const { id, name, type, restore, model, damage, weaponRange, weight } = req.body;

    if (!id || !name) {
        return res.json({ ok: false, error: "ID und Name sind Pflicht" });
    }

    try {
        await db.query(
            `INSERT INTO items (id, name, type, restore, model, damage, weapon_range, weight)
             VALUES (?, ?, ?, ?, ?, ?, ?, ?)
             ON DUPLICATE KEY UPDATE name=?, type=?, restore=?, model=?, damage=?, weapon_range=?, weight=?`,
            [id, name, type || "item", restore || 0, model || null, damage || 0, weaponRange || 0, weight || 1,
             name, type || "item", restore || 0, model || null, damage || 0, weaponRange || 0, weight || 1]
        );
        await loadShops(); // Namen/Typen können sich auf bestehende Shop-Items auswirken
        await loadItemWeights();
        res.json({ ok: true });
    } catch (err) {
        console.error("Fehler beim Speichern von Item:", err);
        res.json({ ok: false, error: err.sqlMessage || err.message || "Speichern fehlgeschlagen" });
    }
});

app.delete("/api/admin/items/:id", async (req, res) => {
    try {
        await logDeletionForUndo("items", "id", req.params.id);
    await db.query("DELETE FROM items WHERE id=?", [req.params.id]);
        await loadShops();
        res.json({ ok: true });
    } catch {
        res.json({ ok: false, error: "Löschen fehlgeschlagen (evtl. noch in Shop verknüpft)" });
    }
});

// -------------------------------------------------------------
// ADMIN API: SHOPS
// -------------------------------------------------------------
app.get("/api/admin/shops", async (req, res) => {
    const [shopRows] = await db.query("SELECT * FROM shops");
    const [itemRows] = await db.query(`
        SELECT si.id, si.shop_id, si.item_id, si.price, i.name
        FROM shop_items si JOIN items i ON i.id = si.item_id
    `);
    const result = shopRows.map(s => ({
        ...s,
        items: itemRows.filter(i => i.shop_id === s.id)
    }));
    res.json({ ok: true, shops: result });
});

app.post("/api/admin/shops", async (req, res) => {
    const { name, world, x, y } = req.body;

    if (!name || !world || x === undefined || y === undefined) {
        return res.json({ ok: false, error: "Alle Felder erforderlich" });
    }

    const [result] = await db.query(
        "INSERT INTO shops (name, world, x, y) VALUES (?, ?, ?, ?)",
        [name, world, x, y]
    );
    await loadShops();
    broadcastMapDataToWorld(world);
    res.json({ ok: true, id: result.insertId });
});

app.delete("/api/admin/shops/:id", async (req, res) => {
    await logDeletionForUndo("shops", "id", req.params.id);
    await db.query("DELETE FROM shops WHERE id=?", [req.params.id]);
    await loadShops();
    res.json({ ok: true });
});

app.post("/api/admin/shops/:id/items", async (req, res) => {
    const { itemId, price } = req.body;

    if (!itemId || !price || price <= 0) {
        return res.json({ ok: false, error: "Item und gültiger Preis erforderlich" });
    }

    try {
        await db.query(
            "INSERT INTO shop_items (shop_id, item_id, price) VALUES (?, ?, ?)",
            [req.params.id, itemId, price]
        );
        await loadShops();
        res.json({ ok: true });
    } catch {
        res.json({ ok: false, error: "Hinzufügen fehlgeschlagen" });
    }
});

app.delete("/api/admin/shop_items/:id", async (req, res) => {
    await logDeletionForUndo("shop_items", "id", req.params.id);
    await db.query("DELETE FROM shop_items WHERE id=?", [req.params.id]);
    await loadShops();
    res.json({ ok: true });
});

// -------------------------------------------------------------
// ADMIN API: GARAGEN
// -------------------------------------------------------------
app.get("/api/admin/garages", async (req, res) => {
    const [rows] = await db.query("SELECT * FROM garages");
    res.json({ ok: true, garages: rows });
});

app.post("/api/admin/garages", async (req, res) => {
    const { name, world, x, y, jobId } = req.body;

    if (!name || !world || x === undefined || y === undefined) {
        return res.json({ ok: false, error: "Alle Felder erforderlich" });
    }

    const [result] = await db.query(
        "INSERT INTO garages (name, world, x, y, job_id) VALUES (?, ?, ?, ?, ?)",
        [name, world, x, y, jobId || null]
    );
    await loadGarages();
    broadcastMapDataToWorld(world);
    res.json({ ok: true, id: result.insertId });
});

app.delete("/api/admin/garages/:id", async (req, res) => {
    await logDeletionForUndo("garages", "id", req.params.id);
    await db.query("DELETE FROM garages WHERE id=?", [req.params.id]);
    await loadGarages();
    res.json({ ok: true });
});

// -------------------------------------------------------------
// ADMIN API: TANKSTELLEN
// -------------------------------------------------------------
app.get("/api/admin/gas_stations", async (req, res) => {
    const [rows] = await db.query("SELECT * FROM gas_stations");
    res.json({ ok: true, gasStations: rows });
});

app.post("/api/admin/gas_stations", async (req, res) => {
    const { id, name, world, x, y, price } = req.body;

    if (id) {
        // Bestehende Tankstelle: nur Preis aktualisierbar
        await db.query("UPDATE gas_stations SET price=? WHERE id=?", [price || 2, id]);
    } else {
        if (!name || !world || x === undefined || y === undefined) {
            return res.json({ ok: false, error: "Alle Felder erforderlich" });
        }
        await db.query(
            "INSERT INTO gas_stations (name, world, x, y, price) VALUES (?, ?, ?, ?, ?)",
            [name, world, x, y, price || 2]
        );
    }

    await loadGasStations();
    if (world) broadcastMapDataToWorld(world);
    res.json({ ok: true });
});

app.delete("/api/admin/gas_stations/:id", async (req, res) => {
    await logDeletionForUndo("gas_stations", "id", req.params.id);
    await db.query("DELETE FROM gas_stations WHERE id=?", [req.params.id]);
    await loadGasStations();
    res.json({ ok: true });
});

// -------------------------------------------------------------
// ADMIN API: REPARATUR-WERKSTÄTTEN
// -------------------------------------------------------------
app.get("/api/admin/repair_shops", async (req, res) => {
    const [rows] = await db.query("SELECT * FROM repair_shops");
    res.json({ ok: true, repairShops: rows });
});

app.post("/api/admin/repair_shops", async (req, res) => {
    const { id, name, world, x, y, pricePerPoint } = req.body;

    if (id) {
        await db.query("UPDATE repair_shops SET price_per_point=? WHERE id=?", [pricePerPoint || 5, id]);
    } else {
        if (!name || !world || x === undefined || y === undefined) {
            return res.json({ ok: false, error: "Alle Felder erforderlich" });
        }
        await db.query(
            "INSERT INTO repair_shops (name, world, x, y, price_per_point) VALUES (?, ?, ?, ?, ?)",
            [name, world, x, y, pricePerPoint || 5]
        );
    }

    await loadRepairShops();
    if (world) broadcastMapDataToWorld(world);
    res.json({ ok: true });
});

app.delete("/api/admin/repair_shops/:id", async (req, res) => {
    await logDeletionForUndo("repair_shops", "id", req.params.id);
    await db.query("DELETE FROM repair_shops WHERE id=?", [req.params.id]);
    await loadRepairShops();
    res.json({ ok: true });
});

// -------------------------------------------------------------
// ADMIN API: TAXI-STÄNDE
// -------------------------------------------------------------
app.get("/api/admin/taxi_stands", async (req, res) => {
    const [rows] = await db.query("SELECT * FROM taxi_stands");
    res.json({ ok: true, taxiStands: rows });
});

app.post("/api/admin/taxi_stands", async (req, res) => {
    const { name, world, x, y } = req.body;
    if (!name || !world || x === undefined || y === undefined) {
        return res.json({ ok: false, error: "Alle Felder erforderlich" });
    }
    const [result] = await db.query(
        "INSERT INTO taxi_stands (name, world, x, y) VALUES (?, ?, ?, ?)",
        [name, world, x, y]
    );
    await loadTaxiStands();
    broadcastMapDataToWorld(world);
    res.json({ ok: true, id: result.insertId });
});

app.delete("/api/admin/taxi_stands/:id", async (req, res) => {
    await logDeletionForUndo("taxi_stands", "id", req.params.id);
    await db.query("DELETE FROM taxi_stands WHERE id=?", [req.params.id]);
    await loadTaxiStands();
    res.json({ ok: true });
});

// -------------------------------------------------------------
// ADMIN API: KRANKENHÄUSER
// -------------------------------------------------------------
app.get("/api/admin/hospitals", async (req, res) => {
    const [rows] = await db.query("SELECT * FROM hospitals");
    res.json({ ok: true, hospitals: rows });
});

app.post("/api/admin/hospitals", async (req, res) => {
    const { name, world, x, y } = req.body;
    if (!name || !world || x === undefined || y === undefined) {
        return res.json({ ok: false, error: "Alle Felder erforderlich" });
    }
    const [result] = await db.query(
        "INSERT INTO hospitals (name, world, x, y) VALUES (?, ?, ?, ?)",
        [name, world, x, y]
    );
    await loadHospitals();
    broadcastMapDataToWorld(world);
    res.json({ ok: true, id: result.insertId });
});

app.delete("/api/admin/hospitals/:id", async (req, res) => {
    await logDeletionForUndo("hospitals", "id", req.params.id);
    await db.query("DELETE FROM hospitals WHERE id=?", [req.params.id]);
    await loadHospitals();
    res.json({ ok: true });
});

// -------------------------------------------------------------
// ADMIN API: GEFÄNGNISSE
// -------------------------------------------------------------
app.get("/api/admin/prisons", async (req, res) => {
    const [rows] = await db.query("SELECT * FROM prisons");
    res.json({ ok: true, prisons: rows });
});

app.post("/api/admin/prisons", async (req, res) => {
    const { name, world, x, y } = req.body;
    if (!name || !world || x === undefined || y === undefined) {
        return res.json({ ok: false, error: "Alle Felder erforderlich" });
    }
    const [result] = await db.query(
        "INSERT INTO prisons (name, world, x, y) VALUES (?, ?, ?, ?)",
        [name, world, x, y]
    );
    await loadPrisons();
    broadcastMapDataToWorld(world);
    res.json({ ok: true, id: result.insertId });
});

app.delete("/api/admin/prisons/:id", async (req, res) => {
    await logDeletionForUndo("prisons", "id", req.params.id);
    await db.query("DELETE FROM prisons WHERE id=?", [req.params.id]);
    await loadPrisons();
    res.json({ ok: true });
});

// -------------------------------------------------------------
// ADMIN API: TERRITORIUMS-ZONEN
// -------------------------------------------------------------
app.get("/api/admin/radio_stations", async (req, res) => {
    const [rows] = await db.query("SELECT * FROM radio_stations ORDER BY name");
    res.json({ ok: true, stations: rows });
});

app.post("/api/admin/radio_stations", async (req, res) => {
    const { name, url } = req.body;
    if (!name || !url) {
        return res.json({ ok: false, error: "Name und Stream-URL erforderlich" });
    }
    const [result] = await db.query("INSERT INTO radio_stations (name, url) VALUES (?, ?)", [name, url]);
    res.json({ ok: true, id: result.insertId });
});

app.delete("/api/admin/radio_stations/:id", async (req, res) => {
    await logDeletionForUndo("radio_stations", "id", req.params.id);
    await db.query("DELETE FROM radio_stations WHERE id=?", [req.params.id]);
    res.json({ ok: true });
});

app.get("/api/admin/drug_types", async (req, res) => {
    const [rows] = await db.query("SELECT * FROM drug_types");
    res.json({ ok: true, drugTypes: rows });
});

app.post("/api/admin/drug_types", async (req, res) => {
    const { id, name, rawItemId, productItemId, basePrice } = req.body;
    if (!id || !name || !rawItemId || !productItemId) {
        return res.json({ ok: false, error: "ID, Name, Roh-Item und Produkt-Item erforderlich" });
    }
    await db.query(
        `INSERT INTO drug_types (id, name, raw_item_id, product_item_id, base_price) VALUES (?, ?, ?, ?, ?)
         ON DUPLICATE KEY UPDATE name=?, raw_item_id=?, product_item_id=?, base_price=?`,
        [id, name, rawItemId, productItemId, basePrice || 50, name, rawItemId, productItemId, basePrice || 50]
    );
    await loadDrugTypes();
    rotateDealerSpot(id, drugTypes.get(id));
    res.json({ ok: true });
});

app.delete("/api/admin/drug_types/:id", async (req, res) => {
    await logDeletionForUndo("drug_types", "id", req.params.id);
    await db.query("DELETE FROM drug_types WHERE id=?", [req.params.id]);
    await db.query("DELETE FROM drug_harvest_spots WHERE drug_id=?", [req.params.id]);
    await db.query("DELETE FROM drug_process_spots WHERE drug_id=?", [req.params.id]);
    await db.query("DELETE FROM drug_dealer_spots WHERE drug_id=?", [req.params.id]);
    activeDealerSpot.delete(req.params.id);
    await loadDrugTypes();
    await loadDrugHarvestSpots();
    await loadDrugProcessSpots();
    await loadDrugDealerSpots();
    res.json({ ok: true });
});

app.get("/api/admin/drug_harvest_spots", async (req, res) => {
    const [rows] = await db.query("SELECT * FROM drug_harvest_spots");
    res.json({ ok: true, spots: rows });
});
app.post("/api/admin/drug_harvest_spots", async (req, res) => {
    const { drugId, name, world, x, y } = req.body;
    if (!drugId || !world || x === undefined || y === undefined) {
        return res.json({ ok: false, error: "Alle Felder erforderlich" });
    }
    const [result] = await db.query(
        "INSERT INTO drug_harvest_spots (drug_id, name, world, x, y) VALUES (?, ?, ?, ?, ?)",
        [drugId, name || "Anbaustelle", world, x, y]
    );
    await loadDrugHarvestSpots();
    res.json({ ok: true, id: result.insertId });
});
app.delete("/api/admin/drug_harvest_spots/:id", async (req, res) => {
    await logDeletionForUndo("drug_harvest_spots", "id", req.params.id);
    await db.query("DELETE FROM drug_harvest_spots WHERE id=?", [req.params.id]);
    await loadDrugHarvestSpots();
    res.json({ ok: true });
});

app.get("/api/admin/drug_process_spots", async (req, res) => {
    const [rows] = await db.query("SELECT * FROM drug_process_spots");
    res.json({ ok: true, spots: rows });
});
app.post("/api/admin/drug_process_spots", async (req, res) => {
    const { drugId, name, world, x, y } = req.body;
    if (!drugId || !world || x === undefined || y === undefined) {
        return res.json({ ok: false, error: "Alle Felder erforderlich" });
    }
    const [result] = await db.query(
        "INSERT INTO drug_process_spots (drug_id, name, world, x, y) VALUES (?, ?, ?, ?, ?)",
        [drugId, name || "Labor", world, x, y]
    );
    await loadDrugProcessSpots();
    res.json({ ok: true, id: result.insertId });
});
app.delete("/api/admin/drug_process_spots/:id", async (req, res) => {
    await logDeletionForUndo("drug_process_spots", "id", req.params.id);
    await db.query("DELETE FROM drug_process_spots WHERE id=?", [req.params.id]);
    await loadDrugProcessSpots();
    res.json({ ok: true });
});

app.get("/api/admin/drug_dealer_spots", async (req, res) => {
    const [rows] = await db.query("SELECT * FROM drug_dealer_spots");
    res.json({ ok: true, spots: rows });
});
app.post("/api/admin/drug_dealer_spots", async (req, res) => {
    const { drugId, name, world, x, y } = req.body;
    if (!drugId || !world || x === undefined || y === undefined) {
        return res.json({ ok: false, error: "Alle Felder erforderlich" });
    }
    const [result] = await db.query(
        "INSERT INTO drug_dealer_spots (drug_id, name, world, x, y) VALUES (?, ?, ?, ?, ?)",
        [drugId, name || "Verkaufsstelle", world, x, y]
    );
    await loadDrugDealerSpots();
    res.json({ ok: true, id: result.insertId });
});
app.delete("/api/admin/drug_dealer_spots/:id", async (req, res) => {
    const [rows] = await db.query("SELECT drug_id FROM drug_dealer_spots WHERE id=?", [req.params.id]);
    await logDeletionForUndo("drug_dealer_spots", "id", req.params.id);
    await db.query("DELETE FROM drug_dealer_spots WHERE id=?", [req.params.id]);
    await loadDrugDealerSpots();
    res.json({ ok: true });
});

app.get("/api/admin/skin_items", async (req, res) => {
    const [rows] = await db.query("SELECT id, name, image_data IS NOT NULL AS has_image FROM skin_items ORDER BY name");
    res.json({ ok: true, items: rows });
});

app.get("/api/admin/skin_items/:id", async (req, res) => {
    const [rows] = await db.query("SELECT * FROM skin_items WHERE id=?", [req.params.id]);
    if (rows.length === 0) return res.json({ ok: false, error: "Nicht gefunden" });
    res.json({ ok: true, item: rows[0] });
});

app.post("/api/admin/skin_items", async (req, res) => {
    const { id, name, imageData } = req.body;
    if (!name) return res.json({ ok: false, error: "Name erforderlich" });
    if (!imageData && !id) return res.json({ ok: false, error: "Textur erforderlich" });
    if (imageData && imageData.length > 150000) return res.json({ ok: false, error: "Textur zu groß" });

    if (id) {
        let finalImageData = imageData;
        if (finalImageData === undefined) {
            const [existing] = await db.query("SELECT image_data FROM skin_items WHERE id=?", [id]);
            finalImageData = existing.length ? existing[0].image_data : null;
        }
        await db.query("UPDATE skin_items SET name=?, image_data=? WHERE id=?", [name, finalImageData, id]);
    } else {
        await db.query("INSERT INTO skin_items (name, image_data) VALUES (?, ?)", [name, imageData]);
    }
    await loadSkinItems();
    res.json({ ok: true });
});

app.delete("/api/admin/skin_items/:id", async (req, res) => {
    await logDeletionForUndo("skin_items", "id", req.params.id);
    await db.query("DELETE FROM skin_items WHERE id=?", [req.params.id]);
    await loadSkinItems();
    res.json({ ok: true });
});

app.get("/api/admin/clothing_items", async (req, res) => {
    const [rows] = await db.query("SELECT * FROM clothing_items ORDER BY slot, name");
    res.json({ ok: true, items: rows });
});

app.post("/api/admin/clothing_items", async (req, res) => {
    const { id, slot, name, color, price, imageData } = req.body;
    if (!slot || !["shirt", "pants", "shoes", "helmet"].includes(slot)) {
        return res.json({ ok: false, error: "Ungültiger Slot (shirt/pants/shoes)" });
    }
    if (!name || !color) {
        return res.json({ ok: false, error: "Name und Farbe erforderlich" });
    }
    if (imageData && imageData.length > 150000) {
        return res.json({ ok: false, error: "Textur zu groß" });
    }
    const finalPrice = Math.max(0, Math.floor(Number(price) || 150));

    if (id) {
        // Bearbeiten eines bestehenden Eintrags
        let finalImageData = imageData;
        if (finalImageData === undefined) {
            const [existing] = await db.query("SELECT image_data FROM clothing_items WHERE id=?", [id]);
            finalImageData = existing.length ? existing[0].image_data : null;
        }
        await db.query(
            "UPDATE clothing_items SET slot=?, name=?, color=?, price=?, image_data=? WHERE id=?",
            [slot, name, color, finalPrice, finalImageData || null, id]
        );
    } else {
        await db.query(
            "INSERT INTO clothing_items (slot, name, color, price, image_data) VALUES (?, ?, ?, ?, ?)",
            [slot, name, color, finalPrice, imageData || null]
        );
    }
    await loadClothingItems();
    res.json({ ok: true });
});

app.delete("/api/admin/clothing_items/:id", async (req, res) => {
    await logDeletionForUndo("clothing_items", "id", req.params.id);
    await db.query("DELETE FROM clothing_items WHERE id=?", [req.params.id]);
    await loadClothingItems();
    res.json({ ok: true });
});

app.get("/api/admin/insurance_offices", async (req, res) => {
    const [rows] = await db.query("SELECT * FROM insurance_offices");
    res.json({ ok: true, offices: rows });
});
app.post("/api/admin/insurance_offices", async (req, res) => {
    const { name, world, x, y } = req.body;
    if (!name || !world || x === undefined || y === undefined) {
        return res.json({ ok: false, error: "Alle Felder erforderlich" });
    }
    const [result] = await db.query(
        "INSERT INTO insurance_offices (name, world, x, y) VALUES (?, ?, ?, ?)",
        [name, world, x, y]
    );
    await loadInsuranceOffices();
    broadcastMapDataToWorld(world);
    res.json({ ok: true, id: result.insertId });
});
app.delete("/api/admin/insurance_offices/:id", async (req, res) => {
    await logDeletionForUndo("insurance_offices", "id", req.params.id);
    await db.query("DELETE FROM insurance_offices WHERE id=?", [req.params.id]);
    await loadInsuranceOffices();
    res.json({ ok: true });
});

app.get("/api/admin/plate_offices", async (req, res) => {
    const [rows] = await db.query("SELECT * FROM plate_offices");
    res.json({ ok: true, offices: rows });
});
app.post("/api/admin/plate_offices", async (req, res) => {
    const { name, world, x, y } = req.body;
    if (!name || !world || x === undefined || y === undefined) {
        return res.json({ ok: false, error: "Alle Felder erforderlich" });
    }
    const [result] = await db.query(
        "INSERT INTO plate_offices (name, world, x, y) VALUES (?, ?, ?, ?)",
        [name, world, x, y]
    );
    await loadPlateOffices();
    broadcastMapDataToWorld(world);
    res.json({ ok: true, id: result.insertId });
});
app.delete("/api/admin/plate_offices/:id", async (req, res) => {
    await logDeletionForUndo("plate_offices", "id", req.params.id);
    await db.query("DELETE FROM plate_offices WHERE id=?", [req.params.id]);
    await loadPlateOffices();
    res.json({ ok: true });
});

app.get("/api/admin/trailer_shops", async (req, res) => {
    const [rows] = await db.query("SELECT * FROM trailer_shops");
    res.json({ ok: true, shops: rows });
});
app.post("/api/admin/trailer_shops", async (req, res) => {
    const { name, world, x, y } = req.body;
    if (!name || !world || x === undefined || y === undefined) {
        return res.json({ ok: false, error: "Alle Felder erforderlich" });
    }
    const [result] = await db.query(
        "INSERT INTO trailer_shops (name, world, x, y) VALUES (?, ?, ?, ?)",
        [name, world, x, y]
    );
    await loadTrailerShops();
    broadcastMapDataToWorld(world);
    res.json({ ok: true, id: result.insertId });
});
app.delete("/api/admin/trailer_shops/:id", async (req, res) => {
    await logDeletionForUndo("trailer_shops", "id", req.params.id);
    await db.query("DELETE FROM trailer_shops WHERE id=?", [req.params.id]);
    await loadTrailerShops();
    res.json({ ok: true });
});

app.get("/api/admin/black_market_spots", async (req, res) => {
    const [rows] = await db.query("SELECT * FROM black_market_spots");
    res.json({ ok: true, shops: rows });
});
app.post("/api/admin/black_market_spots", async (req, res) => {
    const { name, world, x, y } = req.body;
    if (!name || !world || x === undefined || y === undefined) {
        return res.json({ ok: false, error: "Alle Felder erforderlich" });
    }
    const [result] = await db.query(
        "INSERT INTO black_market_spots (name, world, x, y) VALUES (?, ?, ?, ?)",
        [name, world, x, y]
    );
    await loadBlackMarketSpots();
    broadcastMapDataToWorld(world);
    res.json({ ok: true, id: result.insertId });
});
app.delete("/api/admin/black_market_spots/:id", async (req, res) => {
    await logDeletionForUndo("black_market_spots", "id", req.params.id);
    await db.query("DELETE FROM black_market_spots WHERE id=?", [req.params.id]);
    await loadBlackMarketSpots();
    res.json({ ok: true });
});

app.get("/api/admin/highway_links", async (req, res) => {
    const [rows] = await db.query("SELECT * FROM highway_links");
    res.json({ ok: true, links: rows });
});
app.post("/api/admin/highway_links", async (req, res) => {
    const { name, worldA, xA, yA, worldB, xB, yB } = req.body;
    if (!worldA || !worldB || xA === undefined || yA === undefined || xB === undefined || yB === undefined) {
        return res.json({ ok: false, error: "Alle Felder erforderlich" });
    }
    const [result] = await db.query(
        "INSERT INTO highway_links (name, world_a, x_a, y_a, world_b, x_b, y_b) VALUES (?, ?, ?, ?, ?, ?, ?)",
        [name || "Autobahn", worldA, xA, yA, worldB, xB, yB]
    );
    await loadHighwayLinks();
    res.json({ ok: true, id: result.insertId });
});
app.delete("/api/admin/highway_links/:id", async (req, res) => {
    await logDeletionForUndo("highway_links", "id", req.params.id);
    await db.query("DELETE FROM highway_links WHERE id=?", [req.params.id]);
    await loadHighwayLinks();
    res.json({ ok: true });
});

app.get("/api/admin/clothing_shops", async (req, res) => {
    const [rows] = await db.query("SELECT * FROM clothing_shops");
    res.json({ ok: true, shops: rows });
});

app.post("/api/admin/clothing_shops", async (req, res) => {
    const { name, world, x, y } = req.body;
    if (!name || !world || x === undefined || y === undefined) {
        return res.json({ ok: false, error: "Alle Felder erforderlich" });
    }
    const [result] = await db.query(
        "INSERT INTO clothing_shops (name, world, x, y) VALUES (?, ?, ?, ?)",
        [name, world, x, y]
    );
    await loadClothingShops();
    broadcastMapDataToWorld(world);
    res.json({ ok: true, id: result.insertId });
});

app.delete("/api/admin/clothing_shops/:id", async (req, res) => {
    await logDeletionForUndo("clothing_shops", "id", req.params.id);
    await db.query("DELETE FROM clothing_shops WHERE id=?", [req.params.id]);
    await loadClothingShops();
    res.json({ ok: true });
});

app.get("/api/admin/fire_stations", async (req, res) => {
    const [rows] = await db.query("SELECT * FROM fire_stations");
    res.json({ ok: true, stations: rows });
});

app.post("/api/admin/fire_stations", async (req, res) => {
    const { name, world, x, y } = req.body;
    if (!name || !world || x === undefined || y === undefined) {
        return res.json({ ok: false, error: "Alle Felder erforderlich" });
    }
    const [result] = await db.query(
        "INSERT INTO fire_stations (name, world, x, y) VALUES (?, ?, ?, ?)",
        [name, world, x, y]
    );
    await loadFireStations();
    broadcastMapDataToWorld(world);
    res.json({ ok: true, id: result.insertId });
});

app.delete("/api/admin/fire_stations/:id", async (req, res) => {
    await logDeletionForUndo("fire_stations", "id", req.params.id);
    await db.query("DELETE FROM fire_stations WHERE id=?", [req.params.id]);
    await loadFireStations();
    res.json({ ok: true });
});

app.get("/api/admin/impound_lots", async (req, res) => {
    const [rows] = await db.query("SELECT * FROM impound_lots");
    res.json({ ok: true, lots: rows });
});

app.post("/api/admin/impound_lots", async (req, res) => {
    const { name, world, x, y } = req.body;
    if (!name || !world || x === undefined || y === undefined) {
        return res.json({ ok: false, error: "Alle Felder erforderlich" });
    }
    const [result] = await db.query(
        "INSERT INTO impound_lots (name, world, x, y) VALUES (?, ?, ?, ?)",
        [name, world, x, y]
    );
    await loadImpoundLots();
    broadcastMapDataToWorld(world);
    res.json({ ok: true, id: result.insertId });
});

app.delete("/api/admin/impound_lots/:id", async (req, res) => {
    await logDeletionForUndo("impound_lots", "id", req.params.id);
    await db.query("DELETE FROM impound_lots WHERE id=?", [req.params.id]);
    await loadImpoundLots();
    res.json({ ok: true });
});

app.get("/api/admin/gates", async (req, res) => {
    const [rows] = await db.query(`
        SELECT g.*, p.username AS owner_name
        FROM gates g LEFT JOIN players p ON p.id = g.owner_id
    `);
    res.json({ ok: true, gates: rows });
});

app.post("/api/admin/gates/:id/reset", async (req, res) => {
    await db.query("DELETE FROM gate_keys WHERE gate_id=?", [req.params.id]);
    await db.query("UPDATE gates SET owner_id=NULL WHERE id=?", [req.params.id]);
    await loadGates();
    res.json({ ok: true });
});

app.get("/api/admin/territory_zones", async (req, res) => {
    const [rows] = await db.query("SELECT * FROM territory_zones");
    res.json({ ok: true, zones: rows });
});

app.post("/api/admin/territory_zones", async (req, res) => {
    const { name, world, x, y } = req.body;
    if (!name || !world || x === undefined || y === undefined) {
        return res.json({ ok: false, error: "Alle Felder erforderlich" });
    }
    const [result] = await db.query(
        "INSERT INTO territory_zones (name, world, x, y) VALUES (?, ?, ?, ?)",
        [name, world, x, y]
    );
    await loadTerritoryZones();
    res.json({ ok: true, id: result.insertId });
});

app.delete("/api/admin/territory_zones/:id", async (req, res) => {
    await logDeletionForUndo("territory_zones", "id", req.params.id);
    await db.query("DELETE FROM territory_zones WHERE id=?", [req.params.id]);
    await loadTerritoryZones();
    res.json({ ok: true });
});

// -------------------------------------------------------------
// ADMIN API: JOB-PUNKTE
// -------------------------------------------------------------
app.get("/api/admin/job_points", async (req, res) => {
    const [rows] = await db.query("SELECT * FROM job_points");
    res.json({ ok: true, jobPoints: rows });
});

app.post("/api/admin/job_points", async (req, res) => {
    const { jobId, name, world, x, y, kind, reward, itemId } = req.body;
    if (!jobId || !name || !world || x === undefined || y === undefined) {
        return res.json({ ok: false, error: "Alle Felder erforderlich" });
    }
    const [result] = await db.query(
        "INSERT INTO job_points (job_id, name, world, x, y, kind, reward, item_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
        [jobId, name, world, x, y, kind === "dropoff" ? "dropoff" : "pickup", reward || 50, itemId || "package"]
    );
    await loadJobPoints();
    res.json({ ok: true, id: result.insertId });
});

app.delete("/api/admin/job_points/:id", async (req, res) => {
    await logDeletionForUndo("job_points", "id", req.params.id);
    await db.query("DELETE FROM job_points WHERE id=?", [req.params.id]);
    await loadJobPoints();
    res.json({ ok: true });
});

// -------------------------------------------------------------
// ADMIN API: HÄUSER
// -------------------------------------------------------------
app.get("/api/admin/houses", async (req, res) => {
    const [rows] = await db.query("SELECT * FROM houses");
    res.json({ ok: true, houses: rows });
});

app.post("/api/admin/houses", async (req, res) => {
    const { name, world, x, y, price } = req.body;
    if (!name || !world || x === undefined || y === undefined) {
        return res.json({ ok: false, error: "Alle Felder erforderlich" });
    }
    const [result] = await db.query(
        "INSERT INTO houses (name, world, x, y, price) VALUES (?, ?, ?, ?, ?)",
        [name, world, x, y, price || 1000]
    );
    await loadHouses();
    res.json({ ok: true, id: result.insertId });
});

app.delete("/api/admin/houses/:id", async (req, res) => {
    await logDeletionForUndo("houses", "id", req.params.id);
    await db.query("DELETE FROM houses WHERE id=?", [req.params.id]);
    await loadHouses();
    res.json({ ok: true });
});

// -------------------------------------------------------------
// ADMIN API: JOBS & RÄNGE
// -------------------------------------------------------------
app.get("/api/admin/jobs", async (req, res) => {
    const [jobRows] = await db.query("SELECT * FROM jobs ORDER BY name");
    const [rankRows] = await db.query("SELECT * FROM job_ranks ORDER BY job_id, level ASC");

    const result = jobRows.map(j => ({
        id: j.id,
        name: j.name,
        type: j.type || "generic",
        protected: !!j.protected,
        uniformShirtId: j.uniform_shirt_id,
        uniformPantsId: j.uniform_pants_id,
        uniformShoesId: j.uniform_shoes_id,
        uniformHelmetId: j.uniform_helmet_id,
        ranks: rankRows.filter(r => r.job_id === j.id)
    }));

    res.json({ ok: true, jobs: result });
});

app.post("/api/admin/jobs", async (req, res) => {
    const { name, type, protected: isProtected } = req.body;
    if (!name) return res.json({ ok: false, error: "Name erforderlich" });

    const allowedTypes = ["generic", "taxi", "police", "medic", "tow", "fire", "mechanic"];
    const finalType = allowedTypes.includes(type) ? type : "generic";
    const autoProtect = finalType === "police" || finalType === "medic";

    const [result] = await db.query(
        "INSERT INTO jobs (name, type, protected) VALUES (?, ?, ?)",
        [name, finalType, (isProtected || autoProtect) ? 1 : 0]
    );
    await loadJobs();
    res.json({ ok: true, id: result.insertId });
});

app.post("/api/admin/jobs/:id/uniform", async (req, res) => {
    const { shirtId, pantsId, shoesId, helmetId } = req.body;
    await db.query(
        "UPDATE jobs SET uniform_shirt_id=?, uniform_pants_id=?, uniform_shoes_id=?, uniform_helmet_id=? WHERE id=?",
        [shirtId || null, pantsId || null, shoesId || null, helmetId || null, req.params.id]
    );
    await loadJobs();
    res.json({ ok: true });
});

app.delete("/api/admin/jobs/:id", async (req, res) => {
    await logDeletionForUndo("jobs", "id", req.params.id);
    await db.query("DELETE FROM jobs WHERE id=?", [req.params.id]);
    await loadJobs();
    res.json({ ok: true });
});

app.post("/api/admin/job_ranks", async (req, res) => {
    const { id, jobId, level, title, salary } = req.body;

    if (!jobId || !title) {
        return res.json({ ok: false, error: "jobId und Titel erforderlich" });
    }

    if (id) {
        await db.query(
            "UPDATE job_ranks SET level=?, title=?, salary=? WHERE id=?",
            [level || 1, title, salary || 0, id]
        );
    } else {
        await db.query(
            "INSERT INTO job_ranks (job_id, level, title, salary) VALUES (?, ?, ?, ?)",
            [jobId, level || 1, title, salary || 0]
        );
    }

    await loadJobs();
    res.json({ ok: true });
});

app.delete("/api/admin/job_ranks/:id", async (req, res) => {
    await logDeletionForUndo("job_ranks", "id", req.params.id);
    await db.query("DELETE FROM job_ranks WHERE id=?", [req.params.id]);
    await loadJobs();
    res.json({ ok: true });
});

// -------------------------------------------------------------
// ADMIN API: JOB-FAHRZEUGE (Fuhrpark)
// -------------------------------------------------------------
app.get("/api/admin/job_vehicles", async (req, res) => {
    const [rows] = await db.query(
        "SELECT id, model, job_id, is_stored FROM cars WHERE job_id IS NOT NULL"
    );
    res.json({ ok: true, vehicles: rows });
});

app.post("/api/admin/job_vehicles", async (req, res) => {
    const { jobId, model } = req.body;
    if (!jobId || !model) {
        return res.json({ ok: false, error: "Job und Modell erforderlich" });
    }

    const cfg = carConfigs[model] || carConfigs.sedan || {};
    const fuel = cfg.tankSize || 50;

    const [result] = await db.query(
        "INSERT INTO cars (owner_id, model, world, x, y, angle, fuel, health, job_id, is_stored) VALUES (NULL, ?, '', 0, 0, 0, ?, 100, ?, 1)",
        [model, fuel, jobId]
    );
    res.json({ ok: true, id: result.insertId });
});

app.delete("/api/admin/job_vehicles/:id", async (req, res) => {
    await db.query("DELETE FROM cars WHERE id=? AND job_id IS NOT NULL", [req.params.id]);
    cars.delete(Number(req.params.id));
    sendCarsToAll();
    res.json({ ok: true });
});

// -------------------------------------------------------------
// ADMIN API: JOBCENTER
// -------------------------------------------------------------
app.get("/api/admin/jobcenters", async (req, res) => {
    const [rows] = await db.query("SELECT * FROM jobcenters");
    res.json({ ok: true, jobcenters: rows });
});

app.post("/api/admin/jobcenters", async (req, res) => {
    const { name, world, x, y } = req.body;
    if (!name || !world || x === undefined || y === undefined) {
        return res.json({ ok: false, error: "Alle Felder erforderlich" });
    }

    const [result] = await db.query(
        "INSERT INTO jobcenters (name, world, x, y) VALUES (?, ?, ?, ?)",
        [name, world, x, y]
    );
    await loadJobcenters();
    broadcastMapDataToWorld(world);
    res.json({ ok: true, id: result.insertId });
});

app.delete("/api/admin/jobcenters/:id", async (req, res) => {
    await logDeletionForUndo("jobcenters", "id", req.params.id);
    await db.query("DELETE FROM jobcenters WHERE id=?", [req.params.id]);
    await loadJobcenters();
    res.json({ ok: true });
});

// Admin setzt/ändert den Job+Rang eines beliebigen Spielers (Beförderung)
app.post("/api/admin/players/:id/job", async (req, res) => {
    const rankId = req.body.rankId || null;

    await db.query("UPDATE players SET job_rank_id=? WHERE id=?", [rankId, req.params.id]);

    const p = playersOnline.get(Number(req.params.id));
    if (p) {
        p.state.jobRankId = rankId;
    }

    res.json({ ok: true });
});

// -------------------------------------------------------------
// AUTO-CONFIGS: öffentlicher Endpunkt (für den Client)
// -------------------------------------------------------------
app.get("/api/car_configs", async (req, res) => {
    res.json({ ok: true, configs: carConfigs });
});

app.get("/api/clothing_catalog", async (req, res) => {
    res.json({ ok: true, items: [...clothingItems.values()] });
});

app.get("/api/skin_catalog", async (req, res) => {
    res.json({ ok: true, items: [...skinItems.values()] });
});

// -------------------------------------------------------------
// ADMIN API: AUTO-CONFIGS
// -------------------------------------------------------------
app.get("/api/admin/car_configs", async (req, res) => {
    const [rows] = await db.query("SELECT * FROM car_configs");
    res.json({ ok: true, configs: rows });
});

app.post("/api/admin/car_configs", async (req, res) => {
    const { model, width, height, color, maxSpeed, accel, brake, friction, turnSpeed, tankSize, consumption, isTrailerModel, trailerPrice, imageData } = req.body;

    if (!model) return res.json({ ok: false, error: "Model-Name fehlt" });
    if (imageData && imageData.length > 300000) {
        return res.json({ ok: false, error: "Textur zu groß" });
    }

    try {
        await db.query(
            `INSERT INTO car_configs (model, width, height, color, max_speed, accel, brake, friction, turn_speed, tank_size, consumption, is_trailer_model, trailer_price, image_data)
             VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
             ON DUPLICATE KEY UPDATE
                width=?, height=?, color=?, max_speed=?, accel=?, brake=?, friction=?, turn_speed=?, tank_size=?, consumption=?, is_trailer_model=?, trailer_price=?, image_data=?`,
            [model, width, height, color, maxSpeed, accel, brake, friction, turnSpeed, tankSize || 50, consumption || 0.02, isTrailerModel ? 1 : 0, trailerPrice || null, imageData || null,
             width, height, color, maxSpeed, accel, brake, friction, turnSpeed, tankSize || 50, consumption || 0.02, isTrailerModel ? 1 : 0, trailerPrice || null, imageData || null]
        );
        await loadCarConfigs();
        res.json({ ok: true });
    } catch {
        res.json({ ok: false, error: "Speichern fehlgeschlagen" });
    }
});

app.delete("/api/admin/car_configs/:model", async (req, res) => {
    try {
        await logDeletionForUndo("car_configs", "model", req.params.model);
        await db.query("DELETE FROM car_configs WHERE model=?", [req.params.model]);
        await loadCarConfigs();
        res.json({ ok: true });
    } catch {
        res.json({ ok: false, error: "Löschen fehlgeschlagen (evtl. noch Autos dieses Modells im Umlauf)" });
    }
});

// -------------------------------------------------------------
// ADMIN API: NEWS
// -------------------------------------------------------------
app.post("/api/admin/news", async (req, res) => {
    const { title, content } = req.body;

    if (!title || !content) {
        return res.json({ ok: false, error: "Titel und Inhalt erforderlich" });
    }

    await db.query(
        "INSERT INTO news (title, content, author) VALUES (?, ?, ?)",
        [title, content, req.player.username]
    );

    sendDiscordWebhook("news", {
        title: `📰 ${title}`,
        description: content.slice(0, 4000),
        color: 0xf5d90a,
        footer: { text: `Veröffentlicht von ${req.player.username}` },
        timestamp: new Date().toISOString()
    });

    res.json({ ok: true });
});

app.delete("/api/admin/news/:id", async (req, res) => {
    await logDeletionForUndo("news", "id", req.params.id);
    await db.query("DELETE FROM news WHERE id=?", [req.params.id]);
    res.json({ ok: true });
});

// -------------------------------------------------------------
// ADMIN API: CHANGELOG
// -------------------------------------------------------------
app.post("/api/admin/changelog", async (req, res) => {
    const { version, title, content } = req.body;
    if (!version || !title || !content) {
        return res.json({ ok: false, error: "Version, Titel und Inhalt erforderlich" });
    }
    await db.query(
        "INSERT INTO changelog (version, title, content) VALUES (?, ?, ?)",
        [version, title, content]
    );

    sendDiscordWebhook("changelog", {
        title: `🛠️ ${version} - ${title}`,
        description: content.slice(0, 4000),
        color: 0x2c7a3d,
        timestamp: new Date().toISOString()
    });

    res.json({ ok: true });
});

app.delete("/api/admin/changelog/:id", async (req, res) => {
    await logDeletionForUndo("changelog", "id", req.params.id);
    await db.query("DELETE FROM changelog WHERE id=?", [req.params.id]);
    res.json({ ok: true });
});

// -------------------------------------------------------------
// ADMIN API: WÜNSCHE (Status ändern, löschen)
// -------------------------------------------------------------
app.post("/api/admin/wishes/:id/status", async (req, res) => {
    const { status } = req.body;
    const allowed = ["open", "planned", "done", "rejected"];
    if (!allowed.includes(status)) {
        return res.json({ ok: false, error: "Ungültiger Status" });
    }
    await db.query("UPDATE wishes SET status=? WHERE id=?", [status, req.params.id]);
    res.json({ ok: true });
});

app.delete("/api/admin/wishes/:id", async (req, res) => {
    await logDeletionForUndo("wishes", "id", req.params.id);
    await db.query("DELETE FROM wishes WHERE id=?", [req.params.id]);
    await db.query("DELETE FROM wish_votes WHERE wish_id=?", [req.params.id]);
    res.json({ ok: true });
});

// -------------------------------------------------------------
// ADMIN API: ANMERKUNGEN (lesen, löschen)
// -------------------------------------------------------------
app.get("/api/admin/remarks", async (req, res) => {
    const [rows] = await db.query(
        "SELECT id, player_id, username, content, created_at FROM remarks ORDER BY created_at DESC LIMIT 200"
    );
    res.json({ ok: true, remarks: rows });
});

app.delete("/api/admin/remarks/:id", async (req, res) => {
    await logDeletionForUndo("remarks", "id", req.params.id);
    await db.query("DELETE FROM remarks WHERE id=?", [req.params.id]);
    res.json({ ok: true });
});

// -------------------------------------------------------------
// ADMIN API: SPIELER-INVENTARE
// -------------------------------------------------------------
app.get("/api/admin/players", async (req, res) => {
    const [rows] = await db.query(
        "SELECT id, username, money, bank, approved, is_admin, is_beta_tester, job_rank_id, banned, muted_until, application_text FROM players ORDER BY approved ASC, username ASC"
    );
    res.json({ ok: true, players: rows });
});

app.post("/api/admin/players/:id/approve", async (req, res) => {
    await db.query("UPDATE players SET approved=1 WHERE id=?", [req.params.id]);
    res.json({ ok: true });
});

// Bewerbung ablehnen: Account komplett löschen (anders als Bannen, das den
// Account behält und nur den Login blockiert - eine abgelehnte Bewerbung
// hat ja noch nie gespielt, es gibt nichts zu bewahren)
app.post("/api/admin/players/:id/reject", async (req, res) => {
    const [rows] = await db.query("SELECT approved FROM players WHERE id=?", [req.params.id]);
    if (rows.length === 0) return res.json({ ok: false, error: "Nicht gefunden" });
    if (rows[0].approved) {
        return res.json({ ok: false, error: "Bereits freigeschaltete Accounts können nicht abgelehnt werden, nur gebannt." });
    }
    await db.query("DELETE FROM players WHERE id=? AND approved=0", [req.params.id]);
    res.json({ ok: true });
});

app.post("/api/admin/players/:id/revoke", async (req, res) => {
    await db.query("UPDATE players SET approved=0 WHERE id=?", [req.params.id]);

    // Falls gerade online, Verbindung trennen
    const p = playersOnline.get(Number(req.params.id));
    if (p) {
        try { p.ws.close(); } catch {}
        playersOnline.delete(Number(req.params.id));
        sendStateToAll();
    }

    res.json({ ok: true });
});

app.post("/api/admin/players/:id/set_admin", async (req, res) => {
    const makeAdmin = !!req.body.isAdmin;
    await db.query("UPDATE players SET is_admin=? WHERE id=?", [makeAdmin ? 1 : 0, req.params.id]);
    res.json({ ok: true });
});

app.post("/api/admin/players/:id/set_beta_tester", async (req, res) => {
    const isBetaTester = !!req.body.isBetaTester;
    await db.query("UPDATE players SET is_beta_tester=? WHERE id=?", [isBetaTester ? 1 : 0, req.params.id]);
    res.json({ ok: true });
});

app.post("/api/admin/players/:id/ban", async (req, res) => {
    const banned = !!req.body.banned;
    const [rows] = await db.query("SELECT username FROM players WHERE id=?", [req.params.id]);
    await db.query("UPDATE players SET banned=? WHERE id=?", [banned ? 1 : 0, req.params.id]);

    // Aktive Sitzung sofort trennen, falls gebannt
    if (banned) {
        const target = playersOnline.get(Number(req.params.id));
        if (target) {
            try { target.ws.close(); } catch {}
            playersOnline.delete(Number(req.params.id));
            sendStateToAll();
            sendCarsToAll();
        }
    }

    await logEvent("ban", req.player?.username || "admin", `${rows[0]?.username || req.params.id} ${banned ? "gesperrt" : "entsperrt"}`);
    res.json({ ok: true });
});

app.post("/api/admin/players/:id/mute", async (req, res) => {
    const minutes = Math.max(0, Number(req.body.minutes) || 0);
    const [rows] = await db.query("SELECT username FROM players WHERE id=?", [req.params.id]);

    if (minutes === 0) {
        await db.query("UPDATE players SET muted_until=NULL WHERE id=?", [req.params.id]);
        const target = playersOnline.get(Number(req.params.id));
        if (target) target.state.mutedUntil = null;
        await logEvent("mute", req.player?.username || "admin", `${rows[0]?.username || req.params.id} entstummt`);
    } else {
        const until = new Date(Date.now() + minutes * 60000);
        await db.query("UPDATE players SET muted_until=? WHERE id=?", [until, req.params.id]);
        const target = playersOnline.get(Number(req.params.id));
        if (target) target.state.mutedUntil = until.getTime();
        await logEvent("mute", req.player?.username || "admin", `${rows[0]?.username || req.params.id} für ${minutes}min stummgeschaltet`);
    }
    res.json({ ok: true });
});

app.get("/api/admin/players/:id/inventory", async (req, res) => {
    const [rows] = await db.query(
        "SELECT id, username, inventory FROM players WHERE id=?",
        [req.params.id]
    );
    if (rows.length === 0) {
        return res.json({ ok: false, error: "Spieler nicht gefunden" });
    }

    let inventory = [];
    try {
        inventory = JSON.parse(rows[0].inventory || "[]");
    } catch {
        inventory = [];
    }

    res.json({ ok: true, username: rows[0].username, inventory });
});

app.post("/api/admin/players/:id/inventory", async (req, res) => {
    const { inventory } = req.body;

    if (!Array.isArray(inventory)) {
        return res.json({ ok: false, error: "inventory muss ein Array sein" });
    }

    // Grobe Validierung: jeder Eintrag braucht id + amount > 0
    const cleaned = inventory
        .filter(i => i && i.id && Number(i.amount) > 0)
        .map(i => ({ id: String(i.id), amount: Math.floor(Number(i.amount)) }));

    await db.query(
        "UPDATE players SET inventory=? WHERE id=?",
        [JSON.stringify(cleaned), req.params.id]
    );

    // Falls der Spieler gerade online ist, sofort live aktualisieren
    const p = playersOnline.get(Number(req.params.id));
    if (p) {
        p.state.inventory = cleaned;
        sendStateToAll();
    }

    res.json({ ok: true, inventory: cleaned });
});

// -------------------------------------------------------------
// COLLISION (Tiles + Objekte)
// -------------------------------------------------------------
// Prüft, ob eine Position auf einer als "PvP-sicher" markierten Kachel liegt
function isInSafeZone(world, x, y) {
    const map = maps[world];
    if (!map || !map.tiles) return false;

    const tileX = Math.floor(x / 32);
    const tileY = Math.floor(y / 32);
    const tileId = map.tiles[tileY]?.[tileX];
    const tile = tileConfig[tileId];
    return !!(tile && tile.pvpSafe);
}

function canMove(player, nx, ny, debugLabel) {
    const map = maps[player.state.world];
    if (!map || !map.tiles) return true;

    const tileX = Math.floor(nx / 32);
    const tileY = Math.floor(ny / 32);

    // KARTENRAND: außerhalb der Map ist grundsätzlich gesperrt
    if (tileY < 0 || tileY >= map.tiles.length || tileX < 0 || tileX >= map.tiles[0].length) {
        return false;
    }

    // TILE COLLISION
    const tileId = map.tiles[tileY]?.[tileX];
    const tile = tileConfig[tileId];
    if (tile && tile.collision) {
        if (debugLabel) console.log(`[Kollision] ${debugLabel} blockiert an Kachel (${tileX},${tileY}) id=${tileId} name=${tile.name}`);
        return false;
    }

    // STRASSENSPERRE
    if (isBlockedByRoadblock(player.state.world, nx, ny)) {
        return false;
    }

    // OBJECT COLLISION
    if (map.objects) {
        for (const obj of map.objects) {
            const cfg = objectConfig[obj.type];
            if (!cfg || !cfg.collision) continue;
            if (obj.open) continue; // Tor/Tür ist offen -> keine Kollision (gilt für Fußgänger UND Fahrzeuge)

            const ox = obj.x;
            const oy = obj.y - (cfg.height - 32);
            const ow = cfg.width;
            const oh = cfg.height;

            const px = nx;
            const py = ny;
            const pw = 30;
            const ph = 30;

            const hit =
                px < ox + ow &&
                px + pw > ox &&
                py < oy + oh &&
                py + ph > oy;

            if (hit) {
                if (debugLabel) console.log(`[Kollision] ${debugLabel} blockiert an Objekt bei (${ox},${oy}) Größe ${ow}x${oh}, open=${!!obj.open}, type=${obj.type}`);
                return false;
            }
        }
    }

    return true;
}

// -------------------------------------------------------------
// NPC-VERKEHR
// -------------------------------------------------------------
let npcIdCounter = 0;

// Findet die nächste begehbare "Straßen"-Kachel als neues Fahrziel und versetzt
// das Ziel leicht zur rechten Fahrbahnseite (bezogen auf die Fahrtrichtung) -
// simuliert einfaches Rechtsfahren, ohne eine echte Spurführung zu benötigen.
const NPC_LANE_OFFSET = 8; // px, wie weit rechts der Fahrbahnmitte gefahren wird

function pickNewNpcTarget(c) {
    const map = maps[c.world];
    if (!map || !map.tiles || !map.tiles[0]) return;

    const cols = map.tiles[0].length;
    const rows = map.tiles.length;

    for (let i = 0; i < 40; i++) {
        const tx = Math.floor(Math.random() * cols);
        const ty = Math.floor(Math.random() * rows);
        const tileId = map.tiles[ty][tx];
        const cfg = tileConfig[tileId];

        if (cfg && !cfg.collision && cfg.isRoad) {
            const baseX = tx * 32 + 16;
            const baseY = ty * 32 + 16;

            // Fahrtrichtung zum neuen Ziel bestimmen, um rechts davon zu versetzen
            let dx = baseX - (c.x || baseX);
            let dy = baseY - (c.y || baseY);
            const dist = Math.hypot(dx, dy) || 1;
            dx /= dist;
            dy /= dist;

            // "Rechts" der Fahrtrichtung in Bildschirmkoordinaten (y wächst nach unten)
            const rightX = -dy;
            const rightY = dx;

            c.npcTargetX = baseX + rightX * NPC_LANE_OFFSET;
            c.npcTargetY = baseY + rightY * NPC_LANE_OFFSET;
            return;
        }
    }
    // Keine Straßen-Kachel gefunden (Map hat evtl. noch keine markiert) -> nächster Tick versucht es erneut
    c.npcTargetX = undefined;
}

function spawnNpcTraffic() {
    if (!getSetting("npc_traffic_enabled")) {
        console.log("[NPC-Verkehr] Deaktiviert (Einstellung npc_traffic_enabled), überspringe Spawn.");
        return;
    }

    const npcModels = Object.keys(carConfigs);
    if (npcModels.length === 0) {
        console.log("[NPC-Verkehr] Keine Fahrzeug-Configs vorhanden, überspringe Spawn.");
        return;
    }

    let spawned = 0;

    for (const [world, map] of Object.entries(maps)) {
        if (!map.tiles || !map.tiles[0]) continue;
        if (world.startsWith("house_")) continue; // keine NPCs in Haus-Innenräumen

        for (let i = 0; i < NPC_COUNT_PER_WORLD; i++) {
            const model = npcModels[Math.floor(Math.random() * npcModels.length)];
            const npcId = NPC_ID_OFFSET + (npcIdCounter++);

            const npc = {
                id: npcId,
                ownerId: null,
                model,
                world,
                x: 0,
                y: 0,
                angle: 0,
                speed: 0,
                fuel: 100,
                throttle: 0,
                steer: 0,
                driverId: null,
                headlights: false,
                leftBlinker: false,
                rightBlinker: false,
                brakeLight: false,
                hazard: false,
                health: 100,
                trunk: [],
                passengerId: null,
                jobId: null,
                emergency: false,
                odometer: 0,
                isNpc: true
            };

            pickNewNpcTarget(npc);

            // Keine Straßen-Kachel in dieser Welt markiert -> diesen NPC gar nicht erst spawnen,
            // sonst würde er unsichtbar bei (0,0) herumstehen
            if (npc.npcTargetX === undefined) continue;

            npc.x = npc.npcTargetX;
            npc.y = npc.npcTargetY;

            cars.set(npcId, npc);
            spawned++;
        }
    }

    console.log(`[NPC-Verkehr] ${spawned} NPC-Fahrzeuge gespawnt.`);
}

function removeAllNpcCars() {
    let removed = 0;
    for (const [id, c] of cars) {
        if (c.isNpc) {
            cars.delete(id);
            removed++;
        }
    }
    if (removed > 0) sendCarsToAll();
    console.log(`[NPC-Verkehr] ${removed} NPC-Fahrzeuge entfernt.`);
}

// Wird aufgerufen, wenn die Einstellung npc_traffic_enabled live geändert wird
function syncNpcTrafficWithSetting() {
    const shouldBeOn = !!getSetting("npc_traffic_enabled");
    const hasAnyNpc = [...cars.values()].some(c => c.isNpc);

    if (shouldBeOn && !hasAnyNpc) spawnNpcTraffic();
    if (!shouldBeOn && hasAnyNpc) removeAllNpcCars();
}

spawnNpcTraffic();

// Hilfsfunktion: Spieler holen
function getPlayer(playerId) {
    return playersOnline.get(playerId);
}

// -------------------------------------------------------------
// STATE AN ALLE SENDEN
// -------------------------------------------------------------
// -------------------------------------------------------------
// GEBÜNDELTE STATE-BROADCASTS BEI BEWEGUNG: der Client sendet beim Laufen
// bis zu 60 "move"-Nachrichten pro Sekunde und Spieler - würde jede davon
// sofort einen kompletten sendStateToAll() auslösen, bricht das bei mehreren
// gleichzeitig laufenden Spielern spürbar ein (Ruckeln). Stattdessen wird
// hier nur eine Änderung "vorgemerkt" und alle 100ms gesammelt einmal
// gesendet, unabhängig davon, wie viele Spieler sich dazwischen bewegt haben.
// -------------------------------------------------------------
let stateBroadcastPending = false;
function scheduleStateBroadcast() {
    stateBroadcastPending = true;
}
setInterval(() => {
    if (stateBroadcastPending) {
        stateBroadcastPending = false;
        sendStateToAll();
    }
}, 100);

// Positionen werden nicht mehr bei jeder einzelnen Bewegung in die DB
// geschrieben (siehe move-Handler), sondern gebündelt alle 5 Sekunden -
// nur für Spieler, die sich seitdem tatsächlich bewegt haben
setInterval(async () => {
    for (const [playerId, p] of playersOnline) {
        if (!p.positionDirty) continue;
        p.positionDirty = false;
        try {
            await db.query("UPDATE players SET x=?, y=? WHERE id=?", [p.state.x, p.state.y, playerId]);
        } catch (err) {
            // EINE fehlgeschlagene Speicherung darf nie den ganzen Server
            // abstürzen lassen (unbehandelte Promise-Ablehnung crasht in
            // neueren Node-Versionen den kompletten Prozess) - einfach
            // überspringen und beim nächsten Intervall erneut versuchen
            console.error(`[Positions-Speicherung] Fehler bei Spieler ${playerId}:`, err.message);
        }
    }
}, 5000);

function sendStateToAll() {
    const allPlayers = [];
    for (const [id, p] of playersOnline) {
        const rank = jobRanks.get(p.state.jobRankId);
        const jobType = rank ? (jobs.get(rank.jobId)?.type || "generic") : null;
        const jobName = rank ? (jobs.get(rank.jobId)?.name || null) : null;

        const shirtResolved = resolveClothingSlot(p.state.shirtItemId, p.state.shirtColor || "#3498db");
        const pantsResolved = resolveClothingSlot(p.state.pantsItemId, p.state.pantsColor || "#2c3e50");
        const shoesResolved = resolveClothingSlot(p.state.shoesItemId, p.state.shoesColor || "#1a1a1a");
        // Helm hat keine Flächenfarbe als Fallback - ohne Helm wird einfach nichts gezeichnet
        const helmetItem = p.state.helmetItemId ? clothingItems.get(p.state.helmetItemId) : null;

        // Sicherheitsnetz: falls irgendwo im Code money/bank versehentlich als Text
        // landet (z.B. durch einen fehlenden Number()), hier zur Anzeige korrigieren
        // und gleich am state selbst reparieren, damit sich der Fehler nicht weiter aufschaukelt
        p.state.money = round2(Number(p.state.money) || 0);
        p.state.bank = round2(Number(p.state.bank) || 0);

        allPlayers.push({
            id: id,
            username: p.username,
            x: p.state.x,
            y: p.state.y,
            world: p.state.world,
            money: p.state.money,
            bank: p.state.bank,
            health: p.state.health,
            hunger: p.state.hunger,
            thirst: p.state.thirst,
            inventory: p.state.inventory,
            hasPackage: p.state.inventory.some(i => JOB_CARRY_ITEMS.includes(i.id) && i.amount > 0),
            color: p.state.color || "#f1c40f",
            skin: p.state.skin || "none",
            skinImage: (p.state.skinItemId && skinItems.get(p.state.skinItemId)) ? skinItems.get(p.state.skinItemId).image : null,
            wantedLevel: p.state.wantedLevel || 0,
            cuffed: !!p.state.cuffed,
            inSafeZone: isInSafeZone(p.state.world, p.state.x, p.state.y),
            bounty: bounties.get(id) || 0,
            title: p.state.activeTitleAchievementId ? (achievementTitles.get(p.state.activeTitleAchievementId) || null) : null,
            onDuty: !!p.state.onDuty,
            shirtColor: shirtResolved.color,
            shirtImage: shirtResolved.image,
            pantsColor: pantsResolved.color,
            pantsImage: pantsResolved.image,
            shoesColor: shoesResolved.color,
            shoesImage: shoesResolved.image,
            helmetImage: helmetItem ? helmetItem.image : null,
            gangTag: p.state.gangTag || null,
            gangColor: p.state.gangColor || null,
            xp: p.state.xp || 0,
            level: p.state.level || 1,
            jobType,
            jobName
        });
    }
    const msg = JSON.stringify({
        type: "state",
        players: allPlayers
    });

    for (const [, p] of playersOnline) {
        p.ws.send(msg);
    }
}

function round2(value) {
    return Math.round(value * 100) / 100;
}

async function useItem(p, itemId) {
    const invItem = p.state.inventory.find(i => i.id === itemId);
    if (!invItem) {
        return { msg: "Item nicht im Inventar gefunden." };
    }

    const [rows] = await db.query("SELECT * FROM items WHERE id=?", [itemId]);
    if (rows.length === 0) {
        return { msg: "Item existiert nicht in der Datenbank." };
    }

    const itemData = rows[0];

    p.state.hunger = Number(p.state.hunger) || 0;
    p.state.thirst = Number(p.state.thirst) || 0;
    p.state.health = Number(p.state.health) || 100;

    const restore = Number(itemData.restore) || 0;

    if (itemData.type === "food") {
        p.state.hunger = round2(Math.min(100, p.state.hunger + restore));
    }

    if (itemData.type === "drink") {
        p.state.thirst = round2(Math.min(100, p.state.thirst + restore));
    }

    if (itemData.type === "heal") {
        p.state.health = round2(Math.min(100, p.state.health + restore));
    }

    let fuelResultMsg = null;
    if (itemData.type === "fuel") {
        // Nächstes Auto in der Nähe finden (egal ob eigenes oder fremdes - hilft auch anderen aus)
        let nearestCar = null;
        let nearestDist = Infinity;
        for (const [, c] of cars) {
            if (c.world !== p.state.world) continue;
            if (c.isNpc) continue;
            const dist = Math.hypot(c.x - p.state.x, c.y - p.state.y);
            if (dist < 60 && dist < nearestDist) {
                nearestDist = dist;
                nearestCar = c;
            }
        }

        if (!nearestCar) {
            return { msg: "Kein Auto in der Nähe zum Auftanken." };
        }

        const cfg = carConfigs[nearestCar.model] || carConfigs.sedan || {};
        const tankSize = cfg.tankSize || 50;

        if ((nearestCar.fuel ?? 0) >= tankSize - 0.05) {
            return { msg: "Dieses Auto ist bereits voll." };
        }

        nearestCar.fuel = Math.min(tankSize, (nearestCar.fuel ?? 0) + restore);
        await db.query("UPDATE cars SET fuel=? WHERE id=?", [nearestCar.fuel, nearestCar.id]);
        sendCarsToAll();
        fuelResultMsg = `Auto aufgetankt: +${restore}L (jetzt ${nearestCar.fuel.toFixed(1)}/${tankSize}L)`;
    }

    invItem.amount--;
    if (invItem.amount <= 0) {
        p.state.inventory = p.state.inventory.filter(i => i.id !== itemId);
    }

    return {
        msg: fuelResultMsg || `${itemData.name} benutzt.`,
        hunger: p.state.hunger,
        thirst: p.state.thirst,
        health: p.state.health,
        inventory: p.state.inventory
    };
}

// -------------------------------------------------------------
// INTERAKTION (Türen, NPCs, Shops (DB), ATMs (Map), Objekte)
// -------------------------------------------------------------
async function handleInteraction(player, playerId, data) {
    const map = maps[player.state.world];
    if (!map) return;

    const px = player.state.x;
    const py = player.state.y;

    // 1. Türen prüfen
    if (map.doors) {
        const door = map.doors.find(d =>
            Math.abs(d.x - px) < 16 &&
            Math.abs(d.y - py) < 16
        );

        if (door) {
            if (!maps[door.targetMap]) return;

            player.state.world = door.targetMap;
            player.state.x = door.targetX * 32;
            player.state.y = door.targetY * 32;

            db.query(
                "UPDATE players SET world=?, x=?, y=? WHERE id=?",
                [player.state.world, player.state.x, player.state.y, playerId]
            );

            sendStateToAll();
            player.ws.send(JSON.stringify({
                type: "map_data",
                tiles: maps[player.state.world].tiles,
                tileRot: maps[player.state.world].tileRot || null,
                doors: maps[player.state.world].doors,
                objects: maps[player.state.world].objects,
                shops: getShopsForWorld(player.state.world),
                garages: getGaragesForWorld(player.state.world),
                jobcenters: getJobsForWorld(player.state.world),
                gasStations: getGasStationsForWorld(player.state.world),
                repairShops: getRepairShopsForWorld(player.state.world),
                jobPoints: getJobPointsForPlayer(player.state.world, player.state.jobRankId),
                houses: getHousesForWorld(player.state.world),
                taxiStands: getTaxiStandsForWorld(player.state.world),
                hospitals: getHospitalsForWorld(player.state.world),
                prisons: getPrisonsForWorld(player.state.world),
                territoryZones: getZonesForWorld(player.state.world),
                impoundLots: getImpoundLotsForWorld(player.state.world),
                fireStations: getFireStationsForWorld(player.state.world),
                harvestSpots: getHarvestSpotsForWorld(player.state.world),
                clothingShops: getClothingShopsForWorld(player.state.world),
                insuranceOffices: getInsuranceOfficesForWorld(player.state.world),
                plateOffices: getPlateOfficesForWorld(player.state.world),
                trailerShops: getTrailerShopsForWorld(player.state.world),
highwayLinks: getHighwayLinksForWorld(player.state.world),
blackMarketSpots: getBlackMarketSpotsForWorld(player.state.world),
roadblocks: getRoadblocksForWorld(player.state.world),
                groundDrops: getGroundDropsForWorld(player.state.world),
                processSpots: getProcessSpotsForWorld(player.state.world),
                dealerSpots: getActiveDealerSpotsForWorld(player.state.world),
                fires: getFiresForWorld(player.state.world),
                atms: maps[player.state.world].atms || [],
                spawn: maps[player.state.world].spawn
            }));

            return;
        }
    }

    // 2. NPCs prüfen
    if (map.npcs) {
        const npc = map.npcs.find(n =>
            Math.abs(n.x - px) < 32 &&
            Math.abs(n.y - py) < 32
        );

        if (npc) {
            player.ws.send(JSON.stringify({
                type: "npc_dialog",
                npcId: npc.id,
                text: npc.dialog
            }));
            return;
        }
    }

    // 3. Shops prüfen (jetzt aus der DB statt aus der Map-Datei)
    const shop = findShopNear(player.state.world, px, py);
    if (shop) {
        trackOpenLocationWindow(player, "shopWindow");
        player.ws.send(JSON.stringify({
            type: "shop_open",
            shopId: shop.id,
            items: shop.items,
            isOwner: shop.owner_id === playerId,
            purchasePrice: shop.owner_id ? null : shop.purchase_price,
            ownerId: shop.owner_id || null
        }));
        return;
    }

    // 2b. Boden-Fund prüfen (Zufalls-Event, z.B. verlorene LKW-Ladung)
    for (const [dropId, drop] of groundDrops) {
        if (drop.world !== player.state.world) continue;
        if (Math.hypot(drop.x - px, drop.y - py) > 40) continue;

        const gained = await addItemToInventory(playerId, drop.itemId, drop.amount);
        if (gained <= 0) return; // Fehlermeldung kommt schon aus addItemToInventory

        groundDrops.delete(dropId);
        broadcastGroundDropUpdate(drop.world);

        player.ws.send(JSON.stringify({ type: "shop_info", msg: `📦 Aufgesammelt: ${gained}x ${drop.label}` }));
        return;
    }

    // 3a. Kleidungsladen prüfen
    const clothingShop = findClothingShopNear(player.state.world, px, py);
    if (clothingShop) {
        trackOpenLocationWindow(player, "clothingBox");
        player.ws.send(JSON.stringify({
            type: "clothing_shop_open",
            catalog: {
                shirt: getClothingCatalogBySlot("shirt"),
                pants: getClothingCatalogBySlot("pants"),
                shoes: getClothingCatalogBySlot("shoes"),
                helmet: getClothingCatalogBySlot("helmet")
            }
        }));
        return;
    }

    // 3a2. Anhänger-Shop prüfen
    const trailerShop = findTrailerShopNear(player.state.world, px, py);
    if (trailerShop) {
        const catalog = Object.entries(carConfigs)
            .filter(([, cfg]) => cfg.isTrailerModel && cfg.trailerPrice)
            .map(([model, cfg]) => ({ model, price: cfg.trailerPrice, image: cfg.image || null }));

        player.ws.send(JSON.stringify({ type: "trailer_shop_open", catalog }));
        trackOpenLocationWindow(player, "trailerShopBox");
        return;
    }

    // 3b. ATMs prüfen (weiterhin aus der Map-Datei)
    if (map.atms && Array.isArray(map.atms)) {
        const atm = map.atms.find(a =>
            Math.abs(a.x * 32 - player.state.x) < 32 &&
            Math.abs(a.y * 32 - player.state.y) < 32
        );

        if (atm) {
            trackOpenLocationWindow(player, "atmBox");
            player.ws.send(JSON.stringify({
                type: "atm_open",
                atmId: atm.id,
                money: player.state.money,
                bank: player.state.bank
            }));
            return;
        }
    }

    // 3c. Garagen prüfen
    const garage = findGarageNear(player.state.world, px, py);
    if (garage) {
        if (garage.job_id) {
            // Job-Garage: nur für Spieler mit passendem Job, zeigt Job-Fuhrpark statt private Autos
            const rank = jobRanks.get(player.state.jobRankId);
            if (!rank || rank.jobId !== garage.job_id) {
                player.ws.send(JSON.stringify({ type: "shop_error", msg: "Diese Garage gehört zu einem Job, den du nicht ausübst." }));
                return;
            }
            const [rows] = await db.query(
                "SELECT id, model, world, is_stored FROM cars WHERE job_id=?",
                [garage.job_id]
            );
            player.ws.send(JSON.stringify({
                type: "garage_open",
                garageId: garage.id,
                cars: rows,
                isJobGarage: true
            }));
            trackOpenLocationWindow(player, "garageBox");
            return;
        }

        const [rows] = await db.query(
            "SELECT id, model, world, is_stored FROM cars WHERE owner_id=?",
            [playerId]
        );
        player.ws.send(JSON.stringify({
            type: "garage_open",
            garageId: garage.id,
            cars: rows,
            isJobGarage: false
        }));
        trackOpenLocationWindow(player, "garageBox");
        return;
    }

    // 3d. Jobcenter prüfen
    const jobcenter = findJobcenterNear(player.state.world, px, py);
    if (jobcenter) {
        trackOpenLocationWindow(player, "jobBox");
        player.ws.send(JSON.stringify({
            type: "jobcenter_open",
            jobcenterId: jobcenter.id,
            jobs: getJobsWithRanks(),
            currentRankId: player.state.jobRankId
        }));
        return;
    }

    // 3e. Häuser prüfen
    const house = findHouseNear(player.state.world, px, py);
    if (house) {
        if (!house.owner_id) {
            // Unbebaut: Kauffenster zeigen (kein Betreten nötig)
            player.ws.send(JSON.stringify({
                type: "house_open",
                houseId: house.id,
                name: house.name,
                price: house.price,
                owned: false,
                isOwner: false,
                hasKey: false,
                storage: []
            }));
            return;
        }

        let hasKey = house.owner_id === playerId;
        if (!hasKey) {
            const [rows] = await db.query(
                "SELECT 1 FROM house_keys WHERE house_id=? AND player_id=?",
                [house.id, playerId]
            );
            hasKey = rows.length > 0;
        }

        if (!hasKey) {
            if (house.rent_price && !house.renter_id) {
                player.ws.send(JSON.stringify({
                    type: "house_open",
                    houseId: house.id,
                    name: house.name,
                    owned: true,
                    isOwner: false,
                    hasKey: false,
                    forRent: true,
                    rentPrice: house.rent_price,
                    storage: []
                }));
                return;
            }
            player.ws.send(JSON.stringify({ type: "shop_error", msg: "🔒 Dieses Haus ist verschlossen." }));
            return;
        }

        // Betreten: Position draußen merken, ins Innere teleportieren
        const interiorKey = ensureHouseInteriorMap(house.id);
        if (!interiorKey) {
            player.ws.send(JSON.stringify({ type: "shop_error", msg: "Innenraum-Vorlage 'haus_innen' fehlt noch (im Map-Editor anlegen)." }));
            return;
        }

        player.houseReturn = { world: player.state.world, x: player.state.x, y: player.state.y };
        player.state.houseId = house.id;
        player.state.world = interiorKey;

        const spawn = maps[interiorKey].spawn || { x: 100, y: 100 };
        player.state.x = spawn.x;
        player.state.y = spawn.y;

        sendStateToAll();
        player.ws.send(JSON.stringify({
            type: "map_data",
            tiles: maps[interiorKey].tiles,
            tileRot: maps[interiorKey].tileRot || null,
            doors: maps[interiorKey].doors || [],
            objects: maps[interiorKey].objects || [],
            shops: [],
            garages: [],
            jobcenters: [],
            gasStations: [],
            repairShops: [],
            jobPoints: [],
            houses: [],
            atms: [],
            spawn: maps[interiorKey].spawn
        }));
        return;
    }

    // 3f. Job-Punkte prüfen (Item abholen/abliefern - Typ frei wählbar über item_id)
    const jobPoint = findJobPointNear(player.state.world, px, py);
    if (jobPoint) {
        const rank = jobRanks.get(player.state.jobRankId);
        const itemId = jobPoint.item_id || "package";

        if (!rank || rank.jobId !== jobPoint.job_id) {
            // Kein passender Job -> Punkt einfach ignorieren, kein Fehler nötig
        } else if (jobPoint.kind === "dropoff") {
            const invItem = player.state.inventory.find(i => i.id === itemId && i.amount > 0);
            if (!invItem) {
                player.ws.send(JSON.stringify({ type: "shop_error", msg: `Du hast nichts zum Abliefern dabei (auch nicht im Kofferraum eines Autos).` }));
            } else {
                invItem.amount--;
                if (invItem.amount <= 0) {
                    player.state.inventory = player.state.inventory.filter(i => i.id !== itemId);
                }
                player.state.money += jobPoint.reward;
                player.state.deliveryCount = (player.state.deliveryCount || 0) + 1;

                await db.query(
                    "UPDATE players SET money=?, inventory=?, delivery_count=? WHERE id=?",
                    [player.state.money, JSON.stringify(player.state.inventory), player.state.deliveryCount, playerId]
                );
                sendStateToAll();
                player.ws.send(JSON.stringify({ type: "shop_info", msg: `Abgeliefert! +${jobPoint.reward}$` }));
                await awardXp(playerId, player, 10, "Lieferung");
                await checkAchievements(playerId, player);
            }
        } else {
            // pickup
            const alreadyHas = player.state.inventory.some(i => i.id === itemId && i.amount > 0);
            if (alreadyHas) {
                player.ws.send(JSON.stringify({ type: "shop_error", msg: "Du trägst schon eins - erst abliefern." }));
            } else {
                await addItemToInventory(playerId, itemId, 1);
                sendStateToAll();
                player.ws.send(JSON.stringify({ type: "shop_info", msg: `Abgeholt (${jobPoint.name}) - bring es zum Lieferpunkt.` }));
            }
        }
        return;
    }

    // 3g. Taxi-Stände prüfen
    const taxiStand = findTaxiStandNear(player.state.world, px, py);
    if (taxiStand) {
        // Fall 1: Ich fahre gerade und habe einen zahlenden Taxi-Fahrgast -> Ziel erreicht
        if (player.drivingCarId) {
            const car = cars.get(player.drivingCarId);

            if (car && car.passengerId && car.taxiFare) {
                const fareAmount = TAXI_FARE;
                const passenger = playersOnline.get(car.passengerId);

                if (passenger) {
                    passenger.state.money = Math.max(0, passenger.state.money - fareAmount);
                    player.state.money += fareAmount;

                    await db.query("UPDATE players SET money=? WHERE id=?", [passenger.state.money, car.passengerId]);
                    await db.query("UPDATE players SET money=? WHERE id=?", [player.state.money, playerId]);

                    car.passengerId = null;
                    car.taxiFare = false;
                    passenger.state.x = car.x;
                    passenger.state.y = car.y + 30;
                    passenger.passengerCarId = null;

                    sendStateToAll();
                    sendCarsToAll();

                    passenger.ws.send(JSON.stringify({ type: "shop_info", msg: `Angekommen! Fahrt bezahlt: -${fareAmount}$` }));
                    player.ws.send(JSON.stringify({ type: "shop_info", msg: `Fahrt abgeschlossen: +${fareAmount}$` }));
                }
                return;
            }

            // Fall 2: Ich bin Taxifahrer (passender Job), fahre, habe keinen Fahrgast -> Warteliste zeigen
            if (isTaxiJobRank(player.state.jobRankId) && !car?.passengerId) {
                const requests = [...taxiWaiting.entries()].map(([pid, info]) => ({
                    playerId: pid,
                    username: playersOnline.get(pid)?.username || "?",
                    world: playersOnline.get(pid)?.state.world || "?"
                }));
                player.ws.send(JSON.stringify({ type: "taxi_requests", requests }));
                return;
            }
        }

        // Fall 3: Ich bin zu Fuß -> Taxi rufen oder Anfrage stornieren
        if (!player.drivingCarId && !player.passengerCarId) {
            if (taxiWaiting.has(playerId)) {
                taxiWaiting.delete(playerId);
                player.ws.send(JSON.stringify({ type: "shop_info", msg: "Taxi-Anfrage storniert." }));
            } else {
                taxiWaiting.set(playerId, { requestedAt: Date.now() });
                player.ws.send(JSON.stringify({ type: "shop_info", msg: "Taxi angefordert - warte, bis ein Fahrer dich abholt." }));
            }
            return;
        }
    }

    // 3h. Territoriums-Zonen prüfen (Bande erobert per Interaktion)
    const zone = findZoneNear(player.state.world, px, py);
    if (zone) {
        const gang = await getPlayerGang(playerId);
        if (!gang) {
            player.ws.send(JSON.stringify({ type: "shop_error", msg: "Du musst in einer Bande sein, um Territorium zu erobern." }));
            return;
        }
        if (zone.owner_gang_id === gang.id) {
            player.ws.send(JSON.stringify({ type: "shop_info", msg: `"${zone.name}" gehört bereits deiner Bande.` }));
            return;
        }

        await db.query("UPDATE territory_zones SET owner_gang_id=? WHERE id=?", [gang.id, zone.id]);
        await loadTerritoryZones();
        broadcastZoneUpdate(player.state.world);

        player.ws.send(JSON.stringify({ type: "shop_info", msg: `🚩 "${zone.name}" für "${gang.name}" erobert!` }));
        broadcastToAll(`🚩 Die Bande "${gang.name}" hat "${zone.name}" erobert!`, true);
        return;
    }

    // 3i. Drogen-Anbaustellen prüfen
    const harvestSpot = findHarvestSpotNear(player.state.world, px, py);
    if (harvestSpot) {
        const drug = drugTypes.get(harvestSpot.drug_id);
        if (!drug) return;

        const now = Date.now();
        const HARVEST_COOLDOWN = 20000;
        if (player.lastHarvestAt && now - player.lastHarvestAt < HARVEST_COOLDOWN) {
            const wait = Math.ceil((HARVEST_COOLDOWN - (now - player.lastHarvestAt)) / 1000);
            player.ws.send(JSON.stringify({ type: "shop_error", msg: `Warte noch ${wait}s bis zur nächsten Ernte.` }));
            return;
        }
        player.lastHarvestAt = now;

        const gained = await addItemToInventory(playerId, drug.raw_item_id, 1);
        if (gained <= 0) return;

        player.ws.send(JSON.stringify({ type: "shop_info", msg: `${harvestSpot.name}: 1x geerntet.` }));
        return;
    }

    // 3j. Drogen-Labore prüfen
    const processSpot = findProcessSpotNear(player.state.world, px, py);
    if (processSpot) {
        const drug = drugTypes.get(processSpot.drug_id);
        if (!drug) return;

        const rawItem = player.state.inventory.find(i => i.id === drug.raw_item_id && i.amount > 0);
        if (!rawItem) {
            player.ws.send(JSON.stringify({ type: "shop_error", msg: `Du hast keine Rohware für "${drug.name}" dabei.` }));
            return;
        }

        // Gewicht für das ENDPRODUKT vorher prüfen, bevor die Rohware
        // verbraucht wird - sonst wäre sie bei Ablehnung einfach weg
        if (getCarryableAmount(player.state.inventory, drug.product_item_id, 1) < 1) {
            player.ws.send(JSON.stringify({ type: "shop_error", msg: `Inventar zu schwer (${getInventoryWeight(player.state.inventory)}/${getMaxCarryWeight()}kg) - kann das Endprodukt nicht mehr tragen.` }));
            return;
        }

        rawItem.amount--;
        if (rawItem.amount <= 0) {
            player.state.inventory = player.state.inventory.filter(i => i.id !== drug.raw_item_id);
        }

        const productItem = player.state.inventory.find(i => i.id === drug.product_item_id);
        if (productItem) productItem.amount++;
        else player.state.inventory.push({ id: drug.product_item_id, amount: 1 });

        await db.query("UPDATE players SET inventory=? WHERE id=?", [JSON.stringify(player.state.inventory), playerId]);
        sendStateToAll();
        player.ws.send(JSON.stringify({ type: "shop_info", msg: `${processSpot.name}: 1x "${drug.name}" verarbeitet.` }));
        return;
    }

    // 3k. Aktive Drogen-Verkaufsstelle prüfen
    const dealerResult = findActiveDealerNear(player.state.world, px, py);
    if (dealerResult) {
        const { drugId, spot } = dealerResult;
        const drug = drugTypes.get(drugId);
        if (!drug) return;

        const productItem = player.state.inventory.find(i => i.id === drug.product_item_id && i.amount > 0);
        if (!productItem) {
            player.ws.send(JSON.stringify({ type: "shop_error", msg: `Du hast nichts von "${drug.name}" zum Verkaufen dabei.` }));
            return;
        }

        const amountSold = productItem.amount;
        const total = amountSold * spot.price;

        player.state.inventory = player.state.inventory.filter(i => i.id !== drug.product_item_id);
        player.state.money += total;
        player.state.wantedLevel = Math.min(5, (player.state.wantedLevel || 0) + 1);

        await db.query(
            "UPDATE players SET money=?, inventory=?, wanted_level=? WHERE id=?",
            [player.state.money, JSON.stringify(player.state.inventory), player.state.wantedLevel, playerId]
        );
        sendStateToAll();

        player.ws.send(JSON.stringify({
            type: "shop_info",
            msg: `Verkauft: ${amountSold}x "${drug.name}" für ${total}$! (${spot.price}$/Stück)`
        }));

        await logEvent("drug_sale", player.username, `${amountSold}x "${drug.name}" für ${total}$ verkauft`);

        broadcastCrimeAlert({
            crimeType: "Drogenverkauf",
            world: player.state.world,
            x: Math.round(player.state.x),
            y: Math.round(player.state.y)
        });

        // Nach jedem Verkauf: neuen Ort + Preis würfeln
        rotateDealerSpot(drugId, drug);
        return;
    }

    // 4. Interaktive Objekte (z.B. Kisten, Tore)
    if (map.objects) {
        const obj = map.objects.find(o => {
            const cfg = objectConfig[o.type];
            if (!cfg || !cfg.interactive) return false;

            const ox = o.x;
            const oy = o.y - (cfg.height - 32);
            const margin = 32;

            return px > ox - margin && px < ox + cfg.width + margin &&
                   py > oy - margin && py < oy + cfg.height + margin;
        });

        if (obj) {
            const action = (objectConfig[obj.type] || {}).action;

            // Tore reagieren bewusst NICHT mehr auf E - nur noch auf Q (siehe gate_toggle_nearby)
            if (action === "toggle_gate") {
                return;
            }

            // Innerhalb eines Hauses: Kiste = Lager öffnen, Ausgang = Haus verlassen
            if (isHouseInteriorWorld(player.state.world)) {
                const houseId = player.state.houseId;
                const house = houseId ? houses.get(houseId) : null;

                if (action === "house_storage" && house) {
                    player.ws.send(JSON.stringify({
                        type: "house_open",
                        houseId: house.id,
                        name: house.name,
                        price: house.price,
                        owned: true,
                        isOwner: house.owner_id === playerId,
                        hasKey: true,
                        storage: JSON.parse(house.storage || "[]")
                    }));
                    return;
                }

                if (action === "house_wardrobe" && house) {
                    player.ws.send(JSON.stringify({
                        type: "wardrobe_open",
                        houseId: house.id,
                        wardrobe: JSON.parse(house.wardrobe || "[]")
                    }));
                    return;
                }

                if (action === "job_duty") {
                    const rank = jobRanks.get(player.state.jobRankId);
                    const jobDef = rank ? jobs.get(rank.jobId) : null;

                    if (!jobDef) {
                        player.ws.send(JSON.stringify({ type: "shop_error", msg: "Du hast keinen Job." }));
                        return;
                    }

                    if (!player.state.onDuty) {
                        // Dienst antreten: aktuelle Kleidung merken, Uniform anziehen
                        player.state.preDutyShirtId = player.state.shirtItemId || null;
                        player.state.preDutyPantsId = player.state.pantsItemId || null;
                        player.state.preDutyShoesId = player.state.shoesItemId || null;
                        player.state.preDutyHelmetId = player.state.helmetItemId || null;

                        if (jobDef.uniformShirtId) player.state.shirtItemId = jobDef.uniformShirtId;
                        if (jobDef.uniformPantsId) player.state.pantsItemId = jobDef.uniformPantsId;
                        if (jobDef.uniformShoesId) player.state.shoesItemId = jobDef.uniformShoesId;
                        if (jobDef.uniformHelmetId) player.state.helmetItemId = jobDef.uniformHelmetId;
                        player.state.onDuty = true;
                        player.state.fmsStatus = 2; // Einsatzbereit Wache

                        await db.query(
                            `UPDATE players SET on_duty=1, pre_duty_shirt_id=?, pre_duty_pants_id=?, pre_duty_shoes_id=?, pre_duty_helmet_id=?,
                             shirt_item_id=?, pants_item_id=?, shoes_item_id=?, helmet_item_id=? WHERE id=?`,
                            [player.state.preDutyShirtId, player.state.preDutyPantsId, player.state.preDutyShoesId, player.state.preDutyHelmetId,
                             player.state.shirtItemId, player.state.pantsItemId, player.state.shoesItemId, player.state.helmetItemId, playerId]
                        );
                        sendStateToAll();
                        broadcastLeitstelleUpdate();
                        player.ws.send(JSON.stringify({ type: "shop_info", msg: `🛡️ Dienst angetreten (${jobDef.name}) - Uniform angezogen.` }));
                    } else {
                        // Dienst beenden: alte Kleidung wiederherstellen
                        player.state.shirtItemId = player.state.preDutyShirtId || null;
                        player.state.pantsItemId = player.state.preDutyPantsId || null;
                        player.state.shoesItemId = player.state.preDutyShoesId || null;
                        player.state.helmetItemId = player.state.preDutyHelmetId || null;
                        player.state.preDutyShirtId = null;
                        player.state.preDutyPantsId = null;
                        player.state.preDutyShoesId = null;
                        player.state.preDutyHelmetId = null;
                        player.state.onDuty = false;
                        player.state.fmsStatus = null;

                        await db.query(
                            `UPDATE players SET on_duty=0, pre_duty_shirt_id=NULL, pre_duty_pants_id=NULL, pre_duty_shoes_id=NULL, pre_duty_helmet_id=NULL,
                             shirt_item_id=?, pants_item_id=?, shoes_item_id=?, helmet_item_id=? WHERE id=?`,
                            [player.state.shirtItemId, player.state.pantsItemId, player.state.shoesItemId, player.state.helmetItemId, playerId]
                        );
                        sendStateToAll();
                        broadcastLeitstelleUpdate();
                        player.ws.send(JSON.stringify({ type: "shop_info", msg: "Dienst beendet - eigene Kleidung wieder an." }));
                    }
                    return;
                }

                if (action === "house_exit") {
                    const back = player.houseReturn || { world: "stadt", x: 100, y: 100 };
                    player.state.world = back.world;
                    player.state.x = back.x;
                    player.state.y = back.y;
                    player.state.houseId = null;
                    player.houseReturn = null;

                    await db.query("UPDATE players SET world=?, x=?, y=? WHERE id=?",
                        [back.world, back.x, back.y, playerId]);

                    sendStateToAll();
                    const outsideMap = maps[back.world];
                    player.ws.send(JSON.stringify({
                        type: "map_data",
                        tiles: outsideMap.tiles,
                        tileRot: outsideMap.tileRot || null,
                        doors: outsideMap.doors || [],
                        objects: outsideMap.objects || [],
                        shops: getShopsForWorld(back.world),
                        garages: getGaragesForWorld(back.world),
                        jobcenters: getJobsForWorld(back.world),
                        gasStations: getGasStationsForWorld(back.world),
                        repairShops: getRepairShopsForWorld(back.world),
                        jobPoints: getJobPointsForPlayer(back.world, player.state.jobRankId),
                        houses: getHousesForWorld(back.world),
                        taxiStands: getTaxiStandsForWorld(back.world),
                        hospitals: getHospitalsForWorld(back.world),
                        prisons: getPrisonsForWorld(back.world),
                        territoryZones: getZonesForWorld(back.world),
                        impoundLots: getImpoundLotsForWorld(back.world),
                        fireStations: getFireStationsForWorld(back.world),
                        harvestSpots: getHarvestSpotsForWorld(back.world),
                        clothingShops: getClothingShopsForWorld(back.world),
                        insuranceOffices: getInsuranceOfficesForWorld(back.world),
                        plateOffices: getPlateOfficesForWorld(back.world),
                        trailerShops: getTrailerShopsForWorld(back.world),
highwayLinks: getHighwayLinksForWorld(back.world),
blackMarketSpots: getBlackMarketSpotsForWorld(back.world),
roadblocks: getRoadblocksForWorld(back.world),
                        groundDrops: getGroundDropsForWorld(back.world),
                        processSpots: getProcessSpotsForWorld(back.world),
                        dealerSpots: getActiveDealerSpotsForWorld(back.world),
                        fires: getFiresForWorld(back.world),
                        atms: outsideMap.atms || [],
                        spawn: outsideMap.spawn
                    }));
                    return;
                }
            }

            player.ws.send(JSON.stringify({
                type: "object_interact",
                objectId: obj.id,
                action
            }));
            return;
        }
    }
}

// -------------------------------------------------------------
// WEBSOCKET MULTIPLAYER
// -------------------------------------------------------------
wss.on("connection", ws => {
    let playerId = null;

    // Einfaches Rate-Limiting pro Verbindung: schützt vor Flood/Missbrauch und
    // unnötiger Serverlast, ohne normales Spielen (60 FPS Bewegung usw.) zu stören
    let msgWindowStart = Date.now();
    let msgCount = 0;
    const MAX_MSGS_PER_SECOND = 100;

    ws.on("message", async msg => {
        let data;
        try {
            data = JSON.parse(msg);
        } catch {
            return;
        }

        const now = Date.now();
        if (now - msgWindowStart > 1000) {
            msgWindowStart = now;
            msgCount = 0;
        }
        msgCount++;
        if (msgCount > MAX_MSGS_PER_SECOND) {
            return; // Nachricht still verwerfen, keine Antwort nötig
        }

        // AUTH
        if (data.type === "auth") {
            try {
                const payload = jwt.verify(data.token, JWT_SECRET);
                playerId = payload.id;
                ws.playerId = playerId;

                if (getSiteConfig("maintenance_mode") === "1" && !payload.isAdmin) {
                    ws.send(JSON.stringify({ type: "auth_error", msg: "🔧 Der Server ist gerade im Wartungsmodus. Bitte versuch es später erneut." }));
                    ws.close();
                    return;
                }

                if (getSiteConfig("beta_mode_enabled") === "1" && !payload.isAdmin && !payload.isBetaTester) {
                    ws.send(JSON.stringify({ type: "auth_error", msg: "🧪 Der Zugang ist aktuell auf Beta-Tester beschränkt. Frag einen Admin, um freigeschaltet zu werden." }));
                    ws.close();
                    return;
                }

                // Falls für diesen Spieler noch eine alte Verbindung besteht
                // (z.B. altes Tab nicht geschlossen, oder Reconnect nach
                // Server-Neustart) - sauber ablösen statt stillschweigend
                // überschreiben zu lassen
                const oldEntry = playersOnline.get(playerId);
                if (oldEntry) {
                    if (oldEntry.drivingCarId) {
                        const oldCar = cars.get(oldEntry.drivingCarId);
                        if (oldCar) oldCar.driverId = null;
                    }
                    if (oldEntry.positionDirty) {
                        await db.query("UPDATE players SET x=?, y=?, world=? WHERE id=?", [oldEntry.state.x, oldEntry.state.y, oldEntry.state.world, playerId]);
                    }
                    try { oldEntry.ws.close(); } catch {}
                    playersOnline.delete(playerId);
                }

                const [rows] = await db.query(
                    "SELECT world, x, y, health, hunger, thirst, money, bank, inventory, job_rank_id, color, skin, skin_item_id, wanted_level, pvp_enabled, jail_until, muted_until, xp, level, delivery_count, robbery_count, playtime_seconds, shirt_color, pants_color, shoes_color, shirt_item_id, pants_item_id, shoes_item_id, helmet_item_id, on_duty, pre_duty_shirt_id, pre_duty_pants_id, pre_duty_shoes_id, pre_duty_helmet_id, active_title_achievement_id, permission_group_id, is_beta_tester FROM players WHERE id = ?",
                    [playerId]
                );

                const row = rows[0];

                playersOnline.set(playerId, {
                    ws,
                    username: payload.username,
                    isAdmin: !!payload.isAdmin,
                    permissionGroupId: row.permission_group_id || null,
                    drivingCarId: null,
                    state: {
                        world: row.world,
                        x: row.x,
                        y: row.y,
                        health: row.health,
                        hunger: row.hunger,
                        thirst: row.thirst,
                        inventory: JSON.parse(row.inventory || "[]"),
                        money: Number(row.money) || 0,
                        bank: Number(row.bank) || 0,
                        jobRankId: row.job_rank_id ?? null,
                        color: row.color || "#f1c40f",
                        skin: row.skin || "none",
                        wantedLevel: row.wanted_level || 0,
                        pvpEnabled: !!row.pvp_enabled,
                        jailedUntil: row.jail_until ? new Date(row.jail_until).getTime() : null,
                        gangTag: null,
                        gangColor: null,
                        mutedUntil: row.muted_until ? new Date(row.muted_until).getTime() : null,
                        xp: row.xp || 0,
                        level: row.level || 1,
                        deliveryCount: row.delivery_count || 0,
                        robberyCount: row.robbery_count || 0,
                        playtimeSeconds: row.playtime_seconds || 0,
                        shirtColor: row.shirt_color || "#3498db",
                        pantsColor: row.pants_color || "#2c3e50",
                        shoesColor: row.shoes_color || "#1a1a1a",
                        shirtItemId: row.shirt_item_id || null,
                        pantsItemId: row.pants_item_id || null,
                        shoesItemId: row.shoes_item_id || null,
                        helmetItemId: row.helmet_item_id || null,
                        onDuty: !!row.on_duty,
                        preDutyShirtId: row.pre_duty_shirt_id || null,
                        preDutyPantsId: row.pre_duty_pants_id || null,
                        preDutyShoesId: row.pre_duty_shoes_id || null,
                        preDutyHelmetId: row.pre_duty_helmet_id || null,
                        skinItemId: row.skin_item_id || null,
                        activeTitleAchievementId: row.active_title_achievement_id || null
                    }
                });

                ws.send(JSON.stringify({
                    type: "auth_ok",
                    id: playerId,
                    username: payload.username,
                    isAdmin: !!payload.isAdmin,
                    isBetaTester: !!row.is_beta_tester,
                    maxInventoryWeight: getMaxCarryWeight(),
                    maps
                }));

                maybeUpdateDiscordOnlineStatus();

                ws.send(JSON.stringify({ type: "world_info", hour: gameHour, weather: currentWeather }));
                ws.send(JSON.stringify({ type: "event_update", events: activeEvents }));

                ws.send(JSON.stringify({ type: "jail_status", jailedUntil: playersOnline.get(playerId).state.jailedUntil }));

                await refreshPlayerGangInfo(playerId);

                const currentMap = maps[row.world];

                ws.send(JSON.stringify({
                    type: "map_data",
                    tiles: currentMap.tiles,
                    tileRot: currentMap.tileRot || null,
                    doors: currentMap.doors,
                    objects: currentMap.objects,
                    shops: getShopsForWorld(row.world),
                    garages: getGaragesForWorld(row.world),
                    jobcenters: getJobsForWorld(row.world),
                    gasStations: getGasStationsForWorld(row.world),
                    repairShops: getRepairShopsForWorld(row.world),
                    jobPoints: getJobPointsForPlayer(row.world, row.job_rank_id),
                    houses: getHousesForWorld(row.world),
                    taxiStands: getTaxiStandsForWorld(row.world),
                    hospitals: getHospitalsForWorld(row.world),
                    prisons: getPrisonsForWorld(row.world),
                    territoryZones: getZonesForWorld(row.world),
                    impoundLots: getImpoundLotsForWorld(row.world),
                    fireStations: getFireStationsForWorld(row.world),
                    harvestSpots: getHarvestSpotsForWorld(row.world),
                    clothingShops: getClothingShopsForWorld(row.world),
                    insuranceOffices: getInsuranceOfficesForWorld(row.world),
                    plateOffices: getPlateOfficesForWorld(row.world),
                    trailerShops: getTrailerShopsForWorld(row.world),
highwayLinks: getHighwayLinksForWorld(row.world),
blackMarketSpots: getBlackMarketSpotsForWorld(row.world),
roadblocks: getRoadblocksForWorld(row.world),
                    groundDrops: getGroundDropsForWorld(row.world),
                    processSpots: getProcessSpotsForWorld(row.world),
                    dealerSpots: getActiveDealerSpotsForWorld(row.world),
                    fires: getFiresForWorld(row.world),
                    atms: currentMap.atms || [],
                    spawn: currentMap.spawn
                }));

                sendStateToAll();
                sendCarsToAll();
            } catch {
                ws.close();
            }
            return;
        }

        // Ab hier: Spieler muss eingeloggt sein
        if (!playerId) return;
        const p = playersOnline.get(playerId);
        if (!p) return;

        // -----------------------------------------------------
        // BANK / ATM
        // -----------------------------------------------------
        if (data.type === "bank_pin_check") {
            const [rows] = await db.query("SELECT pin FROM players WHERE id=?", [playerId]);

            if (rows.length && Number(rows[0].pin) === Number(data.pin)) {
                p.ws.send(JSON.stringify({ type: "bank_pin_ok" }));
            } else {
                p.ws.send(JSON.stringify({ type: "bank_pin_fail" }));
            }
        }

        if (data.type === "bank_set_pin") {
            const newPin = String(data.newPin || "").trim();
            const oldPin = String(data.oldPin || "").trim();

            if (!/^\d{4}$/.test(newPin)) {
                p.ws.send(JSON.stringify({ type: "bank_pin_set_result", ok: false, msg: "Die neue PIN muss genau 4 Ziffern haben." }));
                return;
            }

            const [rows] = await db.query("SELECT pin FROM players WHERE id=?", [playerId]);
            const currentPin = rows.length ? rows[0].pin : null;

            // Falls noch nie eine PIN gesetzt wurde, reicht die neue PIN allein.
            // Sonst muss die bisherige PIN korrekt bestätigt werden.
            if (currentPin !== null && currentPin !== undefined && String(currentPin) !== "" &&
                Number(currentPin) !== Number(oldPin)) {
                p.ws.send(JSON.stringify({ type: "bank_pin_set_result", ok: false, msg: "Aktuelle PIN stimmt nicht." }));
                return;
            }

            await db.query("UPDATE players SET pin=? WHERE id=?", [newPin, playerId]);
            p.ws.send(JSON.stringify({ type: "bank_pin_set_result", ok: true, msg: "PIN erfolgreich geändert." }));
        }

        if (data.type === "bank_open") {
            p.ws.send(JSON.stringify({
                type: "bank_open",
                money: p.state.money,
                bank: p.state.bank
            }));
        }

        if (data.type === "bank_deposit") {
            const amount = Math.max(0, Number(data.amount));
            if (p.state.money >= amount) {
                p.state.money -= amount;
                p.state.bank += amount;

                await db.query("UPDATE players SET money=?, bank=? WHERE id=?",
                    [p.state.money, p.state.bank, playerId]);

                p.ws.send(JSON.stringify({
                    type: "bank_update",
                    money: p.state.money,
                    bank: p.state.bank
                }));
            }
        }

        if (data.type === "bank_withdraw") {
            const amount = Math.max(0, Number(data.amount));
            if (p.state.bank >= amount) {
                p.state.bank -= amount;
                p.state.money += amount;

                await db.query("UPDATE players SET money=?, bank=? WHERE id=?",
                    [p.state.money, p.state.bank, playerId]);

                p.ws.send(JSON.stringify({
                    type: "bank_update",
                    money: p.state.money,
                    bank: p.state.bank
                }));
            }
        }

        if (data.type === "bank_transfer") {
            const targetId = Number(data.targetId);
            const amount = Math.max(0, Number(data.amount));

            const target = playersOnline.get(targetId);
            if (target && p.state.bank >= amount) {
                p.state.bank -= amount;
                target.state.bank += amount;

                await db.query("UPDATE players SET bank=? WHERE id=?", [p.state.bank, playerId]);
                await db.query("UPDATE players SET bank=? WHERE id=?", [target.state.bank, targetId]);

                p.ws.send(JSON.stringify({
                    type: "bank_update",
                    money: p.state.money,
                    bank: p.state.bank
                }));

                target.ws.send(JSON.stringify({
                    type: "bank_update",
                    money: target.state.money,
                    bank: target.state.bank
                }));
            }
        }

        // -----------------------------------------------------
        // SHOP KAUFEN (DB-basiert, inkl. Autos)
        // -----------------------------------------------------
        // -----------------------------------------------------
        // BÖRSE: kaufen / verkaufen
        // -----------------------------------------------------
        if (data.type === "stock_buy") {
            const stock = stocks.get(Number(data.stockId));
            const shares = Math.floor(Number(data.shares));
            if (!stock || !shares || shares < 1) return;

            const cost = Math.round(Number(stock.price) * shares * 100) / 100;
            if (p.state.money < cost) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: `Nicht genug Geld (${cost}$ nötig).` }));
                return;
            }

            p.state.money -= cost;
            await db.query("UPDATE players SET money=? WHERE id=?", [p.state.money, playerId]);
            await db.query(
                "INSERT INTO player_stocks (player_id, stock_id, shares) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE shares = shares + ?",
                [playerId, stock.id, shares, shares]
            );

            sendStateToAll();
            p.ws.send(JSON.stringify({ type: "shop_info", msg: `${shares}x ${stock.symbol} gekauft für ${cost}$.` }));
        }

        if (data.type === "stock_sell") {
            const stock = stocks.get(Number(data.stockId));
            const shares = Math.floor(Number(data.shares));
            if (!stock || !shares || shares < 1) return;

            const [rows] = await db.query("SELECT shares FROM player_stocks WHERE player_id=? AND stock_id=?", [playerId, stock.id]);
            const owned = rows.length ? rows[0].shares : 0;
            if (owned < shares) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: `Du hast nur ${owned}x ${stock.symbol}.` }));
                return;
            }

            const proceeds = Math.round(Number(stock.price) * shares * 100) / 100;
            p.state.money += proceeds;
            await db.query("UPDATE players SET money=? WHERE id=?", [p.state.money, playerId]);
            await db.query("UPDATE player_stocks SET shares = shares - ? WHERE player_id=? AND stock_id=?", [shares, playerId, stock.id]);
            await db.query("DELETE FROM player_stocks WHERE player_id=? AND stock_id=? AND shares<=0", [playerId, stock.id]);

            sendStateToAll();
            p.ws.send(JSON.stringify({ type: "shop_info", msg: `${shares}x ${stock.symbol} verkauft für ${proceeds}$.` }));
        }

        if (data.type === "shop_set_price") {
            const shop = findShopNear(p.state.world, p.state.x, p.state.y);
            if (!shop) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du musst dazu in deinem Shop stehen." }));
                return;
            }
            if (shop.owner_id !== playerId) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Das ist nicht dein Shop." }));
                return;
            }

            const item = shop.items.find(i => i.id === data.itemId);
            if (!item) return;

            const newPrice = Math.round(Number(data.price));
            if (isNaN(newPrice) || newPrice < 1 || newPrice > 50000) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Preis muss zwischen 1$ und 50.000$ liegen." }));
                return;
            }

            item.price = newPrice;
            await db.query("UPDATE shop_items SET price=? WHERE shop_id=? AND item_id=?", [newPrice, shop.id, data.itemId]);

            p.ws.send(JSON.stringify({ type: "shop_price_updated", itemId: data.itemId, price: newPrice }));
        }

        if (data.type === "shop_buy") {
            const shop = findShopNear(p.state.world, p.state.x, p.state.y);
            if (!shop) return;

            const item = shop.items.find(i => i.id === data.itemId);
            if (!item) return;

            const discountPct = getActiveDiscountPercent();
            const finalPrice = discountPct > 0 ? round2(item.price * (1 - discountPct / 100)) : item.price;

            if (p.state.money < finalPrice) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Zu wenig Geld!" }));
                return;
            }

            // Gewicht vor dem Geldabzug prüfen (bei Fahrzeugen egal, die
            // landen ja nicht im Inventar) - so muss bei Ablehnung nichts
            // zurückerstattet werden
            if (item.type !== "car" && getCarryableAmount(p.state.inventory, item.id, 1) < 1) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: `Inventar zu schwer (${getInventoryWeight(p.state.inventory)}/${getMaxCarryWeight()}kg) - kann das nicht mehr tragen.` }));
                return;
            }

            p.state.money -= finalPrice;

            // Falls der Shop einen Besitzer hat, bekommt dieser einen Anteil vom (rabattierten) Verkaufspreis
            if (shop.owner_id && shop.owner_id !== playerId) {
                const ownerCut = Math.round(finalPrice * (getSetting("shop_owner_cut_percent") / 100));
                await adjustBank(shop.owner_id, ownerCut);
            }

            if (item.type === "car") {
                const cfg = carConfigs[item.model] || carConfigs.sedan || {};
                const startFuel = cfg.tankSize || 50;

                const [result] = await db.query(
                    "INSERT INTO cars (owner_id, model, world, x, y, angle, fuel, health) VALUES (?, ?, ?, ?, ?, 0, ?, 100)",
                    [playerId, item.model || "sedan", p.state.world, p.state.x + 40, p.state.y, startFuel]
                );
                await db.query("INSERT IGNORE INTO car_keys (car_id, player_id) VALUES (?, ?)", [result.insertId, playerId]);
                cars.set(result.insertId, {
                    id: result.insertId,
                    ownerId: playerId,
                    model: item.model || "sedan",
                    world: p.state.world,
                    x: p.state.x + 40,
                    y: p.state.y,
                    angle: 0,
                    speed: 0,
                    driverId: null,
                    throttle: 0,
                    steer: 0,
                    fuel: startFuel,
                    headlights: false,
                    leftBlinker: false,
                    rightBlinker: false,
                    brakeLight: false,
                    hazard: false,
                    health: 100,
                    trunk: [],
                    passengerId: null
                });
                sendCarsToAll();
                p.ws.send(JSON.stringify({ type: "shop_info", msg: "Auto gekauft! Steht neben dir, voll getankt." }));
            } else {
                const invItem = p.state.inventory.find(i => i.id === item.id);
                if (invItem) invItem.amount++;
                else p.state.inventory.push({ id: item.id, amount: 1 });

                await db.query(
                    "UPDATE players SET inventory=? WHERE id=?",
                    [JSON.stringify(p.state.inventory), playerId]
                );
            }

            await db.query("UPDATE players SET money=? WHERE id=?", [p.state.money, playerId]);
            sendStateToAll();
        }

        // -----------------------------------------------------
        // TÜR BENUTZEN
        // -----------------------------------------------------
        if (data.type === "use_door") {
            const currentMap = maps[p.state.world];
            if (!currentMap || !currentMap.doors) return;

            const door = currentMap.doors.find(d =>
                Math.abs(d.x - data.x) < 10 &&
                Math.abs(d.y - data.y) < 10
            );

            if (!door) return;
            if (!maps[door.targetMap]) return;

            p.state.world = door.targetMap;
            p.state.x = door.targetX * 32;
            p.state.y = door.targetY * 32;

            await db.query(
                "UPDATE players SET world = ?, x = ?, y = ? WHERE id = ?",
                [p.state.world, p.state.x, p.state.y, playerId]
            );

            sendStateToAll();
        }

        // -----------------------------------------------------
        // INTERAKTION
        // -----------------------------------------------------
        if (data.type === "interact") {
            if (p.state.jailedUntil && Date.now() < p.state.jailedUntil) {
                const remain = Math.ceil((p.state.jailedUntil - Date.now()) / 1000);
                p.ws.send(JSON.stringify({ type: "shop_error", msg: `Du sitzt noch ${remain}s im Gefängnis.` }));
                return;
            }
            await handleInteraction(p, playerId, data);
        }

        // -----------------------------------------------------
        // ITEM BENUTZEN
        // -----------------------------------------------------
        if (data.type === "use_item") {
            const result = await useItem(p, data.itemId);

            const hunger = Math.max(0, Number(p.state.hunger) || 0);
            const thirst = Math.max(0, Number(p.state.thirst) || 0);
            const health = Math.max(0, Number(p.state.health) || 100);

            await db.query(
                "UPDATE players SET inventory=?, hunger=?, thirst=?, health=? WHERE id=?",
                [
                    JSON.stringify(p.state.inventory),
                    round2(p.state.hunger),
                    round2(p.state.thirst),
                    round2(p.state.health),
                    playerId
                ]
            );

            p.ws.send(JSON.stringify({
                type: "item_used",
                msg: result.msg,
                hunger,
                thirst,
                health,
                inventory: p.state.inventory
            }));

            sendStateToAll();
        }

        // -----------------------------------------------------
        // BEWEGUNG (nur zu Fuß – im Auto läuft das über car_control)
        // -----------------------------------------------------
        // -----------------------------------------------------
        // PORTAL/FAHRSTUHL-TELEPORT: rein clientseitig erkannt (Server
        // kennt keine 3D-Portale/Fahrstühle), setzt Position wie bei
        // Türen DIREKT (umgeht bewusst die Bewegungs-Anti-Cheat-Prüfung
        // von "move", da ein legitimer Sprung sonst als Cheat abgelehnt
        // würde) - Cooldown gegen Missbrauch/Spam
        // -----------------------------------------------------
        if (data.type === "portal_teleport") {
            const now = Date.now();
            const TELEPORT_COOLDOWN = 800;
            if (p.lastPortalTeleportAt && now - p.lastPortalTeleportAt < TELEPORT_COOLDOWN) return;
            p.lastPortalTeleportAt = now;

            const nx = Number(data.x);
            const ny = Number(data.y);
            if (!Number.isFinite(nx) || !Number.isFinite(ny)) return;

            p.state.x = nx;
            p.state.y = ny;
            p.positionDirty = true;
        }

        if (data.type === "move") {
            if (p.drivingCarId) return; // im Auto keine Fuß-Bewegung annehmen
            if (p.state.jailedUntil && Date.now() < p.state.jailedUntil) return; // in Haft
            if (p.state.cuffed) return; // in Handschellen, kann sich nicht bewegen

            const nx = data.x;
            const ny = data.y;

            const oldX = p.state.x;
            const oldY = p.state.y;

            // ANTI-CHEAT: Plausibilitätsprüfung der Bewegungsgeschwindigkeit.
            // Der Client läuft mit 5px pro ~16ms-Tick (~310px/s), großzügiger
            // Puffer für Ruckler/Lag oben drauf.
            const now = Date.now();
            const elapsedSec = p.lastMoveTime ? Math.max(0.001, (now - p.lastMoveTime) / 1000) : 0.05;
            p.lastMoveTime = now;

            const dist = Math.hypot(nx - oldX, ny - oldY);
            const MAX_WALK_SPEED = 450; // px/s, > tatsächliche ~310px/s
            const maxDist = MAX_WALK_SPEED * elapsedSec + 15; // + Toleranzpuffer

            if (dist > maxDist) {
                p.cheatFlags = (p.cheatFlags || 0) + 1;
                console.warn(
                    `[Anti-Cheat] Spieler ${playerId} (${p.username}): verdächtige Bewegung ` +
                    `${dist.toFixed(0)}px in ${elapsedSec.toFixed(3)}s (erlaubt: ${maxDist.toFixed(0)}px), ` +
                    `Zähler: ${p.cheatFlags}`
                );
                // Bewegung ablehnen - Client bekommt beim nächsten regulären
                // State-Broadcast automatisch die korrekte Serverposition zurück
                return;
            }

            if (canMove(p, nx, ny, p.username)) {
                p.state.x = nx;
                p.state.y = ny;
                p.positionDirty = true; // wird periodisch gebündelt in die DB geschrieben, nicht bei jeder einzelnen Bewegung
                checkOpenWindowDistance(p);

                const highwayLink = findHighwayLinkNear(p.state.world, p.state.x, p.state.y);
                if (highwayLink) {
                    teleportPlayerViaHighway(p, playerId, highwayLink);
                    return;
                }

                if (oldX !== p.state.x || oldY !== p.state.y) {
                    scheduleStateBroadcast();
                }
            }
        }

        // -----------------------------------------------------
        // MAP WECHSEL
        // -----------------------------------------------------
        if (data.type === "change_map") {
            const mapName = data.map;
            if (!maps[mapName]) return;

            p.state.world = mapName;
            p.state.x = maps[mapName].spawn.x;
            p.state.y = maps[mapName].spawn.y;

            await db.query(
                "UPDATE players SET world = ?, x = ?, y = ? WHERE id = ?",
                [mapName, p.state.x, p.state.y, playerId]
            );

            sendStateToAll();
            p.ws.send(JSON.stringify({
                type: "map_data",
                tiles: maps[p.state.world].tiles,
                tileRot: maps[p.state.world].tileRot || null,
                doors: maps[p.state.world].doors,
                objects: maps[p.state.world].objects,
                shops: getShopsForWorld(p.state.world),
                garages: getGaragesForWorld(p.state.world),
                jobcenters: getJobsForWorld(p.state.world),
                gasStations: getGasStationsForWorld(p.state.world),
                repairShops: getRepairShopsForWorld(p.state.world),
                jobPoints: getJobPointsForPlayer(p.state.world, p.state.jobRankId),
                houses: getHousesForWorld(p.state.world),
                taxiStands: getTaxiStandsForWorld(p.state.world),
                hospitals: getHospitalsForWorld(p.state.world),
                prisons: getPrisonsForWorld(p.state.world),
                territoryZones: getZonesForWorld(p.state.world),
                impoundLots: getImpoundLotsForWorld(p.state.world),
                fireStations: getFireStationsForWorld(p.state.world),
                harvestSpots: getHarvestSpotsForWorld(p.state.world),
                clothingShops: getClothingShopsForWorld(p.state.world),
                insuranceOffices: getInsuranceOfficesForWorld(p.state.world),
                plateOffices: getPlateOfficesForWorld(p.state.world),
                trailerShops: getTrailerShopsForWorld(p.state.world),
highwayLinks: getHighwayLinksForWorld(p.state.world),
blackMarketSpots: getBlackMarketSpotsForWorld(p.state.world),
roadblocks: getRoadblocksForWorld(p.state.world),
                groundDrops: getGroundDropsForWorld(p.state.world),
                processSpots: getProcessSpotsForWorld(p.state.world),
                dealerSpots: getActiveDealerSpotsForWorld(p.state.world),
                fires: getFiresForWorld(p.state.world),
                atms: maps[p.state.world].atms || [],
                spawn: maps[p.state.world].spawn
            }));
        }

        // -----------------------------------------------------
        // AUTOS: EINSTEIGEN / AUSSTEIGEN / STEUERN
        // -----------------------------------------------------
        if (data.type === "car_enter") {
            if (p.state.jailedUntil && Date.now() < p.state.jailedUntil) return;
            if (p.state.cuffed) return; // in Handschellen, kann nicht selbst einsteigen

            let freeCar = null;
            let carWithFreeSeat = null;

            for (const [, c] of cars) {
                if (c.isTrailer) continue; // Anhänger haben keinen Fahrersitz
                if (c.loadedOnTrailerId) continue; // festgezurrt, kein Einsteigen möglich
                if (c.towedByCarId) continue; // wird gerade gezogen, kein Einsteigen möglich
                if (c.world !== p.state.world) continue;
                if (Math.abs(c.x - p.state.x) >= 40 || Math.abs(c.y - p.state.y) >= 40) continue;

                if (!c.driverId) { freeCar = c; break; }
                if (!c.passengerId) carWithFreeSeat = c;
            }

            if (freeCar) {
                let hasAccess = freeCar.ownerId === playerId;

                // Job-Fahrzeug: Zugriff, wenn der Spieler aktuell in diesem Job ist
                if (!hasAccess && freeCar.jobId) {
                    const rank = jobRanks.get(p.state.jobRankId);
                    if (rank && rank.jobId === freeCar.jobId) hasAccess = true;
                }

                // Privater Schlüssel eines anderen Spielers
                if (!hasAccess) {
                    const [rows] = await db.query(
                        "SELECT 1 FROM car_keys WHERE car_id=? AND player_id=?",
                        [freeCar.id, playerId]
                    );
                    hasAccess = rows.length > 0;
                }

                // Admin im Dienstmodus darf immer fahren
                if (!hasAccess && p.isAdmin && p.adminDuty) hasAccess = true;

                if (!hasAccess) {
                    p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du hast keinen Schlüssel für dieses Auto." }));
                    return;
                }

                freeCar.driverId = playerId;
                freeCar.isNpc = false;
                p.drivingCarId = freeCar.id;
                sendCarsToAll();
            } else if (carWithFreeSeat) {
                // Mitfahren braucht keinen Schlüssel
                carWithFreeSeat.passengerId = playerId;
                p.passengerCarId = carWithFreeSeat.id;
                sendCarsToAll();
            }
        }

        // Auto-Schlüssel an einen anderen Spieler geben/entziehen
        if (data.type === "car_give_key" || data.type === "car_revoke_key") {
            if (!p.drivingCarId) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du musst dazu im Auto sitzen." }));
                return;
            }
            const c = cars.get(p.drivingCarId);
            if (!c || c.ownerId !== playerId) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Nur der Besitzer kann Schlüssel verwalten." }));
                return;
            }

            const [targetRows] = await db.query("SELECT id FROM players WHERE username=? OR id=?", [data.username, data.username]);
            if (targetRows.length === 0) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Spieler nicht gefunden." }));
                return;
            }
            const targetId = targetRows[0].id;

            if (data.type === "car_give_key") {
                await db.query("INSERT IGNORE INTO car_keys (car_id, player_id) VALUES (?, ?)", [c.id, targetId]);
                p.ws.send(JSON.stringify({ type: "shop_info", msg: `Schlüssel an ${data.username} übergeben.` }));
            } else {
                await db.query("DELETE FROM car_keys WHERE car_id=? AND player_id=?", [c.id, targetId]);
                p.ws.send(JSON.stringify({ type: "shop_info", msg: `Schlüssel von ${data.username} entzogen.` }));
            }
        }

        if (data.type === "car_exit") {
            if (!p.drivingCarId) return;

            const c = cars.get(p.drivingCarId);
            if (c) {
                c.driverId = null;
                c.throttle = 0;
                c.steer = 0;
                p.state.x = c.x;
                p.state.y = c.y + 30;

                await db.query("UPDATE cars SET x=?, y=?, angle=?, world=?, fuel=?, health=?, trunk=?, odometer=? WHERE id=?",
                    [c.x, c.y, c.angle, c.world, c.fuel ?? 0, c.health ?? 100, JSON.stringify(c.trunk || []), c.odometer || 0, c.id]);
            }
            p.drivingCarId = null;
            sendCarsToAll();
            sendStateToAll();
        }

        // E-Taste im Auto: direkt einparken, falls vor einer eigenen Garage,
        // sonst normal aussteigen
        if (data.type === "car_exit_or_park") {
            if (!p.drivingCarId) return;

            const c = cars.get(p.drivingCarId);
            if (!c) {
                p.drivingCarId = null;
                return;
            }

            const garage = findGarageNear(c.world, c.x, c.y);

            if (garage && c.ownerId === playerId) {
                // Direkt einparken
                await db.query(
                    "UPDATE cars SET is_stored=1, x=?, y=?, angle=?, fuel=?, health=?, trunk=?, odometer=? WHERE id=?",
                    [c.x, c.y, c.angle, c.fuel ?? 0, c.health ?? 100, JSON.stringify(c.trunk || []), c.odometer || 0, c.id]
                );

                p.state.x = c.x;
                p.state.y = c.y + 30;
                p.drivingCarId = null;

                cars.delete(c.id);
                sendCarsToAll();
                sendStateToAll();
                p.ws.send(JSON.stringify({ type: "garage_info", msg: "Auto eingeparkt." }));
                return;
            }

            // Normales Aussteigen
            c.driverId = null;
            c.throttle = 0;
            c.steer = 0;
            p.state.x = c.x;
            p.state.y = c.y + 30;

            await db.query("UPDATE cars SET x=?, y=?, angle=?, world=?, fuel=?, health=?, trunk=?, odometer=? WHERE id=?",
                [c.x, c.y, c.angle, c.world, c.fuel ?? 0, c.health ?? 100, JSON.stringify(c.trunk || []), c.odometer || 0, c.id]);

            p.drivingCarId = null;
            sendCarsToAll();
            sendStateToAll();
        }

        // Q-Taste: Tor in Reichweite öffnen/schließen (zu Fuß oder im Auto,
        // damit man z.B. durchfahren kann ohne anzuhalten)
        if (data.type === "gate_toggle_nearby") {
            await toggleNearestGate(p, playerId, true); // still, keine Fehlermeldung falls kein Tor da
        }

        // Q-Taste im Auto: an Tankstelle tanken, an Werkstatt reparieren
        // (unabhängig vom Ein-/Aussteigen)
        if (data.type === "car_use_station") {
            if (!p.drivingCarId) return;

            const c = cars.get(p.drivingCarId);
            if (!c) return;

            const gasStation = findGasStationNear(c.world, c.x, c.y);
            const cfg = carConfigs[c.model] || carConfigs.sedan || {};
            const tankSize = cfg.tankSize || 50;

            if (gasStation && (c.fuel ?? 0) < tankSize - 0.05) {
                const discountPct = getActiveDiscountPercent();
                const effectivePrice = discountPct > 0 ? gasStation.price * (1 - discountPct / 100) : gasStation.price;

                const needed = tankSize - (c.fuel ?? 0);
                const maxAffordable = p.state.money / effectivePrice;
                const liters = Math.max(0, Math.min(needed, maxAffordable));

                if (liters <= 0) {
                    p.ws.send(JSON.stringify({ type: "shop_error", msg: "Nicht genug Geld zum Tanken." }));
                    return;
                }

                const cost = round2(liters * effectivePrice);
                p.state.money -= cost;
                c.fuel = (c.fuel ?? 0) + liters;

                // Falls die Tankstelle einen Besitzer hat, bekommt dieser einen Anteil
                if (gasStation.owner_id && gasStation.owner_id !== playerId) {
                    await adjustBank(gasStation.owner_id, Math.round(cost * (getSetting("gas_owner_cut_percent") / 100)));
                }

                await db.query("UPDATE players SET money=? WHERE id=?", [p.state.money, playerId]);
                await db.query("UPDATE cars SET fuel=? WHERE id=?", [c.fuel, c.id]);

                sendStateToAll();
                sendCarsToAll();
                p.ws.send(JSON.stringify({
                    type: "shop_info",
                    msg: `Getankt: ${liters.toFixed(1)}L für ${cost.toFixed(2)}$`
                }));
                return;
            }

            const repairShop = findRepairShopNear(c.world, c.x, c.y);

            if (repairShop && (c.health ?? 100) < 99.5) {
                const wasTotalLoss = (c.health ?? 100) <= 0;
                const missing = 100 - (c.health ?? 100);

                let pricePerPoint = repairShop.price_per_point;
                if (c.insured && wasTotalLoss) {
                    pricePerPoint = pricePerPoint * (1 - getSetting("insurance_repair_discount_percent") / 100);
                }

                const maxAffordablePoints = p.state.money / pricePerPoint;
                const points = Math.max(0, Math.min(missing, maxAffordablePoints));

                if (points <= 0) {
                    p.ws.send(JSON.stringify({ type: "shop_error", msg: "Nicht genug Geld für die Reparatur." }));
                    return;
                }

                const cost = points * pricePerPoint;
                p.state.money -= cost;
                c.health = Math.min(100, (c.health ?? 100) + points);

                await db.query("UPDATE players SET money=? WHERE id=?", [p.state.money, playerId]);
                await db.query("UPDATE cars SET health=? WHERE id=?", [c.health, c.id]);

                sendStateToAll();
                sendCarsToAll();
                p.ws.send(JSON.stringify({
                    type: "shop_info",
                    msg: (c.insured && wasTotalLoss)
                        ? `Repariert (Versicherung greift bei Totalschaden!): +${points.toFixed(0)} Zustand für ${cost.toFixed(2)}$`
                        : `Repariert: +${points.toFixed(0)} Zustand für ${cost.toFixed(2)}$`
                }));
                return;
            }

            // Still bleiben, wenn weder Tankstelle noch Werkstatt in der Nähe ist - Q löst
            // parallel auch das Tor-Umschalten aus, das soll nicht mit einer Fehlermeldung
            // zugespammt werden, wenn man eigentlich nur ans Tor wollte
        }

        // Verbindungsqualität: Client schickt seinen eigenen Zeitstempel,
        // Server gibt ihn unverändert zurück - Client misst die
        // Laufzeit selbst (kein synchronisiertes Zeitgefühl nötig)
        if (data.type === "ping") {
            p.ws.send(JSON.stringify({ type: "pong", t: data.t }));
            return;
        }

        // -----------------------------------------------------
        // FALLSCHADEN: der Client erkennt Stürze selbst (Höhe wird
        // server-seitig nicht getrackt), meldet nur die gefallene Höhe.
        // Server berechnet den Schaden daraus - bewusst grob geprüft
        // (Plausibilitäts-Deckel), da der Client hier die Wahrheit
        // vorgibt und ein manipulierter Client theoretisch falsche
        // Werte senden könnte
        // -----------------------------------------------------
        if (data.type === "fall_damage") {
            if (p.state.health <= 0) return;
            const height = Math.min(10, Math.max(0, Number(data.height) || 0));
            if (height < 1.2) return; // zu gering, um ernsthaft wehzutun

            const damage = Math.round((height - 1.0) * 15); // ~7-135, je nach Höhe
            const wasAlive = p.state.health > 0;
            p.state.health = Math.max(0, p.state.health - damage);
            await db.query("UPDATE players SET health=? WHERE id=?", [p.state.health, playerId]);
            sendStateToAll();

            p.ws.send(JSON.stringify({ type: "shop_error", msg: `💥 Du bist gestürzt und hast dir wehgetan! (-${damage} Leben)` }));

            if (wasAlive && p.state.health <= 0 && !p.isDying) {
                p.isDying = true;
                await killPlayer(playerId, p, null);
            }
        }

        if (data.type === "car_control") {
            if (!p.drivingCarId) return;

            const c = cars.get(p.drivingCarId);
            if (!c) return;

            c.throttle = Math.max(-1, Math.min(1, Number(data.throttle) || 0));
            c.steer = Math.max(-1, Math.min(1, Number(data.steer) || 0));
        }

        // Blinker links/rechts und Licht per Tastendruck umschalten
        if (data.type === "car_toggle") {
            if (!p.drivingCarId) return;

            const c = cars.get(p.drivingCarId);
            if (!c) return;

            if (data.what === "left") {
                c.leftBlinker = !c.leftBlinker;
                if (c.leftBlinker) c.rightBlinker = false;
            }
            if (data.what === "right") {
                c.rightBlinker = !c.rightBlinker;
                if (c.rightBlinker) c.leftBlinker = false;
            }
            if (data.what === "headlights") {
                c.headlights = !c.headlights;
            }
            if (data.what === "hazard") {
                c.hazard = !c.hazard;
                if (c.hazard) {
                    c.leftBlinker = false;
                    c.rightBlinker = false;
                }
            }
            if (data.what === "emergency" && c.jobId) {
                c.emergency = !c.emergency;
                db.query("UPDATE cars SET emergency=? WHERE id=?", [c.emergency ? 1 : 0, c.id]);
            }

            sendCarsToAll();
        }

        // -----------------------------------------------------
        // KOFFERRAUM
        // -----------------------------------------------------
        if (data.type === "trunk_open") {
            let car = null;

            if (p.drivingCarId) {
                car = cars.get(p.drivingCarId);
            } else {
                for (const [, c] of cars) {
                    if (c.world !== p.state.world || c.ownerId !== playerId) continue;
                    if (Math.abs(c.x - p.state.x) < 50 && Math.abs(c.y - p.state.y) < 50) {
                        car = c;
                        break;
                    }
                }
            }

            if (!car || car.ownerId !== playerId) {
                p.ws.send(JSON.stringify({ type: "trunk_error", msg: "Kein eigenes Auto in der Nähe." }));
                return;
            }

            p.openTrunkCarId = car.id;
            p.ws.send(JSON.stringify({
                type: "trunk_open",
                carId: car.id,
                trunk: car.trunk || [],
                inventory: p.state.inventory
            }));
        }

        if (data.type === "trunk_store") {
            const car = cars.get(p.openTrunkCarId);
            if (!car || car.ownerId !== playerId) return;

            const itemId = data.itemId;
            const amount = Math.max(1, Number(data.amount) || 1);

            const invItem = p.state.inventory.find(i => i.id === itemId);
            if (!invItem || invItem.amount < amount) return;

            invItem.amount -= amount;
            if (invItem.amount <= 0) {
                p.state.inventory = p.state.inventory.filter(i => i.id !== itemId);
            }

            car.trunk = car.trunk || [];
            const trunkItem = car.trunk.find(i => i.id === itemId);
            if (trunkItem) trunkItem.amount += amount;
            else car.trunk.push({ id: itemId, amount });

            await db.query("UPDATE players SET inventory=? WHERE id=?",
                [JSON.stringify(p.state.inventory), playerId]);
            await db.query("UPDATE cars SET trunk=? WHERE id=?",
                [JSON.stringify(car.trunk), car.id]);

            sendStateToAll();
            p.ws.send(JSON.stringify({ type: "trunk_open", carId: car.id, trunk: car.trunk, inventory: p.state.inventory }));
        }

        if (data.type === "trunk_take") {
            const car = cars.get(p.openTrunkCarId);
            if (!car || car.ownerId !== playerId) return;

            const itemId = data.itemId;
            const amount = Math.max(1, Number(data.amount) || 1);

            car.trunk = car.trunk || [];
            const trunkItem = car.trunk.find(i => i.id === itemId);
            if (!trunkItem || trunkItem.amount < amount) return;

            const carryable = getCarryableAmount(p.state.inventory, itemId, amount);
            if (carryable < amount) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: `Zu schwer - du kannst nur ${carryable}x davon tragen (${getInventoryWeight(p.state.inventory)}/${getMaxCarryWeight()}kg).` }));
                if (carryable <= 0) return;
            }
            const actualAmount = Math.min(amount, carryable);

            trunkItem.amount -= actualAmount;
            if (trunkItem.amount <= 0) {
                car.trunk = car.trunk.filter(i => i.id !== itemId);
            }

            const invItem = p.state.inventory.find(i => i.id === itemId);
            if (invItem) invItem.amount += actualAmount;
            else p.state.inventory.push({ id: itemId, amount: actualAmount });

            await db.query("UPDATE players SET inventory=? WHERE id=?",
                [JSON.stringify(p.state.inventory), playerId]);
            await db.query("UPDATE cars SET trunk=? WHERE id=?",
                [JSON.stringify(car.trunk), car.id]);

            sendStateToAll();
            p.ws.send(JSON.stringify({ type: "trunk_open", carId: car.id, trunk: car.trunk, inventory: p.state.inventory }));
        }

        // -----------------------------------------------------
        // MITFAHRER (Passagier)
        // -----------------------------------------------------
        if (data.type === "car_enter_passenger") {
            let nearestCar = null;
            for (const [, c] of cars) {
                if (c.world !== p.state.world || !c.driverId || c.passengerId) continue;
                if (Math.abs(c.x - p.state.x) < 40 && Math.abs(c.y - p.state.y) < 40) {
                    nearestCar = c;
                    break;
                }
            }
            if (!nearestCar) return;

            nearestCar.passengerId = playerId;
            p.passengerCarId = nearestCar.id;
            sendCarsToAll();
        }

        if (data.type === "car_exit_passenger") {
            if (!p.passengerCarId) return;
            if (p.state.cuffed) return; // Gefangene können nicht selbst aussteigen, nur der Beamte holt sie raus

            const c = cars.get(p.passengerCarId);
            if (c && c.passengerId === playerId) {
                c.passengerId = null;
                p.state.x = c.x;
                p.state.y = c.y + 30;
            }
            p.passengerCarId = null;
            sendCarsToAll();
            sendStateToAll();
        }

        // -----------------------------------------------------
        // GARAGE: EINLAGERN / HERAUSHOLEN
        // -----------------------------------------------------
        if (data.type === "garage_store") {
            const carId = Number(data.carId);
            const c = cars.get(carId);
            if (!c || c.driverId) return;

            let allowed = c.ownerId === playerId;
            if (!allowed && c.jobId) {
                const rank = jobRanks.get(p.state.jobRankId);
                allowed = !!(rank && rank.jobId === c.jobId);
            }
            if (!allowed) return;

            await db.query("UPDATE cars SET is_stored=1, x=?, y=?, angle=?, fuel=?, health=?, trunk=?, odometer=? WHERE id=?",
                [c.x, c.y, c.angle, c.fuel ?? 0, c.health ?? 100, JSON.stringify(c.trunk || []), c.odometer || 0, carId]);

            cars.delete(carId); // aus der RAM-Welt entfernen
            sendCarsToAll();
            p.ws.send(JSON.stringify({ type: "garage_info", msg: c.jobId ? "Job-Fahrzeug zurückgegeben." : "Auto eingelagert." }));

            if (c.jobId) {
                const [refreshed] = await db.query("SELECT id, model, world, is_stored FROM cars WHERE job_id=?", [c.jobId]);
                p.ws.send(JSON.stringify({ type: "garage_open", cars: refreshed, isJobGarage: true }));
            } else {
                const [refreshed] = await db.query("SELECT id, model, world, is_stored FROM cars WHERE owner_id=?", [playerId]);
                p.ws.send(JSON.stringify({ type: "garage_open", cars: refreshed, isJobGarage: false }));
            }
        }

        if (data.type === "garage_retrieve") {
            const carId = Number(data.carId);
            const garage = findGarageNear(p.state.world, p.state.x, p.state.y);
            if (!garage) return;

            let rows;
            if (garage.job_id) {
                const rank = jobRanks.get(p.state.jobRankId);
                if (!rank || rank.jobId !== garage.job_id) return;
                [rows] = await db.query(
                    "SELECT * FROM cars WHERE id=? AND job_id=? AND is_stored=1",
                    [carId, garage.job_id]
                );
            } else {
                [rows] = await db.query(
                    "SELECT * FROM cars WHERE id=? AND owner_id=? AND is_stored=1",
                    [carId, playerId]
                );
            }
            if (rows.length === 0) return;

            const row = rows[0];
            const nx = garage.x * 32 + 40;
            const ny = garage.y * 32;

            await db.query(
                "UPDATE cars SET is_stored=0, world=?, x=?, y=?, angle=0 WHERE id=?",
                [p.state.world, nx, ny, carId]
            );

            cars.set(carId, {
                id: carId,
                ownerId: row.owner_id,
                model: row.model,
                world: p.state.world,
                x: nx,
                y: ny,
                angle: 0,
                speed: 0,
                driverId: null,
                throttle: 0,
                steer: 0,
                fuel: row.fuel ?? 0,
                headlights: false,
                leftBlinker: false,
                rightBlinker: false,
                brakeLight: false,
                hazard: false,
                health: row.health ?? 100,
                trunk: JSON.parse(row.trunk || "[]"),
                passengerId: null,
                jobId: row.job_id || null,
                emergency: false,
                odometer: row.odometer || 0
            });

            sendCarsToAll();
            p.ws.send(JSON.stringify({ type: "garage_info", msg: garage.job_id ? "Job-Fahrzeug ausgecheckt." : "Auto abgeholt." }));

            if (garage.job_id) {
                const [refreshed] = await db.query("SELECT id, model, world, is_stored FROM cars WHERE job_id=?", [garage.job_id]);
                p.ws.send(JSON.stringify({ type: "garage_open", cars: refreshed, isJobGarage: true }));
            } else {
                const [refreshed] = await db.query("SELECT id, model, world, is_stored FROM cars WHERE owner_id=?", [playerId]);
                p.ws.send(JSON.stringify({ type: "garage_open", cars: refreshed, isJobGarage: false }));
            }
        }

        // -----------------------------------------------------
        // JOBSYSTEM: bewerben / kündigen
        // -----------------------------------------------------
        if (data.type === "job_apply") {
            const rankId = Number(data.rankId);
            const rank = jobRanks.get(rankId);

            // Selbst-Bewerbung nur auf Einstiegsrang (level 1) möglich,
            // höhere Ränge vergibt nur ein Admin (Beförderung)
            if (!rank || rank.level !== 1) {
                p.ws.send(JSON.stringify({ type: "jobcenter_error", msg: "Diesen Job/Rang kannst du dir nicht selbst zuweisen." }));
                return;
            }

            const jobDef = jobs.get(rank.jobId);
            if (jobDef && jobDef.protected) {
                p.ws.send(JSON.stringify({
                    type: "jobcenter_error",
                    msg: `"${jobDef.name}" ist ein geschützter Job - du musst per /job invite von einem Mitglied eingeladen werden.`
                }));
                return;
            }

            p.state.jobRankId = rankId;
            await db.query("UPDATE players SET job_rank_id=? WHERE id=?", [rankId, playerId]);
            await checkAchievements(playerId, p);

            p.ws.send(JSON.stringify({
                type: "jobcenter_info",
                msg: `Job angenommen: ${jobs.get(rank.jobId)?.name || ""} - ${rank.title}`
            }));
            p.ws.send(JSON.stringify({
                type: "jobcenter_open",
                jobs: getJobsWithRanks(),
                currentRankId: p.state.jobRankId
            }));
        }

        if (data.type === "job_quit") {
            p.state.jobRankId = null;
            await db.query("UPDATE players SET job_rank_id=NULL WHERE id=?", [playerId]);
            p.ws.send(JSON.stringify({ type: "jobcenter_info", msg: "Job gekündigt." }));
            p.ws.send(JSON.stringify({
                type: "jobcenter_open",
                jobs: getJobsWithRanks(),
                currentRankId: null
            }));
        }

        // -----------------------------------------------------
        // HÄUSER
        // -----------------------------------------------------
        if (data.type === "house_buy") {
            const house = houses.get(Number(data.houseId));
            if (!house || house.owner_id) return;

            if (p.state.money < house.price) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Nicht genug Geld für dieses Haus." }));
                return;
            }

            p.state.money -= house.price;
            house.owner_id = playerId;

            await db.query("UPDATE players SET money=? WHERE id=?", [p.state.money, playerId]);
            await db.query("UPDATE houses SET owner_id=? WHERE id=?", [playerId, house.id]);
            await db.query("INSERT IGNORE INTO house_keys (house_id, player_id) VALUES (?, ?)", [house.id, playerId]);

            sendStateToAll();
            p.ws.send(JSON.stringify({ type: "shop_info", msg: `Haus "${house.name}" gekauft!` }));
        }

        // -----------------------------------------------------
        // IMMOBILIEN-VERMIETUNG
        // -----------------------------------------------------
        if (data.type === "house_set_rent") {
            const house = findHouseNear(p.state.world, p.state.x, p.state.y);
            if (!house || house.owner_id !== playerId) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du musst dazu vor deinem eigenen Haus stehen." }));
                return;
            }
            if (house.renter_id) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Erst die aktuelle Vermietung beenden, bevor du den Mietpreis änderst." }));
                return;
            }

            const rentPrice = Math.max(0, Math.floor(Number(data.rentPrice) || 0));
            house.rent_price = rentPrice || null;
            await db.query("UPDATE houses SET rent_price=? WHERE id=?", [house.rent_price, house.id]);

            p.ws.send(JSON.stringify({
                type: "shop_info",
                msg: rentPrice > 0 ? `Haus "${house.name}" wird jetzt für ${rentPrice}$ pro Intervall vermietet.` : `Vermietungs-Angebot für "${house.name}" zurückgezogen.`
            }));
        }

        if (data.type === "house_rent") {
            const house = findHouseNear(p.state.world, p.state.x, p.state.y);
            if (!house || !house.rent_price) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du musst vor einem Haus stehen, das zur Miete angeboten wird." }));
                return;
            }
            if (house.owner_id === playerId) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du kannst dein eigenes Haus nicht mieten." }));
                return;
            }
            if (house.renter_id) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Dieses Haus ist bereits vermietet." }));
                return;
            }
            if (p.state.money < house.rent_price) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: `Nicht genug Geld (${house.rent_price}$ nötig für die erste Miete).` }));
                return;
            }

            const intervalMinutes = getSetting("house_rent_interval_minutes");
            const dueAt = new Date(Date.now() + intervalMinutes * 60000);

            p.state.money -= house.rent_price;
            await db.query("UPDATE players SET money=? WHERE id=?", [p.state.money, playerId]);

            const owner = playersOnline.get(house.owner_id);
            if (owner) {
                owner.state.bank += house.rent_price;
                await db.query("UPDATE players SET bank=? WHERE id=?", [owner.state.bank, owner.id]);
                owner.ws.send(JSON.stringify({ type: "shop_info", msg: `${p.username} hat dein Haus "${house.name}" gemietet! +${house.rent_price}$ auf dein Bankkonto.` }));
            } else {
                await db.query("UPDATE players SET bank = bank + ? WHERE id=?", [house.rent_price, house.owner_id]);
            }

            house.renter_id = playerId;
            house.rent_due_at = dueAt;
            await db.query("UPDATE houses SET renter_id=?, rent_due_at=? WHERE id=?", [playerId, dueAt, house.id]);
            await db.query("INSERT IGNORE INTO house_keys (house_id, player_id) VALUES (?, ?)", [house.id, playerId]);

            sendStateToAll();
            p.ws.send(JSON.stringify({ type: "shop_info", msg: `Haus "${house.name}" gemietet! Nächste Miete in ${intervalMinutes} Minuten fällig.` }));
        }

        if (data.type === "house_end_rental") {
            // Funktioniert sowohl von drinnen (p.state.houseId gesetzt) als auch von
            // draußen vor dem Haus stehend
            let house = p.state.houseId ? houses.get(p.state.houseId) : null;
            if (!house) house = findHouseNear(p.state.world, p.state.x, p.state.y);
            if (!house || !house.renter_id) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Kein Mietverhältnis hier gefunden." }));
                return;
            }

            const isOwner = house.owner_id === playerId;
            const isRenter = house.renter_id === playerId;
            if (!isOwner && !isRenter) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Damit hast du nichts zu tun." }));
                return;
            }

            const formerRenterId = house.renter_id;
            house.renter_id = null;
            house.rent_due_at = null;
            await db.query("UPDATE houses SET renter_id=NULL, rent_due_at=NULL WHERE id=?", [house.id]);
            await db.query("DELETE FROM house_keys WHERE house_id=? AND player_id=?", [house.id, formerRenterId]);

            const renterOnline = playersOnline.get(formerRenterId);
            const ownerOnline = playersOnline.get(house.owner_id);
            const msg = isOwner ? `Du wurdest aus "${house.name}" gekündigt.` : `Du bist aus "${house.name}" ausgezogen.`;
            if (renterOnline && isOwner) renterOnline.ws.send(JSON.stringify({ type: "shop_info", msg }));
            if (ownerOnline && isRenter) ownerOnline.ws.send(JSON.stringify({ type: "shop_info", msg: `${p.username} ist aus "${house.name}" ausgezogen - wieder frei zur Vermietung.` }));

            p.ws.send(JSON.stringify({ type: "shop_info", msg: isOwner ? "Mieter gekündigt." : "Du bist ausgezogen." }));
        }

        if (data.type === "wardrobe_store" || data.type === "wardrobe_take") {
            const houseId = p.state.houseId;
            const house = houseId ? houses.get(houseId) : null;
            if (!house) return;

            let hasKey = house.owner_id === playerId;
            if (!hasKey && house.owner_id) {
                const [rows] = await db.query(
                    "SELECT 1 FROM house_keys WHERE house_id=? AND player_id=?",
                    [house.id, playerId]
                );
                hasKey = rows.length > 0;
            }
            if (!hasKey) return;

            const itemId = data.itemId;
            const amount = Math.max(1, Number(data.amount) || 1);
            let wardrobe = JSON.parse(house.wardrobe || "[]");

            if (data.type === "wardrobe_store") {
                const invItem = p.state.inventory.find(i => i.id === itemId);
                if (!invItem || invItem.amount < amount) return;

                invItem.amount -= amount;
                if (invItem.amount <= 0) {
                    p.state.inventory = p.state.inventory.filter(i => i.id !== itemId);
                }

                const wItem = wardrobe.find(i => i.id === itemId);
                if (wItem) wItem.amount += amount;
                else wardrobe.push({ id: itemId, amount });
            } else {
                const wItem = wardrobe.find(i => i.id === itemId);
                if (!wItem || wItem.amount < amount) return;

                const carryable = getCarryableAmount(p.state.inventory, itemId, amount);
                if (carryable < amount) {
                    p.ws.send(JSON.stringify({ type: "shop_error", msg: `Zu schwer - du kannst nur ${carryable}x davon tragen.` }));
                    if (carryable <= 0) return;
                }
                const actualAmount = Math.min(amount, carryable);

                wItem.amount -= actualAmount;
                if (wItem.amount <= 0) wardrobe = wardrobe.filter(i => i.id !== itemId);

                const invItem = p.state.inventory.find(i => i.id === itemId);
                if (invItem) invItem.amount += actualAmount;
                else p.state.inventory.push({ id: itemId, amount: actualAmount });
            }

            house.wardrobe = JSON.stringify(wardrobe);
            await db.query("UPDATE players SET inventory=? WHERE id=?",
                [JSON.stringify(p.state.inventory), playerId]);
            await db.query("UPDATE houses SET wardrobe=? WHERE id=?", [house.wardrobe, house.id]);

            sendStateToAll();
            p.ws.send(JSON.stringify({ type: "wardrobe_open", houseId: house.id, wardrobe }));
        }

        if (data.type === "house_store" || data.type === "house_take") {
            const houseId = p.state.houseId;
            const house = houseId ? houses.get(houseId) : null;
            if (!house) return;

            let hasKey = house.owner_id === playerId;
            if (!hasKey && house.owner_id) {
                const [rows] = await db.query(
                    "SELECT 1 FROM house_keys WHERE house_id=? AND player_id=?",
                    [house.id, playerId]
                );
                hasKey = rows.length > 0;
            }
            if (!hasKey) return;

            const itemId = data.itemId;
            const amount = Math.max(1, Number(data.amount) || 1);
            let storage = JSON.parse(house.storage || "[]");

            if (data.type === "house_store") {
                const invItem = p.state.inventory.find(i => i.id === itemId);
                if (!invItem || invItem.amount < amount) return;

                invItem.amount -= amount;
                if (invItem.amount <= 0) {
                    p.state.inventory = p.state.inventory.filter(i => i.id !== itemId);
                }

                const stItem = storage.find(i => i.id === itemId);
                if (stItem) stItem.amount += amount;
                else storage.push({ id: itemId, amount });
            } else {
                const stItem = storage.find(i => i.id === itemId);
                if (!stItem || stItem.amount < amount) return;

                const carryable = getCarryableAmount(p.state.inventory, itemId, amount);
                if (carryable < amount) {
                    p.ws.send(JSON.stringify({ type: "shop_error", msg: `Zu schwer - du kannst nur ${carryable}x davon tragen.` }));
                    if (carryable <= 0) return;
                }
                const actualAmount = Math.min(amount, carryable);

                stItem.amount -= actualAmount;
                if (stItem.amount <= 0) storage = storage.filter(i => i.id !== itemId);

                const invItem = p.state.inventory.find(i => i.id === itemId);
                if (invItem) invItem.amount += actualAmount;
                else p.state.inventory.push({ id: itemId, amount: actualAmount });
            }

            house.storage = JSON.stringify(storage);
            await db.query("UPDATE players SET inventory=? WHERE id=?",
                [JSON.stringify(p.state.inventory), playerId]);
            await db.query("UPDATE houses SET storage=? WHERE id=?", [house.storage, house.id]);

            sendStateToAll();
            p.ws.send(JSON.stringify({
                type: "house_open",
                houseId: house.id,
                name: house.name,
                price: house.price,
                owned: true,
                isOwner: house.owner_id === playerId,
                hasKey: true,
                storage
            }));
        }

        if (data.type === "house_give_key" || data.type === "house_revoke_key") {
            let house = p.state.houseId ? houses.get(p.state.houseId) : null;
            if (!house) house = findHouseNear(p.state.world, p.state.x, p.state.y);

            if (!house || house.owner_id !== playerId) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du musst dazu Besitzer sein und im/am Haus sein." }));
                return;
            }

            const [targetRows] = await db.query("SELECT id FROM players WHERE username=? OR id=?", [data.username, data.username]);
            if (targetRows.length === 0) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Spieler nicht gefunden." }));
                return;
            }
            const targetId = targetRows[0].id;

            if (data.type === "house_give_key") {
                await db.query("INSERT IGNORE INTO house_keys (house_id, player_id) VALUES (?, ?)", [house.id, targetId]);
                p.ws.send(JSON.stringify({ type: "shop_info", msg: `Hausschlüssel an ${data.username} übergeben.` }));
            } else {
                await db.query("DELETE FROM house_keys WHERE house_id=? AND player_id=?", [house.id, targetId]);
                p.ws.send(JSON.stringify({ type: "shop_info", msg: `Hausschlüssel von ${data.username} entzogen.` }));
            }
        }

        // -----------------------------------------------------
        // VERBRECHEN: Raubüberfall / Auto kurzschließen
        // -----------------------------------------------------
        if (data.type === "commit_crime") {
            if (data.crimeType === "robbery") {
                const shop = findShopNear(p.state.world, p.state.x, p.state.y);
                const atm = findAtmNear(p.state.world, p.state.x, p.state.y);

                if (!shop && !atm) {
                    p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du musst dazu in der Nähe eines Shops oder Geldautomaten sein." }));
                    return;
                }

                const now = Date.now();
                const COOLDOWN = 5 * 60 * 1000;
                if (p.lastRobberyAt && now - p.lastRobberyAt < COOLDOWN) {
                    const waitSec = Math.ceil((COOLDOWN - (now - p.lastRobberyAt)) / 1000);
                    p.ws.send(JSON.stringify({ type: "shop_error", msg: `Zu früh - warte noch ${waitSec}s.` }));
                    return;
                }
                p.lastRobberyAt = now;

                const ROBBERY_SUCCESS_CHANCE = 0.65;
                const success = Math.random() < ROBBERY_SUCCESS_CHANCE;

                if (success) {
                    const reward = 100 + Math.floor(Math.random() * 300);
                    p.state.money += reward;
                    p.state.wantedLevel = Math.min(5, (p.state.wantedLevel || 0) + 2);
                    p.state.robberyCount = (p.state.robberyCount || 0) + 1;

                    await db.query("UPDATE players SET money=?, wanted_level=?, robbery_count=? WHERE id=?",
                        [p.state.money, p.state.wantedLevel, p.state.robberyCount, playerId]);
                    sendStateToAll();

                    p.ws.send(JSON.stringify({ type: "shop_info", msg: `Raubüberfall erfolgreich! +${reward}$ - Fahndungslevel erhöht!` }));
                    await logEvent("crime", p.username, `Raubüberfall erfolgreich (+${reward}$) in ${p.state.world}`);
                    await awardXp(playerId, p, 15, "Raubüberfall");
                    await checkAchievements(playerId, p);
                    broadcastCrimeAlert({
                        crimeType: "Raubüberfall",
                        world: p.state.world,
                        x: Math.round(p.state.x),
                        y: Math.round(p.state.y)
                    });
                } else {
                    // Fehlgeschlagen: stiller Alarm ausgelöst, kein Geld, höheres Risiko
                    p.state.wantedLevel = Math.min(5, (p.state.wantedLevel || 0) + 3);

                    await db.query("UPDATE players SET wanted_level=? WHERE id=?", [p.state.wantedLevel, playerId]);
                    sendStateToAll();

                    p.ws.send(JSON.stringify({ type: "shop_error", msg: "🚨 Stiller Alarm ausgelöst! Du bist ohne Beute geflohen - Fahndungslevel stark erhöht!" }));
                    broadcastCrimeAlert({
                        crimeType: "Raubüberfall (Alarm ausgelöst)",
                        world: p.state.world,
                        x: Math.round(p.state.x),
                        y: Math.round(p.state.y)
                    });
                }
            }

            if (data.crimeType === "car_theft") {
                let target = null;
                for (const [, c] of cars) {
                    if (c.world !== p.state.world || c.driverId) continue;
                    if (Math.abs(c.x - p.state.x) < 40 && Math.abs(c.y - p.state.y) < 40) {
                        target = c;
                        break;
                    }
                }

                if (!target) {
                    p.ws.send(JSON.stringify({ type: "shop_error", msg: "Kein Auto in der Nähe zum Kurzschließen." }));
                    return;
                }
                if (target.ownerId === playerId) {
                    p.ws.send(JSON.stringify({ type: "shop_error", msg: "Das ist dein eigenes Auto - du brauchst es nicht kurzzuschließen." }));
                    return;
                }

                const now = Date.now();
                const HOTWIRE_COOLDOWN = 10 * 1000;
                if (p.lastHotwireAttemptAt && now - p.lastHotwireAttemptAt < HOTWIRE_COOLDOWN) {
                    return; // still, kein Spam-Retry - der Client blendet währenddessen eh das Minispiel
                }
                p.lastHotwireAttemptAt = now;

                // Nur das Minispiel anstoßen - das eigentliche Ergebnis kommt
                // über car_theft_minigame_result zurück (siehe unten)
                p.pendingHotwireCarId = target.id;
                p.ws.send(JSON.stringify({ type: "car_theft_minigame_start", carId: target.id }));
            }
        }

        // -----------------------------------------------------
        // AUTO-DIEBSTAHL-MINISPIEL: Ergebnis vom Client entgegennehmen
        // (Client meldet nur, OB der Timing-Treffer gelungen ist -
        // das eigentliche Ergebnis (Auto bekommen ja/nein, Fahndung
        // erhöhen) entscheidet weiterhin der Server)
        // -----------------------------------------------------
        if (data.type === "car_theft_minigame_result") {
            const carId = p.pendingHotwireCarId;
            p.pendingHotwireCarId = null;

            if (!carId || carId !== data.carId) return; // veraltete/gefälschte Anfrage
            const target = cars.get(carId);
            if (!target || target.driverId || target.world !== p.state.world) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Das Auto ist nicht mehr verfügbar." }));
                return;
            }

            p.state.wantedLevel = Math.min(5, (p.state.wantedLevel || 0) + 1);
            await db.query("UPDATE players SET wanted_level=? WHERE id=?", [p.state.wantedLevel, playerId]);

            if (data.success) {
                target.driverId = playerId;
                target.isNpc = false; // KI-Steuerung deaktivieren, sobald ein Spieler übernimmt
                p.drivingCarId = target.id;

                sendCarsToAll();
                sendStateToAll();

                p.ws.send(JSON.stringify({ type: "shop_info", msg: "Auto kurzgeschlossen! Fahndungslevel erhöht!" }));
                broadcastCrimeAlert({
                    crimeType: "Autodiebstahl",
                    world: p.state.world,
                    x: Math.round(p.state.x),
                    y: Math.round(p.state.y)
                });
            } else {
                sendStateToAll();
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Kurzschließen fehlgeschlagen - Alarm ausgelöst! Fahndungslevel erhöht!" }));
                broadcastCrimeAlert({
                    crimeType: "Autodiebstahl (fehlgeschlagen)",
                    world: p.state.world,
                    x: Math.round(p.state.x),
                    y: Math.round(p.state.y)
                });
            }
        }

        // -----------------------------------------------------
        // ANGELN: rein clientseitig erkannt (Server kennt keine
        // Wasser-Zonen aus dem 3D-Deko-System), Cooldown + Belohnung
        // sind serverseitig autoritativ
        // -----------------------------------------------------
        if (data.type === "fishing_start") {
            const now = Date.now();
            const FISHING_COOLDOWN = 8 * 1000;
            if (p.lastFishingAt && now - p.lastFishingAt < FISHING_COOLDOWN) return;
            p.lastFishingAt = now;
            p.ws.send(JSON.stringify({ type: "fishing_minigame_start" }));
        }

        if (data.type === "fishing_minigame_result") {
            if (data.success) {
                const reward = 15 + Math.floor(Math.random() * 26); // 15-40$
                p.state.money += reward;
                await db.query("UPDATE players SET money=? WHERE id=?", [p.state.money, playerId]);
                sendStateToAll();
                p.ws.send(JSON.stringify({ type: "shop_info", msg: `🐟 Fisch gefangen! +${reward}$` }));
            } else {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Kein Biss - der Fisch ist entkommen." }));
            }
        }

        // -----------------------------------------------------
        // SCHWARZMARKT: gestohlenes (kurzgeschlossenes) Auto beim
        // Hehler verkaufen, mit Entdeckungsrisiko
        // -----------------------------------------------------
        if (data.type === "sell_stolen_car") {
            const spot = findBlackMarketSpotNear(p.state.world, p.state.x, p.state.y);
            if (!spot) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du musst dazu beim Hehler sein." }));
                return;
            }
            if (!p.drivingCarId) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du musst dazu in einem gestohlenen Auto sitzen." }));
                return;
            }
            const car = cars.get(p.drivingCarId);
            if (!car || car.ownerId === playerId) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Das ist dein eigenes Auto - der Hehler nimmt nur gestohlene Ware." }));
                return;
            }

            const cfg = carConfigs[car.model] || {};
            const basePrice = getSetting("trailer_price") || 500; // grobe Wertbasis, falls kein spezifischer Fahrzeugwert existiert
            const markup = 1.3 + Math.random() * 0.4; // 30-70% Aufpreis gegenüber dem "ehrlichen" Wert
            const payout = Math.round(basePrice * markup);

            const DISCOVERY_RISK = 0.25; // 25% Chance, dass der Deal auffliegt
            const caught = Math.random() < DISCOVERY_RISK;

            // Auto verschwindet in jedem Fall (an den Hehler übergeben/zerlegt)
            p.drivingCarId = null;
            cars.delete(car.id);
            await db.query("DELETE FROM cars WHERE id=?", [car.id]);

            if (caught) {
                p.state.wantedLevel = Math.min(5, (p.state.wantedLevel || 0) + 3);
                await db.query("UPDATE players SET wanted_level=? WHERE id=?", [p.state.wantedLevel, playerId]);
                sendStateToAll();
                sendCarsToAll();

                p.ws.send(JSON.stringify({ type: "shop_error", msg: "🚨 Falle! Der Hehler war verdeckter Ermittler - Auto weg, kein Geld, Fahndungslevel stark erhöht!" }));
                broadcastCrimeAlert({
                    crimeType: "Hehlerei aufgeflogen",
                    world: p.state.world,
                    x: Math.round(p.state.x),
                    y: Math.round(p.state.y)
                });
            } else {
                p.state.money += payout;
                await db.query("UPDATE players SET money=? WHERE id=?", [p.state.money, playerId]);
                sendStateToAll();
                sendCarsToAll();

                p.ws.send(JSON.stringify({ type: "shop_info", msg: `Auto beim Hehler verkauft! +${payout}$` }));
                await logEvent("crime", p.username, `Gestohlenes Auto beim Hehler verkauft (+${payout}$)`);
            }
        }

        // -----------------------------------------------------
        // POLIZEI: Spieler verhaften
        // -----------------------------------------------------
        if (data.type === "arrest_player") {
            if (!isPoliceJobRank(p.state.jobRankId)) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Nur Polizei kann verhaften." }));
                return;
            }

            const [rows] = await db.query("SELECT id FROM players WHERE username=? OR id=?", [data.username, data.username]);
            if (rows.length === 0) return;
            const targetId = rows[0].id;

            const target = playersOnline.get(targetId);
            if (!target) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Spieler ist nicht online." }));
                return;
            }
            if (target.state.world !== p.state.world ||
                Math.hypot(target.state.x - p.state.x, target.state.y - p.state.y) > 60) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Spieler ist nicht in der Nähe." }));
                return;
            }
            if (!target.state.wantedLevel) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Dieser Spieler wird nicht gesucht." }));
                return;
            }

            const fine = Math.min(target.state.money, 200);
            const jailSeconds = target.state.wantedLevel * 25; // Handschellen + Haftzeit je nach Sternen
            const jailedUntil = Date.now() + jailSeconds * 1000;

            const prison = getAnyPrison();

            target.state.money -= fine;
            target.state.wantedLevel = 0;
            target.state.jailedUntil = jailedUntil;
            target.state.world = prison.world;
            target.state.x = prison.x * 32 + 16;
            target.state.y = prison.y * 32 + 16;

            // Falls Fahrer/Mitfahrer, Auto verlassen
            if (target.drivingCarId) {
                const c = cars.get(target.drivingCarId);
                if (c) c.driverId = null;
                target.drivingCarId = null;
            }
            if (target.passengerCarId) {
                const c = cars.get(target.passengerCarId);
                if (c) c.passengerId = null;
                target.passengerCarId = null;
            }

            p.state.bank += 100;

            await db.query(
                "UPDATE players SET money=?, wanted_level=0, world=?, x=?, y=?, jail_until=FROM_UNIXTIME(?) WHERE id=?",
                [target.state.money, target.state.world, target.state.x, target.state.y, jailedUntil / 1000, targetId]
            );
            await db.query("UPDATE players SET bank=? WHERE id=?", [p.state.bank, playerId]);

            sendStateToAll();
            sendCarsToAll();

            const prisonMap = maps[prison.world];
            if (prisonMap) {
                target.ws.send(JSON.stringify({
                    type: "map_data",
                    tiles: prisonMap.tiles,
                    tileRot: prisonMap.tileRot || null,
                    doors: prisonMap.doors || [],
                    objects: prisonMap.objects || [],
                    shops: getShopsForWorld(prison.world),
                    garages: getGaragesForWorld(prison.world),
                    jobcenters: getJobsForWorld(prison.world),
                    gasStations: getGasStationsForWorld(prison.world),
                    repairShops: getRepairShopsForWorld(prison.world),
                    jobPoints: getJobPointsForPlayer(prison.world, target.state.jobRankId),
                    houses: getHousesForWorld(prison.world),
                    taxiStands: getTaxiStandsForWorld(prison.world),
                    hospitals: getHospitalsForWorld(prison.world),
                    prisons: getPrisonsForWorld(prison.world),
                    territoryZones: getZonesForWorld(prison.world),
                    impoundLots: getImpoundLotsForWorld(prison.world),
                    fireStations: getFireStationsForWorld(prison.world),
                    harvestSpots: getHarvestSpotsForWorld(prison.world),
                    clothingShops: getClothingShopsForWorld(prison.world),
                    insuranceOffices: getInsuranceOfficesForWorld(prison.world),
                    plateOffices: getPlateOfficesForWorld(prison.world),
                    trailerShops: getTrailerShopsForWorld(prison.world),
highwayLinks: getHighwayLinksForWorld(prison.world),
blackMarketSpots: getBlackMarketSpotsForWorld(prison.world),
roadblocks: getRoadblocksForWorld(prison.world),
                    groundDrops: getGroundDropsForWorld(prison.world),
                    processSpots: getProcessSpotsForWorld(prison.world),
                    dealerSpots: getActiveDealerSpotsForWorld(prison.world),
                    fires: getFiresForWorld(prison.world),
                    atms: prisonMap.atms || [],
                    spawn: prisonMap.spawn
                }));
            }

            target.ws.send(JSON.stringify({ type: "jail_status", jailedUntil }));
            target.ws.send(JSON.stringify({ type: "shop_info", msg: `🔒 Verhaftet! Strafe: -${fine}$. Haftzeit: ${jailSeconds}s.` }));
            await logEvent("arrest", p.username, `${target.username} verhaftet (${jailSeconds}s Haft, -${fine}$)`);
            p.ws.send(JSON.stringify({ type: "shop_info", msg: `${target.username} verhaftet und eingesperrt! +100$ Prämie.` }));

            target.state.timesArrested = (target.state.timesArrested || 0) + 1;
            await db.query("UPDATE players SET times_arrested=? WHERE id=?", [target.state.timesArrested, targetId]);
            await unlockAchievement(targetId, target, "arrested_once");
            await awardXp(playerId, p, 20, "Verhaftung");
        }

        // -----------------------------------------------------
        // HANDSCHELLEN + TRANSPORT: Alternative zur Sofort-Verhaftung -
        // fesseln, ins Auto setzen, fahren, am Gefängnis abliefern
        // -----------------------------------------------------
        if (data.type === "police_cuff") {
            if (!isPoliceJobRank(p.state.jobRankId)) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Nur Polizei kann Handschellen anlegen." }));
                return;
            }
            const [rows] = await db.query("SELECT id FROM players WHERE username=? OR id=?", [data.username, data.username]);
            if (rows.length === 0) return;
            const targetId = rows[0].id;
            const target = playersOnline.get(targetId);

            if (!target) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Spieler ist nicht online." }));
                return;
            }
            if (target.state.world !== p.state.world ||
                Math.hypot(target.state.x - p.state.x, target.state.y - p.state.y) > 60) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Spieler ist nicht in der Nähe." }));
                return;
            }
            if (!target.state.wantedLevel) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Dieser Spieler wird nicht gesucht." }));
                return;
            }
            if (target.state.cuffed) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Ist schon gefesselt." }));
                return;
            }

            target.state.cuffed = true;
            target.state.cuffedBy = playerId;
            closeOpenWindowIfAny(target, "cuffed");
            sendStateToAll();

            target.ws.send(JSON.stringify({ type: "shop_info", msg: `🔗 ${p.username} hat dir Handschellen angelegt!` }));
            p.ws.send(JSON.stringify({ type: "shop_info", msg: `${target.username} gefesselt.` }));
        }

        if (data.type === "police_uncuff") {
            const [rows] = await db.query("SELECT id FROM players WHERE username=? OR id=?", [data.username, data.username]);
            if (rows.length === 0) return;
            const target = playersOnline.get(rows[0].id);
            if (!target || !target.state.cuffed) return;

            if (target.state.world !== p.state.world ||
                Math.hypot(target.state.x - p.state.x, target.state.y - p.state.y) > 60) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Spieler ist nicht in der Nähe." }));
                return;
            }

            target.state.cuffed = false;
            target.state.cuffedBy = null;
            sendStateToAll();

            target.ws.send(JSON.stringify({ type: "shop_info", msg: "Handschellen wurden abgenommen." }));
            p.ws.send(JSON.stringify({ type: "shop_info", msg: `${target.username} losgelassen.` }));
        }

        if (data.type === "police_put_in_car") {
            if (!p.drivingCarId) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du musst dazu in deinem Streifenwagen sitzen." }));
                return;
            }
            const car = cars.get(p.drivingCarId);
            if (!car || car.passengerId) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Kein freier Sitzplatz." }));
                return;
            }

            const [rows] = await db.query("SELECT id FROM players WHERE username=? OR id=?", [data.username, data.username]);
            if (rows.length === 0) return;
            const target = playersOnline.get(rows[0].id);

            if (!target || !target.state.cuffed) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Dieser Spieler ist nicht gefesselt." }));
                return;
            }
            if (target.drivingCarId || target.passengerCarId) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Sitzt schon in einem Fahrzeug." }));
                return;
            }
            if (target.state.world !== car.world || Math.hypot(target.state.x - car.x, target.state.y - car.y) > 60) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Gefangener ist nicht in der Nähe des Autos." }));
                return;
            }

            car.passengerId = target.id;
            target.passengerCarId = car.id;
            sendCarsToAll();
            sendStateToAll();

            target.ws.send(JSON.stringify({ type: "shop_info", msg: "Du wurdest ins Fahrzeug gesetzt." }));
            p.ws.send(JSON.stringify({ type: "shop_info", msg: `${target.username} ins Auto gesetzt.` }));
        }

        if (data.type === "police_take_out_of_car") {
            const [rows] = await db.query("SELECT id FROM players WHERE username=? OR id=?", [data.username, data.username]);
            if (rows.length === 0) return;
            const target = playersOnline.get(rows[0].id);
            if (!target || !target.passengerCarId) return;

            const car = cars.get(target.passengerCarId);
            if (!car || car.passengerId !== target.id) return;

            if (car.world !== p.state.world || Math.hypot(car.x - p.state.x, car.y - p.state.y) > 60) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du musst näher am Fahrzeug sein." }));
                return;
            }

            car.passengerId = null;
            target.passengerCarId = null;
            target.state.x = car.x;
            target.state.y = car.y + 30;
            target.state.world = car.world;
            sendCarsToAll();
            sendStateToAll();

            target.ws.send(JSON.stringify({ type: "shop_info", msg: "Du wurdest aus dem Fahrzeug geholt." }));
            p.ws.send(JSON.stringify({ type: "shop_info", msg: `${target.username} rausgeholt.` }));
        }

        if (data.type === "police_jail_dropoff") {
            if (!isPoliceJobRank(p.state.jobRankId)) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Nur Polizei kann am Gefängnis abliefern." }));
                return;
            }

            const prison = findPrisonNear(p.state.world, p.state.x, p.state.y);
            if (!prison) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du musst dazu am Gefängnis sein." }));
                return;
            }

            const [rows] = await db.query("SELECT id FROM players WHERE username=? OR id=?", [data.username, data.username]);
            if (rows.length === 0) return;
            const target = playersOnline.get(rows[0].id);

            if (!target || !target.state.cuffed) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Dieser Spieler ist nicht gefesselt." }));
                return;
            }
            if (target.passengerCarId !== p.drivingCarId) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Der Gefangene muss bei dir im Auto sitzen." }));
                return;
            }

            const fine = Math.min(target.state.money, 200);
            const jailSeconds = (target.state.wantedLevel || 1) * 25;
            const jailedUntil = Date.now() + jailSeconds * 1000;

            const car = cars.get(target.passengerCarId);
            if (car) car.passengerId = null;
            target.passengerCarId = null;

            target.state.money -= fine;
            target.state.wantedLevel = 0;
            target.state.cuffed = false;
            target.state.cuffedBy = null;
            target.state.jailedUntil = jailedUntil;
            target.state.world = prison.world;
            target.state.x = prison.x * 32 + 16;
            target.state.y = prison.y * 32 + 16;

            p.state.bank += 100;

            await db.query(
                "UPDATE players SET money=?, wanted_level=0, world=?, x=?, y=?, jail_until=FROM_UNIXTIME(?) WHERE id=?",
                [target.state.money, target.state.world, target.state.x, target.state.y, jailedUntil / 1000, target.id]
            );
            await db.query("UPDATE players SET bank=? WHERE id=?", [p.state.bank, playerId]);

            sendStateToAll();
            sendCarsToAll();

            const prisonMap = maps[prison.world];
            if (prisonMap) {
                target.ws.send(JSON.stringify({
                    type: "map_data",
                    tiles: prisonMap.tiles,
                    tileRot: prisonMap.tileRot || null,
                    doors: prisonMap.doors || [],
                    objects: prisonMap.objects || [],
                    shops: getShopsForWorld(prison.world),
                    garages: getGaragesForWorld(prison.world),
                    jobcenters: getJobsForWorld(prison.world),
                    gasStations: getGasStationsForWorld(prison.world),
                    repairShops: getRepairShopsForWorld(prison.world),
                    jobPoints: getJobPointsForPlayer(prison.world, target.state.jobRankId),
                    houses: getHousesForWorld(prison.world),
                    taxiStands: getTaxiStandsForWorld(prison.world),
                    hospitals: getHospitalsForWorld(prison.world),
                    prisons: getPrisonsForWorld(prison.world),
                    territoryZones: getZonesForWorld(prison.world),
                    impoundLots: getImpoundLotsForWorld(prison.world),
                    fireStations: getFireStationsForWorld(prison.world),
                    harvestSpots: getHarvestSpotsForWorld(prison.world),
                    clothingShops: getClothingShopsForWorld(prison.world),
                    insuranceOffices: getInsuranceOfficesForWorld(prison.world),
                    plateOffices: getPlateOfficesForWorld(prison.world),
                    trailerShops: getTrailerShopsForWorld(prison.world),
highwayLinks: getHighwayLinksForWorld(prison.world),
blackMarketSpots: getBlackMarketSpotsForWorld(prison.world),
roadblocks: getRoadblocksForWorld(prison.world),
                    groundDrops: getGroundDropsForWorld(prison.world),
                    processSpots: getProcessSpotsForWorld(prison.world),
                    dealerSpots: getActiveDealerSpotsForWorld(prison.world),
                    fires: getFiresForWorld(prison.world),
                    atms: prisonMap.atms || [],
                    spawn: prisonMap.spawn
                }));
            }

            target.ws.send(JSON.stringify({ type: "jail_status", jailedUntil }));
            target.ws.send(JSON.stringify({ type: "shop_info", msg: `🔒 Eingesperrt! Strafe: -${fine}$. Haftzeit: ${jailSeconds}s.` }));
            p.ws.send(JSON.stringify({ type: "shop_info", msg: `${target.username} abgeliefert! +100$ Prämie.` }));
            await logEvent("arrest", p.username, `${target.username} per Transport eingesperrt (${jailSeconds}s Haft, -${fine}$)`);

            target.state.timesArrested = (target.state.timesArrested || 0) + 1;
            await db.query("UPDATE players SET times_arrested=? WHERE id=?", [target.state.timesArrested, target.id]);
            await unlockAchievement(target.id, target, "arrested_once");
            await awardXp(playerId, p, 20, "Verhaftung");
        }

        // -----------------------------------------------------
        // TAXI: Fahrgast aufnehmen
        // -----------------------------------------------------
        if (data.type === "taxi_pickup") {
            if (!p.drivingCarId || !isTaxiJobRank(p.state.jobRankId)) return;

            const car = cars.get(p.drivingCarId);
            if (!car || car.passengerId) return;

            const targetId = Number(data.playerId);
            if (!taxiWaiting.has(targetId)) return;

            const passenger = playersOnline.get(targetId);
            if (!passenger || passenger.passengerCarId || passenger.drivingCarId) {
                taxiWaiting.delete(targetId); // nicht mehr gültig
                return;
            }

            taxiWaiting.delete(targetId);
            car.passengerId = targetId;
            car.taxiFare = true;
            passenger.passengerCarId = car.id;
            passenger.state.world = car.world;
            passenger.state.x = car.x;
            passenger.state.y = car.y;

            sendCarsToAll();
            sendStateToAll();

            p.ws.send(JSON.stringify({ type: "shop_info", msg: `${passenger.username} eingestiegen.` }));
            passenger.ws.send(JSON.stringify({ type: "shop_info", msg: `Du wurdest von ${p.username} abgeholt.` }));
        }

        // -----------------------------------------------------
        // IN-GAME ADMIN-MENÜ
        // -----------------------------------------------------
        if (data.type === "admin_toggle_duty") {
            if (!p.isAdmin) return;
            p.adminDuty = !p.adminDuty;

            if (p.adminDuty) {
                // Dienst antreten: aktuelle Kleidung/Skin merken, Admin-Look anziehen
                p.preAdminShirtId = p.state.shirtItemId || null;
                p.preAdminPantsId = p.state.pantsItemId || null;
                p.preAdminShoesId = p.state.shoesItemId || null;
                p.preAdminHelmetId = p.state.helmetItemId || null;
                p.preAdminSkinItemId = p.state.skinItemId || null;

                const adminShirtId = Number(getSiteConfig("admin_shirt_id")) || null;
                const adminPantsId = Number(getSiteConfig("admin_pants_id")) || null;
                const adminShoesId = Number(getSiteConfig("admin_shoes_id")) || null;
                const adminHelmetId = Number(getSiteConfig("admin_helmet_id")) || null;
                const adminSkinId = Number(getSiteConfig("admin_skin_id")) || null;

                if (adminShirtId) p.state.shirtItemId = adminShirtId;
                if (adminPantsId) p.state.pantsItemId = adminPantsId;
                if (adminShoesId) p.state.shoesItemId = adminShoesId;
                if (adminHelmetId) p.state.helmetItemId = adminHelmetId;
                if (adminSkinId) p.state.skinItemId = adminSkinId;
            } else {
                // Dienst beenden: vorherige Kleidung/Skin wiederherstellen
                p.state.shirtItemId = p.preAdminShirtId || null;
                p.state.pantsItemId = p.preAdminPantsId || null;
                p.state.shoesItemId = p.preAdminShoesId || null;
                p.state.helmetItemId = p.preAdminHelmetId || null;
                p.state.skinItemId = p.preAdminSkinItemId || null;
            }

            await db.query(
                "UPDATE players SET shirt_item_id=?, pants_item_id=?, shoes_item_id=?, helmet_item_id=?, skin_item_id=? WHERE id=?",
                [p.state.shirtItemId, p.state.pantsItemId, p.state.shoesItemId, p.state.helmetItemId, p.state.skinItemId, playerId]
            );
            sendStateToAll();

            p.ws.send(JSON.stringify({
                type: "admin_duty_status",
                onDuty: p.adminDuty
            }));
            p.ws.send(JSON.stringify({
                type: "shop_info",
                msg: p.adminDuty
                    ? "🛡️ Admin-Dienstmodus aktiv - du kannst jedes Auto fahren und Admin-Befehle nutzen."
                    : "Admin-Dienstmodus beendet - Admin-Befehle sind jetzt gesperrt."
            }));
            await logEvent("admin_duty", p.username, p.adminDuty ? "Dienstmodus aktiviert" : "Dienstmodus beendet");
        }

        if (data.type === "admin_get_players") {
            if (!p.isAdmin || !p.adminDuty) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du musst dazu im Admin-Dienst sein." }));
                return;
            }

            const list = [];
            for (const [id, other] of playersOnline) {
                list.push({
                    id,
                    username: other.username,
                    world: other.state.world,
                    x: Math.round(other.state.x),
                    y: Math.round(other.state.y),
                    money: other.state.money
                });
            }
            p.ws.send(JSON.stringify({ type: "admin_players", players: list }));
        }

        if (data.type === "admin_set_money") {
            if (!p.isAdmin || !p.adminDuty) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du musst dazu im Admin-Dienst sein." }));
                return;
            }

            const targetId = Number(data.targetId);
            const amount = Math.max(0, Math.floor(Number(data.amount)));
            const target = playersOnline.get(targetId);
            if (!target || isNaN(amount)) return;

            target.state.money = amount;
            await db.query("UPDATE players SET money=? WHERE id=?", [amount, targetId]);
            sendStateToAll();
            p.ws.send(JSON.stringify({ type: "shop_info", msg: `Bargeld von ${target.username} auf ${amount}$ gesetzt.` }));
            await logEvent("admin_money", p.username, `Bargeld von ${target.username} auf ${amount}$ gesetzt`);
        }

        if (data.type === "admin_lock_player") {
            if (!p.isAdmin || !p.adminDuty) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du musst dazu im Admin-Dienst sein." }));
                return;
            }

            const targetId = Number(data.targetId);
            const target = playersOnline.get(targetId);

            await db.query("UPDATE players SET approved=0 WHERE id=?", [targetId]);

            if (target) {
                const targetName = target.username;
                try { target.ws.close(); } catch {}
                playersOnline.delete(targetId);
                sendStateToAll();
                sendCarsToAll();
                p.ws.send(JSON.stringify({ type: "shop_info", msg: `${targetName} wurde gesperrt und getrennt.` }));
                await logEvent("admin_lock", p.username, `${targetName} gesperrt und getrennt`);
            }
        }

        if (data.type === "admin_teleport_to") {
            if (!p.isAdmin || !p.adminDuty) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du musst dazu im Admin-Dienst sein." }));
                return;
            }

            const targetId = Number(data.targetId);
            const target = playersOnline.get(targetId);
            if (!target) return;

            p.state.world = target.state.world;
            p.state.x = target.state.x + 20;
            p.state.y = target.state.y;

            await db.query(
                "UPDATE players SET world=?, x=?, y=? WHERE id=?",
                [p.state.world, p.state.x, p.state.y, playerId]
            );

            sendStateToAll();

            const map = maps[p.state.world];
            if (map) {
                p.ws.send(JSON.stringify({
                    type: "map_data",
                    tiles: map.tiles,
                    tileRot: map.tileRot || null,
                    doors: map.doors || [],
                    objects: map.objects || [],
                    shops: getShopsForWorld(p.state.world),
                    garages: getGaragesForWorld(p.state.world),
                    jobcenters: getJobsForWorld(p.state.world),
                    gasStations: getGasStationsForWorld(p.state.world),
                    repairShops: getRepairShopsForWorld(p.state.world),
                    jobPoints: getJobPointsForPlayer(p.state.world, p.state.jobRankId),
                    houses: getHousesForWorld(p.state.world),
                taxiStands: getTaxiStandsForWorld(p.state.world),
                    hospitals: getHospitalsForWorld(p.state.world),
                    prisons: getPrisonsForWorld(p.state.world),
                    territoryZones: getZonesForWorld(p.state.world),
                    impoundLots: getImpoundLotsForWorld(p.state.world),
                    fireStations: getFireStationsForWorld(p.state.world),
                    harvestSpots: getHarvestSpotsForWorld(p.state.world),
                    clothingShops: getClothingShopsForWorld(p.state.world),
                insuranceOffices: getInsuranceOfficesForWorld(p.state.world),
                plateOffices: getPlateOfficesForWorld(p.state.world),
                trailerShops: getTrailerShopsForWorld(p.state.world),
highwayLinks: getHighwayLinksForWorld(p.state.world),
blackMarketSpots: getBlackMarketSpotsForWorld(p.state.world),
roadblocks: getRoadblocksForWorld(p.state.world),
                    groundDrops: getGroundDropsForWorld(p.state.world),
                    processSpots: getProcessSpotsForWorld(p.state.world),
                    dealerSpots: getActiveDealerSpotsForWorld(p.state.world),
                    fires: getFiresForWorld(p.state.world),
                    atms: map.atms || [],
                    spawn: map.spawn
                }));
            }
        }

        // -----------------------------------------------------
        // -----------------------------------------------------
        // TOR-SCHLÜSSELSYSTEM
        // -----------------------------------------------------
        // -----------------------------------------------------
        // STRASSENSPERRE: nur Polizei, an eigener Position
        // -----------------------------------------------------
        if (data.type === "roadblock_place") {
            if (!isPoliceJobRank(p.state.jobRankId) || !p.state.onDuty) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Nur im Polizeidienst möglich." }));
                return;
            }
            const id = nextRoadblockId++;
            roadblocks.set(id, { world: p.state.world, x: p.state.x, y: p.state.y, placedBy: playerId });
            broadcastRoadblockUpdate(p.state.world);
            p.ws.send(JSON.stringify({ type: "shop_info", msg: "🚧 Straßensperre aufgestellt." }));
        }

        if (data.type === "roadblock_remove") {
            if (!isPoliceJobRank(p.state.jobRankId) || !p.state.onDuty) return;

            let removedAny = false;
            for (const [id, r] of roadblocks) {
                if (r.world !== p.state.world) continue;
                if (Math.hypot(p.state.x - r.x, p.state.y - r.y) < ROADBLOCK_RADIUS + 20) {
                    roadblocks.delete(id);
                    removedAny = true;
                }
            }
            if (removedAny) {
                broadcastRoadblockUpdate(p.state.world);
                p.ws.send(JSON.stringify({ type: "shop_info", msg: "🚧 Straßensperre entfernt." }));
            } else {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Keine Straßensperre in der Nähe." }));
            }
        }

        // -----------------------------------------------------
        // EMOTES/GESTEN: nur eine Weiterleitung an alle in derselben
        // Welt - keine Spielauswirkung, rein optisch
        // -----------------------------------------------------
        if (data.type === "emote") {
            const ALLOWED_EMOTES = ["wave", "dance", "point", "sit", "salute"];
            if (!ALLOWED_EMOTES.includes(data.emoteType)) return;

            const now = Date.now();
            if (p.lastEmoteAt && now - p.lastEmoteAt < 1500) return; // Spam-Schutz
            p.lastEmoteAt = now;

            const msg = JSON.stringify({ type: "emote_triggered", playerId, emoteType: data.emoteType });
            for (const [, other] of playersOnline) {
                if (other.state.world === p.state.world) other.ws.send(msg);
            }
        }

        if (data.type === "gate_claim") {
            const obj = findGateObjectNear(p.state.world, p.state.x, p.state.y);
            if (!obj) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Kein Tor in der Nähe." }));
                return;
            }

            let gate = findGateByPosition(p.state.world, obj.x, obj.y);
            if (gate && gate.owner_id) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Dieses Tor gehört bereits jemandem." }));
                return;
            }

            if (gate) {
                await db.query("UPDATE gates SET owner_id=? WHERE id=?", [playerId, gate.id]);
            } else {
                await db.query(
                    "INSERT INTO gates (name, world, x, y, owner_id) VALUES (?, ?, ?, ?, ?)",
                    ["Tor", p.state.world, obj.x, obj.y, playerId]
                );
            }
            await loadGates();
            p.ws.send(JSON.stringify({ type: "shop_info", msg: "Tor beansprucht - du kannst jetzt Schlüssel vergeben (/gatekey Name)." }));
        }

        if (data.type === "gate_unclaim") {
            const obj = findGateObjectNear(p.state.world, p.state.x, p.state.y);
            if (!obj) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Kein Tor in der Nähe." }));
                return;
            }

            const gate = findGateByPosition(p.state.world, obj.x, obj.y);
            if (!gate || gate.owner_id !== playerId) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du bist nicht Besitzer dieses Tores." }));
                return;
            }

            await db.query("DELETE FROM gate_keys WHERE gate_id=?", [gate.id]);
            await db.query("UPDATE gates SET owner_id=NULL WHERE id=?", [gate.id]);
            await loadGates();
            p.ws.send(JSON.stringify({ type: "shop_info", msg: "Tor freigegeben - jeder kann es wieder nutzen." }));
        }

        if (data.type === "gate_info") {
            const obj = findGateObjectNear(p.state.world, p.state.x, p.state.y);
            if (!obj) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Kein Tor in der Nähe." }));
                return;
            }

            const gate = findGateByPosition(p.state.world, obj.x, obj.y);
            if (!gate || (!gate.owner_id && !gate.required_job_id)) {
                p.ws.send(JSON.stringify({ type: "shop_info", msg: "Dieses Tor ist frei nutzbar (kein Besitzer, kein Job-Schutz)." }));
                return;
            }

            if (gate.required_job_id) {
                const jobDef = jobs.get(gate.required_job_id);
                addSystemLine(p, `👷 Nur Job "${jobDef?.name || "?"}" kann dieses Tor bedienen.`);
            }
            if (gate.owner_id) {
                const [ownerRows] = await db.query("SELECT username FROM players WHERE id=?", [gate.owner_id]);
                const [keyRows] = await db.query(
                    "SELECT p.username FROM gate_keys gk JOIN players p ON p.id = gk.player_id WHERE gk.gate_id=?",
                    [gate.id]
                );
                addSystemLine(p, `🔑 Tor-Besitzer: ${ownerRows[0]?.username || "?"}`);
                addSystemLine(p, `Schlüssel-Inhaber: ${keyRows.length ? keyRows.map(r => r.username).join(", ") : "keine"}`);
            }
        }

        // -----------------------------------------------------
        // KOPFGELD-SYSTEM
        // -----------------------------------------------------
        if (data.type === "bounty_place") {
            const amount = Math.max(1, Math.floor(Number(data.amount) || 0));
            const targetUsername = String(data.username || "").trim();

            if (!targetUsername || amount <= 0) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Name und Betrag erforderlich." }));
                return;
            }

            const [targetRows] = await db.query("SELECT id FROM players WHERE username=? OR id=?", [targetUsername, targetUsername]);
            if (targetRows.length === 0) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Spieler nicht gefunden." }));
                return;
            }
            const targetId = targetRows[0].id;

            if (targetId === playerId) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du kannst kein Kopfgeld auf dich selbst aussetzen." }));
                return;
            }
            if (p.state.money < amount) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Nicht genug Bargeld." }));
                return;
            }

            p.state.money -= amount;
            await db.query("UPDATE players SET money=? WHERE id=?", [p.state.money, playerId]);
            sendStateToAll();

            const newTotal = (bounties.get(targetId) || 0) + amount;
            bounties.set(targetId, newTotal);
            await db.query(
                "INSERT INTO bounties (target_id, amount) VALUES (?, ?) ON DUPLICATE KEY UPDATE amount = amount + ?",
                [targetId, amount, amount]
            );

            broadcastToAll(`💀 ${p.username} hat ein Kopfgeld von ${amount}$ auf ${targetUsername} ausgesetzt! (Gesamt: ${newTotal}$)`, true);
            await logEvent("bounty", p.username, `Kopfgeld auf ${targetUsername} ausgesetzt: +${amount}$ (Gesamt ${newTotal}$)`);
        }

        if (data.type === "bounty_info") {
            const targetUsername = String(data.username || "").trim();
            const [targetRows] = await db.query("SELECT id FROM players WHERE username=? OR id=?", [targetUsername, targetUsername]);
            if (targetRows.length === 0) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Spieler nicht gefunden." }));
                return;
            }
            const amount = bounties.get(targetRows[0].id) || 0;
            p.ws.send(JSON.stringify({
                type: "shop_info",
                msg: amount > 0 ? `💀 Kopfgeld auf ${targetUsername}: ${amount}$` : `Kein Kopfgeld auf ${targetUsername}.`
            }));
        }

        // -----------------------------------------------------
        // JOB-FUNK: Nachricht geht nur an alle Online-Spieler mit demselben Job-Typ
        // -----------------------------------------------------
        // -----------------------------------------------------
        // FMS-STATUS: eigenen Status setzen (nur mit BOS-Job im Dienst)
        // -----------------------------------------------------
        if (data.type === "set_fms_status") {
            const jobType = getJobTypeOf(p.state.jobRankId);
            if (!p.state.onDuty || !["police", "medic", "fire"].includes(jobType)) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "FMS-Status nur im Dienst bei Polizei/Rettungsdienst/Feuerwehr verfügbar." }));
                return;
            }

            const status = Math.floor(Number(data.status));
            if (isNaN(status) || status < 0 || status > 8) return;

            p.state.fmsStatus = status;

            const radioIcons = { police: "🚓", medic: "⛑️", fire: "🚒" };
            const statusMsg = JSON.stringify({
                type: "radio_message",
                icon: radioIcons[jobType],
                jobName: jobs.get(jobRanks.get(p.state.jobRankId)?.jobId)?.name || jobType,
                username: p.username,
                text: `[FMS ${status}] ${FMS_STATUS_LABELS[status]}`
            });
            for (const [, other] of playersOnline) {
                if (getJobTypeOf(other.state.jobRankId) === jobType) other.ws.send(statusMsg);
            }

            broadcastLeitstelleUpdate();
        }

        // -----------------------------------------------------
        // LEITSTELLE: Übersicht öffnen/schließen, Einsatz verteilen
        // -----------------------------------------------------
        if (data.type === "leitstelle_open") {
            if (!hasTabletAccess(p.state.jobRankId)) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: tabletAccessDenyReason(p.state.jobRankId) }));
                return;
            }
            leitstelleViewers.add(playerId);
            p.ws.send(JSON.stringify({ type: "leitstelle_units", units: getLeitstelleUnitsSnapshot() }));
        }

        if (data.type === "leitstelle_close") {
            leitstelleViewers.delete(playerId);
        }

        if (data.type === "leitstelle_dispatch") {
            if (!hasTabletAccess(p.state.jobRankId)) return;

            const targetType = ["police", "medic", "fire"].includes(data.jobType) ? data.jobType : null;
            const text = String(data.text || "").trim().slice(0, 300);
            if (!targetType || !text) return;

            const radioIcons = { police: "🚓", medic: "⛑️", fire: "🚒" };
            const dispatchMsg = JSON.stringify({
                type: "radio_message",
                icon: "📻",
                jobName: "Leitstelle",
                username: `Leitstelle → ${radioIcons[targetType]}`,
                text
            });
            for (const [, other] of playersOnline) {
                if (getJobTypeOf(other.state.jobRankId) === targetType && other.state.onDuty) {
                    other.ws.send(dispatchMsg);
                }
            }

            await logEvent("leitstelle", p.username, `Einsatz an ${targetType} verteilt: ${text}`);
            p.ws.send(JSON.stringify({ type: "shop_info", msg: "Einsatz an alle im Dienst befindlichen Einheiten gesendet." }));
        }

        if (data.type === "radio_message") {
            const rank = jobRanks.get(p.state.jobRankId);
            const jobDef = rank ? jobs.get(rank.jobId) : null;
            const jobType = jobDef ? jobDef.type : null;

            if (!jobType || !["police", "medic", "fire"].includes(jobType)) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du hast keinen Job mit eigenem Funk-Kanal." }));
                return;
            }

            const text = String(data.text || "").trim().slice(0, 200);
            if (!text) return;

            const radioIcons = { police: "🚓", medic: "⛑️", fire: "🚒" };
            const msg = JSON.stringify({
                type: "radio_message",
                icon: radioIcons[jobType],
                jobName: jobDef.name,
                username: p.username,
                text
            });

            for (const [, other] of playersOnline) {
                const otherRank = jobRanks.get(other.state.jobRankId);
                const otherJob = otherRank ? jobs.get(otherRank.jobId) : null;
                if (otherJob && otherJob.type === jobType) {
                    other.ws.send(msg);
                }
            }
        }

        // -----------------------------------------------------
        // MOBILTELEFON: KONTAKTE
        // -----------------------------------------------------
        if (data.type === "phone_add_contact") {
            const username = String(data.username || "").trim();
            const [targetRows] = await db.query("SELECT id FROM players WHERE username=? OR id=?", [username, username]);
            if (targetRows.length === 0) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Spieler nicht gefunden." }));
                return;
            }
            const targetId = targetRows[0].id;
            if (targetId === playerId) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du kannst dich nicht selbst hinzufügen." }));
                return;
            }

            const [already] = await db.query(
                "SELECT 1 FROM friends WHERE (player_id=? AND friend_id=?) OR (player_id=? AND friend_id=?)",
                [playerId, targetId, targetId, playerId]
            );
            if (already.length > 0) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Ist schon in deinen Kontakten." }));
                return;
            }

            // Falls der andere bereits eine Anfrage an MICH geschickt hat -> direkt annehmen statt doppelt anfragen
            const [reverseRequest] = await db.query(
                "SELECT 1 FROM friend_requests WHERE from_id=? AND to_id=?",
                [targetId, playerId]
            );
            if (reverseRequest.length > 0) {
                await db.query("DELETE FROM friend_requests WHERE from_id=? AND to_id=?", [targetId, playerId]);
                await db.query("INSERT INTO friends (player_id, friend_id) VALUES (?, ?)", [playerId, targetId]);
                p.ws.send(JSON.stringify({ type: "shop_info", msg: `${username} als Kontakt hinzugefügt (ihr hattet beide angefragt).` }));
                const target = playersOnline.get(targetId);
                if (target) target.ws.send(JSON.stringify({ type: "shop_info", msg: `${p.username} ist jetzt in deinen Kontakten.` }));
                return;
            }

            await db.query(
                "INSERT IGNORE INTO friend_requests (from_id, to_id) VALUES (?, ?)",
                [playerId, targetId]
            );
            p.ws.send(JSON.stringify({ type: "shop_info", msg: `Kontaktanfrage an ${username} gesendet.` }));

            const target = playersOnline.get(targetId);
            if (target) target.ws.send(JSON.stringify({ type: "shop_info", msg: `📱 Neue Kontaktanfrage von ${p.username} (/phone accept ${p.username})` }));
        }

        if (data.type === "phone_accept_contact") {
            const username = String(data.username || "").trim();
            const [fromRows] = await db.query("SELECT id FROM players WHERE username=? OR id=?", [username, username]);
            if (fromRows.length === 0) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Spieler nicht gefunden." }));
                return;
            }
            const fromId = fromRows[0].id;

            const [reqRows] = await db.query(
                "SELECT 1 FROM friend_requests WHERE from_id=? AND to_id=?",
                [fromId, playerId]
            );
            if (reqRows.length === 0) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Keine Anfrage von diesem Spieler vorhanden." }));
                return;
            }

            await db.query("DELETE FROM friend_requests WHERE from_id=? AND to_id=?", [fromId, playerId]);
            await db.query("INSERT INTO friends (player_id, friend_id) VALUES (?, ?)", [playerId, fromId]);

            p.ws.send(JSON.stringify({ type: "shop_info", msg: `${username} als Kontakt hinzugefügt.` }));
            const fromPlayer = playersOnline.get(fromId);
            if (fromPlayer) fromPlayer.ws.send(JSON.stringify({ type: "shop_info", msg: `${p.username} hat deine Kontaktanfrage angenommen.` }));
        }

        if (data.type === "phone_remove_contact") {
            const username = String(data.username || "").trim();
            const [targetRows] = await db.query("SELECT id FROM players WHERE username=? OR id=?", [username, username]);
            if (targetRows.length === 0) return;
            const targetId = targetRows[0].id;

            await db.query(
                "DELETE FROM friends WHERE (player_id=? AND friend_id=?) OR (player_id=? AND friend_id=?)",
                [playerId, targetId, targetId, playerId]
            );
            p.ws.send(JSON.stringify({ type: "shop_info", msg: `${username} aus Kontakten entfernt.` }));
        }

        if (data.type === "phone_get_contacts") {
            const [rows] = await db.query(`
                SELECT p.id, p.username
                FROM friends f
                JOIN players p ON p.id = (CASE WHEN f.player_id = ? THEN f.friend_id ELSE f.player_id END)
                WHERE f.player_id = ? OR f.friend_id = ?
            `, [playerId, playerId, playerId]);

            const [pendingRows] = await db.query(`
                SELECT p.username FROM friend_requests fr JOIN players p ON p.id = fr.from_id WHERE fr.to_id = ?
            `, [playerId]);

            const contacts = rows.map(r => ({
                id: r.id,
                username: r.username,
                online: playersOnline.has(r.id)
            }));

            p.ws.send(JSON.stringify({
                type: "phone_contacts_data",
                contacts,
                pendingRequests: pendingRows.map(r => r.username)
            }));
        }

        // -----------------------------------------------------
        // MOBILTELEFON: SMS
        // -----------------------------------------------------
        if (data.type === "phone_send_sms") {
            const username = String(data.username || "").trim();
            const text = String(data.text || "").trim().slice(0, 500);
            if (!username || !text) return;

            const [targetRows] = await db.query("SELECT id FROM players WHERE username=? OR id=?", [username, username]);
            if (targetRows.length === 0) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Spieler nicht gefunden." }));
                return;
            }
            const targetId = targetRows[0].id;

            await db.query(
                "INSERT INTO phone_messages (sender_id, receiver_id, text) VALUES (?, ?, ?)",
                [playerId, targetId, text]
            );

            p.ws.send(JSON.stringify({ type: "phone_sms_sent", username, text }));

            const target = playersOnline.get(targetId);
            if (target) {
                target.ws.send(JSON.stringify({ type: "phone_sms_received", username: p.username, text }));
            }
        }

        if (data.type === "phone_get_messages") {
            const username = String(data.username || "").trim();
            const [otherRows] = await db.query("SELECT id FROM players WHERE username=? OR id=?", [username, username]);
            if (otherRows.length === 0) return;
            const otherId = otherRows[0].id;

            const [rows] = await db.query(`
                SELECT sender_id, text, sent_at FROM phone_messages
                WHERE (sender_id=? AND receiver_id=?) OR (sender_id=? AND receiver_id=?)
                ORDER BY sent_at ASC LIMIT 100
            `, [playerId, otherId, otherId, playerId]);

            await db.query(
                "UPDATE phone_messages SET read_flag=1 WHERE sender_id=? AND receiver_id=?",
                [otherId, playerId]
            );

            p.ws.send(JSON.stringify({
                type: "phone_messages_data",
                username,
                messages: rows.map(r => ({ fromMe: r.sender_id === playerId, text: r.text, sentAt: r.sent_at }))
            }));
        }

        // -----------------------------------------------------
        // MOBILTELEFON: ANRUFE
        // -----------------------------------------------------
        if (data.type === "phone_call_request") {
            if (LIVEKIT_API_KEY.startsWith("TODO")) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Sprachchat ist serverseitig noch nicht eingerichtet." }));
                return;
            }

            const username = String(data.username || "").trim();
            const [targetRows] = await db.query("SELECT id FROM players WHERE username=? OR id=?", [username, username]);
            if (targetRows.length === 0) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Spieler nicht gefunden." }));
                return;
            }
            const targetId = targetRows[0].id;
            const target = playersOnline.get(targetId);

            if (!target) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: `${username} ist nicht online.` }));
                return;
            }
            if (targetId === playerId) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du kannst dich nicht selbst anrufen." }));
                return;
            }
            // Schon ein Anruf für dieses Ziel unterwegs? -> nicht doppelt klingeln lassen
            for (const [, c] of pendingCalls) {
                if (c.targetId === targetId) {
                    p.ws.send(JSON.stringify({ type: "shop_error", msg: `${username} klingelt bereits.` }));
                    return;
                }
            }

            const callId = callIdCounter++;
            const room = `call_${callId}_${Date.now()}`;

            const timeoutHandle = setTimeout(() => {
                if (pendingCalls.has(callId)) {
                    pendingCalls.delete(callId);
                    p.ws.send(JSON.stringify({ type: "call_missed", username }));
                    target.ws.send(JSON.stringify({ type: "call_cancelled" }));
                }
            }, CALL_RING_TIMEOUT_MS);

            pendingCalls.set(callId, { callerId: playerId, targetId, room, timeoutHandle });

            target.ws.send(JSON.stringify({ type: "incoming_call", callId, callerName: p.username }));
            p.ws.send(JSON.stringify({ type: "call_ringing", callId, username }));
        }

        if (data.type === "phone_call_accept") {
            const callId = Number(data.callId);
            const call = pendingCalls.get(callId);
            if (!call || call.targetId !== playerId) return;

            clearTimeout(call.timeoutHandle);
            pendingCalls.delete(callId);

            const caller = playersOnline.get(call.callerId);
            if (!caller) return; // Anrufer inzwischen offline gegangen

            try {
                const callerToken = new AccessToken(LIVEKIT_API_KEY, LIVEKIT_API_SECRET, {
                    identity: String(call.callerId), name: caller.username
                });
                callerToken.addGrant({ roomJoin: true, room: call.room, canPublish: true, canSubscribe: true });

                const targetToken = new AccessToken(LIVEKIT_API_KEY, LIVEKIT_API_SECRET, {
                    identity: String(playerId), name: p.username
                });
                targetToken.addGrant({ roomJoin: true, room: call.room, canPublish: true, canSubscribe: true });

                caller.ws.send(JSON.stringify({
                    type: "call_connected", callId, room: call.room, url: LIVEKIT_URL,
                    token: await callerToken.toJwt(), otherUsername: p.username
                }));
                p.ws.send(JSON.stringify({
                    type: "call_connected", callId, room: call.room, url: LIVEKIT_URL,
                    token: await targetToken.toJwt(), otherUsername: caller.username
                }));
            } catch (err) {
                console.error("Anruf-Token-Fehler:", err);
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Anruf konnte nicht verbunden werden." }));
            }
        }

        if (data.type === "phone_call_decline") {
            const callId = Number(data.callId);
            const call = pendingCalls.get(callId);
            if (!call || call.targetId !== playerId) return;

            clearTimeout(call.timeoutHandle);
            pendingCalls.delete(callId);

            const caller = playersOnline.get(call.callerId);
            if (caller) caller.ws.send(JSON.stringify({ type: "call_declined" }));
        }

        if (data.type === "phone_call_end") {
            // Auflegen kann von beiden Seiten kommen, während der Anruf schon verbunden ist -
            // wir kennen den Partner nicht mehr aus pendingCalls (das wurde ja schon gelöscht),
            // daher sendet der Client einfach mit, wen er gerade dran hat
            const otherUsername = String(data.otherUsername || "").trim();
            const [otherRows] = await db.query("SELECT id FROM players WHERE username=? OR id=?", [otherUsername, otherUsername]);
            if (otherRows.length === 0) return;
            const other = playersOnline.get(otherRows[0].id);
            if (other) other.ws.send(JSON.stringify({ type: "call_ended" }));
        }

        // -----------------------------------------------------
        // KLEIDUNG: Farben kaufen (nur am Kleidungsladen)
        // -----------------------------------------------------
        if (data.type === "buy_clothing_item") {
            const shop = findClothingShopNear(p.state.world, p.state.x, p.state.y);
            if (!shop) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du musst dazu am Kleidungsladen sein." }));
                return;
            }

            const item = clothingItems.get(Number(data.itemId));
            if (!item) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Kleidungsstück nicht gefunden." }));
                return;
            }
            if (p.state.money < item.price) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: `Nicht genug Geld (${item.price}$ nötig).` }));
                return;
            }

            p.state.money -= item.price;

            const invId = "clothing_" + item.id;
            const invItem = p.state.inventory.find(i => i.id === invId);
            if (invItem) invItem.amount++;
            else p.state.inventory.push({ id: invId, amount: 1 });

            await db.query(
                "UPDATE players SET money=?, inventory=? WHERE id=?",
                [p.state.money, JSON.stringify(p.state.inventory), playerId]
            );
            sendStateToAll();

            p.ws.send(JSON.stringify({ type: "shop_info", msg: `${item.name} gekauft! -${item.price}$ (im Inventar, zum Anziehen benutzen)` }));
        }

        if (data.type === "wear_clothing") {
            const invId = String(data.invItemId || "");
            const match = invId.match(/^clothing_(\d+)$/);
            if (!match) return;

            const item = clothingItems.get(Number(match[1]));
            if (!item) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Dieses Kleidungsstück existiert nicht mehr." }));
                return;
            }

            const invItem = p.state.inventory.find(i => i.id === invId && i.amount > 0);
            if (!invItem) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Das hast du nicht im Inventar." }));
                return;
            }

            const slotField = { shirt: "shirtItemId", pants: "pantsItemId", shoes: "shoesItemId", helmet: "helmetItemId" }[item.slot];
            const dbField = { shirt: "shirt_item_id", pants: "pants_item_id", shoes: "shoes_item_id", helmet: "helmet_item_id" }[item.slot];

            // Aktuell getragenes Teil (falls vorhanden) zurück ins Inventar
            const previousId = p.state[slotField];
            if (previousId) {
                const prevInvId = "clothing_" + previousId;
                const prevInvItem = p.state.inventory.find(i => i.id === prevInvId);
                if (prevInvItem) prevInvItem.amount++;
                else p.state.inventory.push({ id: prevInvId, amount: 1 });
            }

            // Neues Teil anziehen, aus dem Inventar entfernen
            invItem.amount--;
            if (invItem.amount <= 0) {
                p.state.inventory = p.state.inventory.filter(i => i.id !== invId);
            }
            p.state[slotField] = item.id;

            await db.query(
                `UPDATE players SET inventory=?, ${dbField}=? WHERE id=?`,
                [JSON.stringify(p.state.inventory), item.id, playerId]
            );
            sendStateToAll();

            p.ws.send(JSON.stringify({ type: "shop_info", msg: `${item.name} angezogen.` }));
        }

        if (data.type === "unwear_clothing") {
            const slot = String(data.slot || "");
            const slotField = { shirt: "shirtItemId", pants: "pantsItemId", shoes: "shoesItemId", helmet: "helmetItemId" }[slot];
            const dbField = { shirt: "shirt_item_id", pants: "pants_item_id", shoes: "shoes_item_id", helmet: "helmet_item_id" }[slot];
            if (!slotField) return;

            const currentId = p.state[slotField];
            if (!currentId) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Da trägst du gerade nichts." }));
                return;
            }

            const invId = "clothing_" + currentId;
            const invItem = p.state.inventory.find(i => i.id === invId);
            if (invItem) invItem.amount++;
            else p.state.inventory.push({ id: invId, amount: 1 });

            p.state[slotField] = null;

            await db.query(
                `UPDATE players SET inventory=?, ${dbField}=NULL WHERE id=?`,
                [JSON.stringify(p.state.inventory), playerId]
            );
            sendStateToAll();

            p.ws.send(JSON.stringify({ type: "shop_info", msg: "Ausgezogen - liegt jetzt wieder im Inventar." }));
        }

        if (data.type === "set_skin_item") {
            const skinId = data.skinId ? Number(data.skinId) : null;
            if (skinId && !skinItems.has(skinId)) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Dieser Skin existiert nicht." }));
                return;
            }

            p.state.skinItemId = skinId;
            await db.query("UPDATE players SET skin_item_id=? WHERE id=?", [skinId, playerId]);
            sendStateToAll();

            p.ws.send(JSON.stringify({ type: "shop_info", msg: skinId ? "Skin geändert." : "Skin zurückgesetzt (Emoji)." }));
        }

        if (data.type === "set_title") {
            const achievementId = data.achievementId ? Number(data.achievementId) : null;

            if (achievementId) {
                // Nur zulassen, wenn der Spieler dieses Achievement wirklich freigeschaltet hat
                // UND es einen Titel-Text hat
                const [unlocked] = await db.query(
                    "SELECT 1 FROM player_achievements WHERE player_id=? AND achievement_id=?",
                    [playerId, achievementId]
                );
                if (unlocked.length === 0 || !achievementTitles.has(achievementId)) {
                    p.ws.send(JSON.stringify({ type: "shop_error", msg: "Diesen Titel hast du nicht freigeschaltet." }));
                    return;
                }
            }

            p.state.activeTitleAchievementId = achievementId;
            await db.query("UPDATE players SET active_title_achievement_id=? WHERE id=?", [achievementId, playerId]);
            sendStateToAll();

            p.ws.send(JSON.stringify({ type: "shop_info", msg: achievementId ? "Titel gesetzt." : "Titel entfernt." }));
        }

        if (data.type === "get_my_titles") {
            const [rows] = await db.query(`
                SELECT a.id, a.name, a.title_text
                FROM player_achievements pa
                JOIN achievements a ON a.id = pa.achievement_id
                WHERE pa.player_id=? AND a.title_text IS NOT NULL AND a.title_text != ''
            `, [playerId]);

            p.ws.send(JSON.stringify({ type: "my_titles_data", titles: rows, active: p.state.activeTitleAchievementId }));
        }

        if (data.type === "gate_set_job") {
            if (!p.isAdmin || !p.adminDuty) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Nur Admins im Admin-Dienst können das festlegen." }));
                return;
            }

            const obj = findGateObjectNear(p.state.world, p.state.x, p.state.y);
            if (!obj) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Kein Tor in der Nähe." }));
                return;
            }

            let jobId = null;
            if (data.jobName && data.jobName.toLowerCase() !== "none") {
                const jobEntry = [...jobs.values()].find(j => j.name.toLowerCase() === data.jobName.toLowerCase());
                if (!jobEntry) {
                    p.ws.send(JSON.stringify({ type: "shop_error", msg: `Job "${data.jobName}" nicht gefunden.` }));
                    return;
                }
                jobId = jobEntry.id;
            }

            let gate = findGateByPosition(p.state.world, obj.x, obj.y);
            if (gate) {
                await db.query("UPDATE gates SET required_job_id=? WHERE id=?", [jobId, gate.id]);
            } else {
                await db.query(
                    "INSERT INTO gates (name, world, x, y, required_job_id) VALUES (?, ?, ?, ?, ?)",
                    ["Tor", p.state.world, obj.x, obj.y, jobId]
                );
            }
            await loadGates();

            p.ws.send(JSON.stringify({
                type: "shop_info",
                msg: jobId ? `Tor auf Job "${data.jobName}" beschränkt.` : "Job-Beschränkung entfernt."
            }));
        }

        // -----------------------------------------------------
        // JOB-MENÜ (statt Chat-Befehle: klickbares Fenster)
        // -----------------------------------------------------
        if (data.type === "job_menu_open") {
            const rank = jobRanks.get(p.state.jobRankId);
            const jobDef = rank ? jobs.get(rank.jobId) : null;
            const jobType = jobDef ? jobDef.type : null;

            if (jobType === "taxi") {
                const requests = [...taxiWaiting.entries()].map(([pid]) => ({
                    playerId: pid,
                    username: playersOnline.get(pid)?.username || "?",
                    world: playersOnline.get(pid)?.state.world || "?"
                }));
                p.ws.send(JSON.stringify({ type: "job_menu_data", jobType, jobName: jobDef.name, requests }));
            } else if (jobType === "police") {
                const wanted = [];
                const cuffed = [];
                for (const [pid, other] of playersOnline) {
                    if ((other.state.wantedLevel || 0) > 0) {
                        wanted.push({ playerId: pid, username: other.username, wantedLevel: other.state.wantedLevel, world: other.state.world });
                    }
                    if (other.state.cuffed) {
                        cuffed.push({
                            playerId: pid, username: other.username, world: other.state.world,
                            inMyCar: !!(p.drivingCarId && other.passengerCarId === p.drivingCarId),
                            inAnyCar: !!other.passengerCarId
                        });
                    }
                }
                p.ws.send(JSON.stringify({
                    type: "job_menu_data", jobType, jobName: jobDef.name, wanted, cuffed,
                    isDriving: !!p.drivingCarId
                }));
            } else if (jobType === "medic") {
                const injured = [];
                for (const [pid, other] of playersOnline) {
                    if (pid === playerId) continue;
                    if (other.state.health > 0 && other.state.health < 100) {
                        injured.push({ playerId: pid, username: other.username, health: Math.round(other.state.health), world: other.state.world });
                    }
                }
                p.ws.send(JSON.stringify({ type: "job_menu_data", jobType, jobName: jobDef.name, injured }));
            } else if (jobType === "tow") {
                let currentlyTowing = null;
                if (p.drivingCarId) {
                    for (const [, c] of cars) {
                        if (c.towedByCarId === p.drivingCarId) { currentlyTowing = c.id; break; }
                    }
                }
                p.ws.send(JSON.stringify({
                    type: "job_menu_data", jobType, jobName: jobDef.name,
                    isDriving: !!p.drivingCarId, isTowing: !!currentlyTowing
                }));
            } else if (jobType === "fire") {
                const fires = getFiresForWorld(p.state.world);
                p.ws.send(JSON.stringify({ type: "job_menu_data", jobType, jobName: jobDef.name, fires }));
            } else if (jobType === "mechanic") {
                const damaged = [];
                for (const [, c] of cars) {
                    if (c.world !== p.state.world) continue;
                    if (c.isTrailer || c.towedByCarId || c.loadedOnTrailerId) continue;
                    if ((c.health ?? 100) >= 100) continue;
                    const dist = Math.hypot(c.x - p.state.x, c.y - p.state.y);
                    if (dist > 90) continue;
                    damaged.push({
                        carId: c.id,
                        model: c.model,
                        health: Math.round(c.health ?? 100),
                        ownerName: c.ownerId ? (playersOnline.get(c.ownerId)?.username || "Besitzer offline") : "herrenlos"
                    });
                }
                p.ws.send(JSON.stringify({ type: "job_menu_data", jobType, jobName: jobDef.name, damaged }));
            } else {
                p.ws.send(JSON.stringify({ type: "job_menu_data", jobType: jobType || null, jobName: jobDef ? jobDef.name : null }));
            }
        }

        if (data.type === "medic_heal") {
            if (!isMedicJobRank(p.state.jobRankId)) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Nur Sanitäter können heilen." }));
                return;
            }

            const targetId = Number(data.targetId);
            const target = playersOnline.get(targetId);
            if (!target) return;

            if (target.state.world !== p.state.world ||
                Math.hypot(target.state.x - p.state.x, target.state.y - p.state.y) > 60) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Spieler ist nicht in der Nähe." }));
                return;
            }
            if (target.state.health <= 0 || target.state.health >= 100) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Nichts zu heilen." }));
                return;
            }

            target.state.health = 100;
            await db.query("UPDATE players SET health=100 WHERE id=?", [targetId]);
            sendStateToAll();

            target.ws.send(JSON.stringify({ type: "shop_info", msg: `Du wurdest von ${p.username} geheilt!` }));
            p.ws.send(JSON.stringify({ type: "shop_info", msg: `${target.username} geheilt!` }));
            await awardXp(playerId, p, 15, "Heilung");
        }

        if (data.type === "mechanic_repair") {
            if (!isMechanicJobRank(p.state.jobRankId)) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Nur Handwerker können vor Ort reparieren." }));
                return;
            }

            const car = cars.get(Number(data.carId));
            if (!car || car.world !== p.state.world) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Dieses Fahrzeug existiert nicht mehr." }));
                return;
            }
            if (car.isTrailer || car.towedByCarId || car.loadedOnTrailerId) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Dieses Fahrzeug kann gerade nicht repariert werden." }));
                return;
            }
            const dist = Math.hypot(car.x - p.state.x, car.y - p.state.y);
            if (dist > 90) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du musst näher am Fahrzeug sein." }));
                return;
            }
            if ((car.health ?? 100) >= 100) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Nichts zu reparieren." }));
                return;
            }

            const missingHealth = 100 - (car.health ?? 100);
            const fee = Math.round(missingHealth * getSetting("mechanic_fee_per_percent"));

            car.health = 100;
            await db.query("UPDATE cars SET health=100 WHERE id=?", [car.id]);
            sendCarsToAll();

            p.state.money += fee;
            await db.query("UPDATE players SET money=? WHERE id=?", [p.state.money, playerId]);
            sendStateToAll();

            if (car.ownerId) {
                const owner = playersOnline.get(car.ownerId);
                if (owner) owner.ws.send(JSON.stringify({ type: "shop_info", msg: `Dein Fahrzeug wurde von ${p.username} vor Ort repariert.` }));
            }

            p.ws.send(JSON.stringify({ type: "shop_info", msg: `Fahrzeug repariert! +${fee}$` }));
            await awardXp(playerId, p, 15, "Reparatur");
            await logEvent("mechanic", p.username, `Fahrzeug #${car.id} vor Ort repariert (+${fee}$)`);
        }

        // -----------------------------------------------------
        // TABLET (Polizei/Sanitäter/Feuerwehr): Personensuche, Fahndungsliste,
        // Kennzeichen-Abfrage
        // -----------------------------------------------------
        if (data.type === "tablet_search_person") {
            if (!hasTabletAccess(p.state.jobRankId)) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: tabletAccessDenyReason(p.state.jobRankId) }));
                return;
            }
            const username = String(data.username || "").trim();
            const [rows] = await db.query(
                "SELECT id, health, wanted_level, banned, jail_until FROM players WHERE username=?",
                [username]
            );
            if (rows.length === 0) {
                p.ws.send(JSON.stringify({ type: "tablet_person_result", found: false, username }));
                return;
            }
            const row = rows[0];
            const online = playersOnline.get(row.id);

            p.ws.send(JSON.stringify({
                type: "tablet_person_result",
                found: true,
                username,
                playerId: row.id,
                online: !!online,
                world: online ? online.state.world : null,
                health: online ? Math.round(online.state.health) : Math.round(row.health),
                wantedLevel: online ? (online.state.wantedLevel || 0) : (row.wanted_level || 0),
                bounty: bounties.get(row.id) || 0,
                banned: !!row.banned,
                jailed: !!(row.jail_until && Date.now() < new Date(row.jail_until).getTime())
            }));
        }

        if (data.type === "tablet_wanted_list") {
            if (!hasTabletAccess(p.state.jobRankId)) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: tabletAccessDenyReason(p.state.jobRankId) }));
                return;
            }
            const wanted = [];
            for (const [pid, other] of playersOnline) {
                if ((other.state.wantedLevel || 0) > 0) {
                    wanted.push({
                        playerId: pid, username: other.username,
                        wantedLevel: other.state.wantedLevel, world: other.state.world,
                        bounty: bounties.get(pid) || 0
                    });
                }
            }
            p.ws.send(JSON.stringify({ type: "tablet_wanted_list_result", wanted }));
        }

        if (data.type === "tablet_plate_lookup") {
            if (!hasTabletAccess(p.state.jobRankId)) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: tabletAccessDenyReason(p.state.jobRankId) }));
                return;
            }
            const plate = String(data.plate || "").trim().toUpperCase();
            let match = null;
            for (const [, c] of cars) {
                if (c.plate && c.plate.toUpperCase() === plate) { match = c; break; }
            }
            if (!match) {
                p.ws.send(JSON.stringify({ type: "tablet_plate_result", found: false, plate }));
                return;
            }

            let ownerName = "herrenlos";
            let ownerWanted = 0;
            let ownerBounty = 0;
            if (match.ownerId) {
                const [ownerRows] = await db.query("SELECT username, wanted_level FROM players WHERE id=?", [match.ownerId]);
                if (ownerRows.length > 0) {
                    ownerName = ownerRows[0].username;
                    const onlineOwner = playersOnline.get(match.ownerId);
                    ownerWanted = onlineOwner ? (onlineOwner.state.wantedLevel || 0) : (ownerRows[0].wanted_level || 0);
                    ownerBounty = bounties.get(match.ownerId) || 0;
                }
            }

            p.ws.send(JSON.stringify({
                type: "tablet_plate_result", found: true, plate,
                model: match.model, ownerName, ownerWanted, ownerBounty,
                world: match.world, isTrailer: !!match.isTrailer
            }));
        }

        if (data.type === "fire_extinguish") {
            if (!isFireJobRank(p.state.jobRankId)) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Nur die Feuerwehr kann Feuer löschen." }));
                return;
            }

            const fireId = Number(data.fireId);
            const fire = activeFires.get(fireId);
            if (!fire || fire.world !== p.state.world) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Dieses Feuer existiert nicht mehr." }));
                return;
            }

            const dist = Math.hypot(p.state.x - fire.x, p.state.y - fire.y);
            if (dist > 90) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du musst näher am Feuer sein." }));
                return;
            }

            fire.intensity -= 35;

            if (fire.intensity <= 0) {
                activeFires.delete(fireId);
                broadcastFireUpdate(fire.world);
                p.ws.send(JSON.stringify({ type: "shop_info", msg: "🧯 Feuer gelöscht!" }));
                await awardXp(playerId, p, 25, "Feuer gelöscht");

                p.state.money += 75;
                await db.query("UPDATE players SET money=? WHERE id=?", [p.state.money, playerId]);
                sendStateToAll();
                await logEvent("fire", p.username, `Feuer bei (${Math.round(fire.x)},${Math.round(fire.y)}) gelöscht`);
            } else {
                broadcastFireUpdate(fire.world);
                p.ws.send(JSON.stringify({ type: "shop_info", msg: `Feuer wird kleiner... (${fire.intensity}% übrig)` }));
            }
        }

        // -----------------------------------------------------
        // SHOP-/TANKSTELLEN-BESITZ
        // -----------------------------------------------------
        if (data.type === "shop_claim") {
            const shop = findShopNear(p.state.world, p.state.x, p.state.y);
            if (!shop) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Kein Shop in der Nähe." }));
                return;
            }
            if (shop.owner_id) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Dieser Shop gehört bereits jemandem." }));
                return;
            }
            if (p.state.money < shop.purchase_price) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: `Du brauchst ${shop.purchase_price}$ um diesen Shop zu kaufen.` }));
                return;
            }

            p.state.money -= shop.purchase_price;
            await db.query("UPDATE players SET money=? WHERE id=?", [p.state.money, playerId]);
            await db.query("UPDATE shops SET owner_id=? WHERE id=?", [playerId, shop.id]);
            await loadShops();
            sendStateToAll();

            p.ws.send(JSON.stringify({ type: "shop_info", msg: `Shop "${shop.name}" gekauft! Du bekommst jetzt 20% jedes Verkaufs.` }));
            await logEvent("shop_ownership", p.username, `Shop "${shop.name}" gekauft für ${shop.purchase_price}$`);
        }

        if (data.type === "shop_unclaim") {
            const shop = findShopNear(p.state.world, p.state.x, p.state.y);
            if (!shop || shop.owner_id !== playerId) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du bist nicht Besitzer dieses Shops." }));
                return;
            }
            await db.query("UPDATE shops SET owner_id=NULL WHERE id=?", [shop.id]);
            await loadShops();
            p.ws.send(JSON.stringify({ type: "shop_info", msg: `Shop "${shop.name}" wieder freigegeben.` }));
        }

        if (data.type === "shop_owner_info") {
            const shop = findShopNear(p.state.world, p.state.x, p.state.y);
            if (!shop) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Kein Shop in der Nähe." }));
                return;
            }
            if (!shop.owner_id) {
                addSystemLine(p, `"${shop.name}" ist unverkauft - kostet ${shop.purchase_price}$ (/shop buy).`);
            } else {
                const [rows] = await db.query("SELECT username FROM players WHERE id=?", [shop.owner_id]);
                addSystemLine(p, `"${shop.name}" gehört ${rows[0]?.username || "?"}.`);
            }
        }

        if (data.type === "station_claim") {
            const station = findGasStationNear(p.state.world, p.state.x, p.state.y);
            if (!station) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Keine Tankstelle in der Nähe." }));
                return;
            }
            if (station.owner_id) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Diese Tankstelle gehört bereits jemandem." }));
                return;
            }
            if (p.state.money < station.purchase_price) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: `Du brauchst ${station.purchase_price}$ um diese Tankstelle zu kaufen.` }));
                return;
            }

            p.state.money -= station.purchase_price;
            await db.query("UPDATE players SET money=? WHERE id=?", [p.state.money, playerId]);
            await db.query("UPDATE gas_stations SET owner_id=? WHERE id=?", [playerId, station.id]);
            await loadGasStations();
            sendStateToAll();

            p.ws.send(JSON.stringify({ type: "shop_info", msg: `Tankstelle "${station.name}" gekauft! Du bekommst jetzt 20% jeder Betankung.` }));
            await logEvent("shop_ownership", p.username, `Tankstelle "${station.name}" gekauft für ${station.purchase_price}$`);
        }

        if (data.type === "station_unclaim") {
            const station = findGasStationNear(p.state.world, p.state.x, p.state.y);
            if (!station || station.owner_id !== playerId) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du bist nicht Besitzer dieser Tankstelle." }));
                return;
            }
            await db.query("UPDATE gas_stations SET owner_id=NULL WHERE id=?", [station.id]);
            await loadGasStations();
            p.ws.send(JSON.stringify({ type: "shop_info", msg: `Tankstelle "${station.name}" wieder freigegeben.` }));
        }

        if (data.type === "station_owner_info") {
            const station = findGasStationNear(p.state.world, p.state.x, p.state.y);
            if (!station) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Keine Tankstelle in der Nähe." }));
                return;
            }
            if (!station.owner_id) {
                addSystemLine(p, `"${station.name}" ist unverkauft - kostet ${station.purchase_price}$ (/station buy).`);
            } else {
                const [rows] = await db.query("SELECT username FROM players WHERE id=?", [station.owner_id]);
                addSystemLine(p, `"${station.name}" gehört ${rows[0]?.username || "?"}.`);
            }
        }

        // -----------------------------------------------------
        // ABSCHLEPPSYSTEM
        // -----------------------------------------------------
        if (data.type === "tow_hook") {
            if (!p.drivingCarId) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du musst dazu einen Abschleppwagen fahren." }));
                return;
            }
            if (!isTowJobRank(p.state.jobRankId)) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Nur Abschlepper können Fahrzeuge anhängen." }));
                return;
            }

            const truck = cars.get(p.drivingCarId);
            if (!truck) return;

            let alreadyTowing = false;
            for (const [, c] of cars) {
                if (c.towedByCarId === truck.id) { alreadyTowing = true; break; }
            }
            if (alreadyTowing) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du ziehst bereits ein Fahrzeug." }));
                return;
            }

            let target = null;
            for (const [, c] of cars) {
                if (c.id === truck.id) continue;
                if (c.driverId || c.passengerId || c.towedByCarId) continue;
                if (c.world !== truck.world) continue;
                const dist = Math.hypot(c.x - truck.x, c.y - truck.y);
                if (dist < 60) { target = c; break; }
            }

            if (!target) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Kein freistehendes Fahrzeug in der Nähe zum Abschleppen." }));
                return;
            }

            target.towedByCarId = truck.id;
            sendCarsToAll();
            p.ws.send(JSON.stringify({ type: "shop_info", msg: "Fahrzeug angehängt - bring es zum Abschlepphof." }));
        }

        if (data.type === "tow_dropoff") {
            if (!p.drivingCarId) return;
            const truck = cars.get(p.drivingCarId);
            if (!truck) return;

            let towed = null;
            for (const [, c] of cars) {
                if (c.towedByCarId === truck.id) { towed = c; break; }
            }
            if (!towed) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du ziehst gerade kein Fahrzeug." }));
                return;
            }

            const lot = findImpoundLotNear(truck.world, truck.x, truck.y);
            if (!lot) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du musst dazu am Abschlepphof sein." }));
                return;
            }

            towed.towedByCarId = null;
            towed.x = lot.x * 32 + 16;
            towed.y = lot.y * 32 + 48;
            towed.world = lot.world;
            towed.speed = 0;

            if (towed.ownerId) {
                await db.query("UPDATE cars SET x=?, y=?, world=? WHERE id=?", [towed.x, towed.y, towed.world, towed.id]);
            }
            sendCarsToAll();

            const fee = getSetting("tow_fee");
            p.state.money += fee;
            await db.query("UPDATE players SET money=? WHERE id=?", [p.state.money, playerId]);
            sendStateToAll();

            p.ws.send(JSON.stringify({ type: "shop_info", msg: `Fahrzeug abgeliefert! +${fee}$` }));
            await logEvent("tow", p.username, `Fahrzeug zum Abschlepphof "${lot.name}" gebracht`);
        }

        if (data.type === "tow_release") {
            if (!p.drivingCarId) return;
            const truck = cars.get(p.drivingCarId);
            if (!truck) return;

            let found = false;
            for (const [, c] of cars) {
                if (c.towedByCarId === truck.id) {
                    c.towedByCarId = null;
                    found = true;
                    break;
                }
            }

            if (found) {
                sendCarsToAll();
                p.ws.send(JSON.stringify({ type: "shop_info", msg: "Fahrzeug losgelassen." }));
            } else {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du ziehst gerade kein Fahrzeug." }));
            }
        }

        // -----------------------------------------------------
        // ANHÄNGER: kaufen, ankuppeln, abkuppeln (jedes Fahrzeug kann ziehen,
        // nicht nur der Abschleppwagen - nutzt dieselbe Zug-Physik)
        // -----------------------------------------------------
        if (data.type === "trailer_buy") {
            const price = getSetting("trailer_price");
            if (p.state.money < price) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: `Nicht genug Geld (${price}$ nötig).` }));
                return;
            }

            const model = carConfigs["anhaenger"] ? "anhaenger" : Object.keys(carConfigs)[0];
            if (!model) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Kein Anhänger-Modell konfiguriert (Admin muss eins in der Auto-Verwaltung anlegen)." }));
                return;
            }

            p.state.money -= price;
            await db.query("UPDATE players SET money=? WHERE id=?", [p.state.money, playerId]);
            sendStateToAll();

            await spawnTrailerForPlayer(p, playerId, model);
            p.ws.send(JSON.stringify({ type: "shop_info", msg: `Anhänger gekauft! -${price}$ (steht neben dir)` }));
        }

        // -----------------------------------------------------
        // ANHÄNGER-SHOP: Kauf mit wählbarem Modell aus dem Katalog
        // -----------------------------------------------------
        if (data.type === "trailer_shop_buy") {
            const trailerShop = findTrailerShopNear(p.state.world, p.state.x, p.state.y);
            if (!trailerShop) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du musst dazu am Anhänger-Shop sein." }));
                return;
            }

            const model = String(data.model || "");
            const cfg = carConfigs[model];
            if (!cfg || !cfg.isTrailerModel || !cfg.trailerPrice) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Dieses Modell ist nicht (mehr) im Angebot." }));
                return;
            }

            if (p.state.money < cfg.trailerPrice) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: `Nicht genug Geld (${cfg.trailerPrice}$ nötig).` }));
                return;
            }

            p.state.money -= cfg.trailerPrice;
            await db.query("UPDATE players SET money=? WHERE id=?", [p.state.money, playerId]);
            sendStateToAll();

            await spawnTrailerForPlayer(p, playerId, model);
            p.ws.send(JSON.stringify({ type: "shop_info", msg: `Anhänger "${model}" gekauft! -${cfg.trailerPrice}$ (steht neben dir)` }));
        }

        if (data.type === "trailer_hitch") {
            if (!p.drivingCarId) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du musst dazu in einem Fahrzeug sitzen." }));
                return;
            }
            const towCar = cars.get(p.drivingCarId);
            if (!towCar) return;

            let alreadyTowing = false;
            for (const [, c] of cars) {
                if (c.towedByCarId === towCar.id) { alreadyTowing = true; break; }
            }
            if (alreadyTowing) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du ziehst schon etwas." }));
                return;
            }

            let target = null;
            for (const [, c] of cars) {
                if (!c.isTrailer) continue;
                if (c.towedByCarId) continue;
                if (c.world !== towCar.world) continue;
                if (c.ownerId && c.ownerId !== playerId) continue; // nur eigene oder herrenlose Anhänger
                const dist = Math.hypot(c.x - towCar.x, c.y - towCar.y);
                if (dist < 60) { target = c; break; }
            }

            if (!target) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Kein eigener Anhänger in der Nähe." }));
                return;
            }

            target.towedByCarId = towCar.id;
            sendCarsToAll();
            p.ws.send(JSON.stringify({ type: "shop_info", msg: "Anhänger angekuppelt." }));
        }

        if (data.type === "trailer_unhitch") {
            if (!p.drivingCarId) return;
            const towCar = cars.get(p.drivingCarId);
            if (!towCar) return;

            let found = false;
            for (const [, c] of cars) {
                if (c.towedByCarId === towCar.id && c.isTrailer) {
                    c.towedByCarId = null;
                    found = true;
                    break;
                }
            }

            if (found) {
                sendCarsToAll();
                p.ws.send(JSON.stringify({ type: "shop_info", msg: "Anhänger abgekuppelt." }));
            } else {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du ziehst gerade keinen Anhänger." }));
            }
        }

        // Taste K: automatisch an- oder abkuppeln, je nachdem was gerade zutrifft
        if (data.type === "trailer_toggle_hitch") {
            if (!p.drivingCarId) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du musst dazu in einem Fahrzeug sitzen." }));
                return;
            }
            const towCar = cars.get(p.drivingCarId);
            if (!towCar) return;

            let alreadyHitched = null;
            for (const [, c] of cars) {
                if (c.towedByCarId === towCar.id && c.isTrailer) { alreadyHitched = c; break; }
            }

            if (alreadyHitched) {
                alreadyHitched.towedByCarId = null;
                sendCarsToAll();
                p.ws.send(JSON.stringify({ type: "shop_info", msg: "Anhänger abgekuppelt." }));
                return;
            }

            let target = null;
            for (const [, c] of cars) {
                if (!c.isTrailer) continue;
                if (c.towedByCarId) continue;
                if (c.world !== towCar.world) continue;
                if (c.ownerId && c.ownerId !== playerId) continue;
                const dist = Math.hypot(c.x - towCar.x, c.y - towCar.y);
                if (dist < 60) { target = c; break; }
            }

            if (!target) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Kein eigener Anhänger in der Nähe." }));
                return;
            }

            target.towedByCarId = towCar.id;
            sendCarsToAll();
            p.ws.send(JSON.stringify({ type: "shop_info", msg: "Anhänger angekuppelt." }));
        }

        // -----------------------------------------------------
        // ANHÄNGER: Auto auf die Ladefläche laden/entladen (starr verschweißt,
        // im Gegensatz zur schwingenden Zugkette - sitzt einfach fest drauf)
        // -----------------------------------------------------
        if (data.type === "trailer_load_car") {
            if (!p.drivingCarId) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du musst dazu in dem Auto sitzen, das du aufladen willst." }));
                return;
            }
            const car = cars.get(p.drivingCarId);
            if (!car || car.isTrailer) return;
            if (car.towedByCarId || car.loadedOnTrailerId) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Dieses Fahrzeug wird bereits gezogen/transportiert." }));
                return;
            }

            let trailer = null;
            for (const [, c] of cars) {
                if (!c.isTrailer) continue;
                if (c.ownerId && c.ownerId !== playerId) continue; // nur eigene oder herrenlose Anhänger
                if (c.world !== car.world) continue;
                // Anhänger darf nicht schon ein anderes Auto geladen haben
                let alreadyLoaded = false;
                for (const [, other] of cars) {
                    if (other.loadedOnTrailerId === c.id) { alreadyLoaded = true; break; }
                }
                if (alreadyLoaded) continue;
                const dist = Math.hypot(c.x - car.x, c.y - car.y);
                if (dist < 70) { trailer = c; break; }
            }

            if (!trailer) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Kein freier eigener Anhänger in der Nähe." }));
                return;
            }

            // Fahrer muss aussteigen, das Auto steht ab jetzt fest auf dem Anhänger
            car.driverId = null;
            p.drivingCarId = null;
            car.loadedOnTrailerId = trailer.id;
            car.speed = 0;

            sendCarsToAll();
            p.ws.send(JSON.stringify({ type: "shop_info", msg: "Auto auf den Anhänger geladen." }));
        }

        if (data.type === "trailer_unload_car") {
            let loaded = null;
            for (const [, c] of cars) {
                if (c.loadedOnTrailerId && c.world === p.state.world) {
                    const dist = Math.hypot(c.x - p.state.x, c.y - p.state.y);
                    if (dist < 70) { loaded = c; break; }
                }
            }

            if (!loaded) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Kein aufgeladenes Auto in der Nähe." }));
                return;
            }
            if (loaded.ownerId && loaded.ownerId !== playerId) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Das ist nicht dein Auto." }));
                return;
            }

            const trailer = cars.get(loaded.loadedOnTrailerId);
            loaded.loadedOnTrailerId = null;
            if (trailer) {
                // Seitlich neben dem Anhänger absetzen, nicht mitten drin
                loaded.x = trailer.x + Math.sin(trailer.angle) * 40;
                loaded.y = trailer.y - Math.cos(trailer.angle) * 40;
                loaded.world = trailer.world;
            }

            sendCarsToAll();
            p.ws.send(JSON.stringify({ type: "shop_info", msg: "Auto vom Anhänger entladen." }));
        }

        // -----------------------------------------------------
        // FAHRZEUGVERSICHERUNG
        // -----------------------------------------------------
        // -----------------------------------------------------
        // NUMMERNSCHILD
        // -----------------------------------------------------
        if (data.type === "car_set_plate") {
            if (!p.drivingCarId) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du musst dazu in deinem Auto sitzen." }));
                return;
            }
            const c = cars.get(p.drivingCarId);
            if (!c || c.ownerId !== playerId) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Nur dein eigenes Auto kannst du beschriften." }));
                return;
            }
            if (!findPlateOfficeNear(p.state.world, p.state.x, p.state.y)) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du musst dazu bei einer Kfz-Zulassungsstelle sein." }));
                return;
            }

            const plate = String(data.plate || "").trim().toUpperCase().slice(0, 10);
            if (!/^[A-Z0-9 \-]{2,10}$/.test(plate)) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Ungültiges Nummernschild (2-10 Zeichen, Buchstaben/Zahlen/Leerzeichen/Bindestrich)." }));
                return;
            }

            const price = getSetting("plate_price");
            if (p.state.money < price) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: `Nicht genug Geld (${price}$ nötig).` }));
                return;
            }

            p.state.money -= price;
            await db.query("UPDATE players SET money=? WHERE id=?", [p.state.money, playerId]);
            sendStateToAll();

            c.plate = plate;
            await db.query("UPDATE cars SET plate=? WHERE id=?", [plate, c.id]);
            sendCarsToAll();

            p.ws.send(JSON.stringify({ type: "shop_info", msg: `Nummernschild "${plate}" gesetzt! -${price}$` }));
        }

        if (data.type === "car_insure" || data.type === "car_uninsure") {
            if (!p.drivingCarId) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du musst dazu in deinem Auto sitzen." }));
                return;
            }
            const c = cars.get(p.drivingCarId);
            if (!c || c.ownerId !== playerId) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Nur dein eigenes Auto kannst du versichern." }));
                return;
            }
            if (!findInsuranceOfficeNear(p.state.world, p.state.x, p.state.y)) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du musst dazu bei einem Versicherungsbüro sein." }));
                return;
            }

            const wantInsured = data.type === "car_insure";
            if (c.insured === wantInsured) {
                p.ws.send(JSON.stringify({ type: "shop_info", msg: wantInsured ? "Bereits versichert." : "Bereits unversichert." }));
                return;
            }

            c.insured = wantInsured;
            await db.query("UPDATE cars SET insured=? WHERE id=?", [wantInsured ? 1 : 0, c.id]);
            sendCarsToAll();

            p.ws.send(JSON.stringify({
                type: "shop_info",
                msg: wantInsured
                    ? `Versichert! Alle ${getSetting("insurance_fee_interval_minutes")} Minuten werden ${getSetting("insurance_fee_amount")}$ von deiner Bank abgebucht, dafür ${getSetting("insurance_repair_discount_percent")}% Rabatt bei Totalschaden-Reparatur.`
                    : "Versicherung gekündigt."
            }));
        }

        // -----------------------------------------------------
        // FAHRZEUG-TUNING (an der Werkstatt)
        // -----------------------------------------------------
        if (data.type === "tune_menu_open") {
            if (!p.drivingCarId) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du musst dazu in deinem Auto sitzen." }));
                return;
            }
            const c = cars.get(p.drivingCarId);
            if (!c || c.ownerId !== playerId) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Nur dein eigenes Auto kannst du tunen." }));
                return;
            }
            const repairShop = findRepairShopNear(c.world, c.x, c.y);
            if (!repairShop) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du musst dazu an einer Werkstatt sein." }));
                return;
            }

            const maxLevel = getSetting("tuning_max_level");
            const costPerLevel = getSetting("tuning_cost_per_level");

            p.ws.send(JSON.stringify({
                type: "tune_menu_data",
                maxLevel,
                paintCost: getSetting("tuning_paint_cost"),
                levels: {
                    speed: c.tuningSpeed || 0,
                    accel: c.tuningAccel || 0,
                    brake: c.tuningBrake || 0
                },
                costs: {
                    speed: (c.tuningSpeed || 0) < maxLevel ? costPerLevel * ((c.tuningSpeed || 0) + 1) : null,
                    accel: (c.tuningAccel || 0) < maxLevel ? costPerLevel * ((c.tuningAccel || 0) + 1) : null,
                    brake: (c.tuningBrake || 0) < maxLevel ? costPerLevel * ((c.tuningBrake || 0) + 1) : null
                },
                currentColor: c.paintColor || null
            }));
        }

        if (data.type === "tune_upgrade") {
            if (!p.drivingCarId) return;
            const c = cars.get(p.drivingCarId);
            if (!c || c.ownerId !== playerId) return;
            if (!findRepairShopNear(c.world, c.x, c.y)) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du musst dazu an einer Werkstatt sein." }));
                return;
            }

            const stat = data.stat; // "speed" | "accel" | "brake"
            const field = { speed: "tuningSpeed", accel: "tuningAccel", brake: "tuningBrake" }[stat];
            const dbField = { speed: "tuning_speed", accel: "tuning_accel", brake: "tuning_brake" }[stat];
            if (!field) return;

            const maxLevel = getSetting("tuning_max_level");
            const currentLevel = c[field] || 0;
            if (currentLevel >= maxLevel) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Bereits maximal ausgebaut." }));
                return;
            }

            const cost = getSetting("tuning_cost_per_level") * (currentLevel + 1);
            if (p.state.money < cost) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: `Nicht genug Geld (${cost}$ nötig).` }));
                return;
            }

            p.state.money -= cost;
            c[field] = currentLevel + 1;

            await db.query("UPDATE players SET money=? WHERE id=?", [p.state.money, playerId]);
            await db.query(`UPDATE cars SET ${dbField}=? WHERE id=?`, [c[field], c.id]);

            sendStateToAll();
            sendCarsToAll();
            p.ws.send(JSON.stringify({ type: "shop_info", msg: `${stat === "speed" ? "Motor" : stat === "accel" ? "Beschleunigung" : "Bremsen"} auf Stufe ${c[field]} aufgewertet! -${cost}$` }));
        }

        if (data.type === "tune_paint") {
            if (!p.drivingCarId) return;
            const c = cars.get(p.drivingCarId);
            if (!c || c.ownerId !== playerId) return;
            if (!findRepairShopNear(c.world, c.x, c.y)) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du musst dazu an einer Werkstatt sein." }));
                return;
            }

            const color = String(data.color || "");
            if (!/^#[0-9a-fA-F]{6}$/.test(color)) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Ungültige Farbe." }));
                return;
            }

            const cost = getSetting("tuning_paint_cost");
            if (p.state.money < cost) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: `Nicht genug Geld (${cost}$ nötig).` }));
                return;
            }

            p.state.money -= cost;
            c.paintColor = color;

            await db.query("UPDATE players SET money=? WHERE id=?", [p.state.money, playerId]);
            await db.query("UPDATE cars SET paint_color=? WHERE id=?", [color, c.id]);

            sendStateToAll();
            sendCarsToAll();
            p.ws.send(JSON.stringify({ type: "shop_info", msg: `Neu lackiert! -${cost}$` }));
        }

        if (data.type === "gate_give_key" || data.type === "gate_revoke_key") {
            const obj = findGateObjectNear(p.state.world, p.state.x, p.state.y);
            if (!obj) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du musst dazu am Tor stehen." }));
                return;
            }

            const gate = findGateByPosition(p.state.world, obj.x, obj.y);
            if (!gate || gate.owner_id !== playerId) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du musst Besitzer dieses Tores sein." }));
                return;
            }

            const [targetRows] = await db.query("SELECT id FROM players WHERE username=? OR id=?", [data.username, data.username]);
            if (targetRows.length === 0) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Spieler nicht gefunden." }));
                return;
            }
            const targetId = targetRows[0].id;

            if (data.type === "gate_give_key") {
                await db.query("INSERT IGNORE INTO gate_keys (gate_id, player_id) VALUES (?, ?)", [gate.id, targetId]);
                p.ws.send(JSON.stringify({ type: "shop_info", msg: `Tor-Schlüssel an ${data.username} vergeben.` }));
            } else {
                await db.query("DELETE FROM gate_keys WHERE gate_id=? AND player_id=?", [gate.id, targetId]);
                p.ws.send(JSON.stringify({ type: "shop_info", msg: `Tor-Schlüssel von ${data.username} entzogen.` }));
            }
        }

        // GESCHÜTZTE JOBS: Einladung durch bestehende Mitglieder
        // -----------------------------------------------------
        if (data.type === "job_invite") {
            const myRank = jobRanks.get(p.state.jobRankId);
            const myJob = myRank ? jobs.get(myRank.jobId) : null;

            if (!myJob || !myJob.protected) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du hast keinen geschützten Job, aus dem du einladen kannst." }));
                return;
            }

            const [targetRows] = await db.query("SELECT id FROM players WHERE username=? OR id=?", [data.username, data.username]);
            if (targetRows.length === 0) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Spieler nicht gefunden." }));
                return;
            }
            const targetId = targetRows[0].id;

            const entryRank = [...jobRanks.values()].find(r => r.jobId === myJob.id && r.level === 1);
            if (!entryRank) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Für diesen Job existiert kein Einstiegsrang." }));
                return;
            }

            await db.query("UPDATE players SET job_rank_id=? WHERE id=?", [entryRank.id, targetId]);
            await logEvent("job_invite", p.username, `${data.username} in geschützten Job "${myJob.name}" aufgenommen`);

            const target = playersOnline.get(targetId);
            if (target) {
                target.state.jobRankId = entryRank.id;
                target.ws.send(JSON.stringify({ type: "shop_info", msg: `Du wurdest in den Job "${myJob.name}" aufgenommen! (${entryRank.title})` }));
                sendStateToAll();
            }

            p.ws.send(JSON.stringify({ type: "shop_info", msg: `${data.username} in "${myJob.name}" aufgenommen.` }));
        }

        // -----------------------------------------------------
        // BANDEN/FRAKTIONEN
        // -----------------------------------------------------
        if (data.type === "gang_create") {
            const existing = await getPlayerGang(playerId);
            if (existing) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du bist bereits in einer Bande." }));
                return;
            }

            const name = String(data.name || "").trim().slice(0, 50);
            const tag = String(data.tag || "").trim().toUpperCase().slice(0, 6);
            const GANG_COST = 5000;

            if (!name || !tag) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Name und Tag sind Pflicht." }));
                return;
            }
            if (p.state.money < GANG_COST) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: `Bandengründung kostet ${GANG_COST}$.` }));
                return;
            }

            p.state.money -= GANG_COST;
            await db.query("UPDATE players SET money=? WHERE id=?", [p.state.money, playerId]);

            const color = "#" + Math.floor(Math.random() * 0xffffff).toString(16).padStart(6, "0");
            const [result] = await db.query(
                "INSERT INTO gangs (name, tag, color, bank, leader_id) VALUES (?, ?, ?, 0, ?)",
                [name, tag, color, playerId]
            );
            await db.query(
                "INSERT INTO gang_members (gang_id, player_id, role) VALUES (?, ?, 'leader')",
                [result.insertId, playerId]
            );

            await refreshPlayerGangInfo(playerId);
            sendStateToAll();
            p.ws.send(JSON.stringify({ type: "shop_info", msg: `Bande "${name}" [${tag}] gegründet!` }));
            await unlockAchievement(playerId, p, "gang_leader");
            await logEvent("gang", p.username, `Bande "${name}" [${tag}] gegründet`);
        }

        if (data.type === "gang_invite") {
            const gang = await getPlayerGang(playerId);
            if (!gang || gang.leader_id !== playerId) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Nur der Anführer kann einladen." }));
                return;
            }

            const [targetRows] = await db.query("SELECT id FROM players WHERE username=? OR id=?", [data.username, data.username]);
            if (targetRows.length === 0) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Spieler nicht gefunden." }));
                return;
            }
            const targetId = targetRows[0].id;

            const targetGang = await getPlayerGang(targetId);
            if (targetGang) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Spieler ist schon in einer Bande." }));
                return;
            }

            await db.query("INSERT IGNORE INTO gang_invites (gang_id, player_id) VALUES (?, ?)", [gang.id, targetId]);
            p.ws.send(JSON.stringify({ type: "shop_info", msg: `Einladung an ${data.username} verschickt.` }));

            const target = playersOnline.get(targetId);
            if (target) {
                target.ws.send(JSON.stringify({
                    type: "shop_info",
                    msg: `Du wurdest in die Bande "${gang.name}" [${gang.tag}] eingeladen - /gang accept ${gang.name}`
                }));
            }
        }

        if (data.type === "gang_accept") {
            const existing = await getPlayerGang(playerId);
            if (existing) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du bist bereits in einer Bande." }));
                return;
            }

            const [gangRows] = await db.query("SELECT * FROM gangs WHERE name=?", [data.gangName]);
            if (gangRows.length === 0) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Bande nicht gefunden." }));
                return;
            }
            const gang = gangRows[0];

            const [inviteRows] = await db.query(
                "SELECT 1 FROM gang_invites WHERE gang_id=? AND player_id=?", [gang.id, playerId]
            );
            if (inviteRows.length === 0) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Keine Einladung von dieser Bande gefunden." }));
                return;
            }

            await db.query("INSERT INTO gang_members (gang_id, player_id, role) VALUES (?, ?, 'member')", [gang.id, playerId]);
            await db.query("DELETE FROM gang_invites WHERE gang_id=? AND player_id=?", [gang.id, playerId]);

            await refreshPlayerGangInfo(playerId);
            sendStateToAll();
            p.ws.send(JSON.stringify({ type: "shop_info", msg: `Du bist jetzt Mitglied von "${gang.name}" [${gang.tag}]!` }));

            const leader = playersOnline.get(gang.leader_id);
            if (leader) {
                leader.ws.send(JSON.stringify({ type: "shop_info", msg: `${p.username} ist der Bande beigetreten.` }));
            }
        }

        if (data.type === "gang_kick") {
            const gang = await getPlayerGang(playerId);
            if (!gang || gang.leader_id !== playerId) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Nur der Anführer kann rauswerfen." }));
                return;
            }

            const [targetRows] = await db.query("SELECT id FROM players WHERE username=? OR id=?", [data.username, data.username]);
            if (targetRows.length === 0) return;
            const targetId = targetRows[0].id;

            if (targetId === playerId) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du kannst dich nicht selbst rauswerfen - nutze /gang leave." }));
                return;
            }

            await db.query("DELETE FROM gang_members WHERE gang_id=? AND player_id=?", [gang.id, targetId]);
            await refreshPlayerGangInfo(targetId);
            sendStateToAll();

            p.ws.send(JSON.stringify({ type: "shop_info", msg: `${data.username} aus der Bande entfernt.` }));
            const target = playersOnline.get(targetId);
            if (target) {
                target.ws.send(JSON.stringify({ type: "shop_info", msg: `Du wurdest aus "${gang.name}" entfernt.` }));
            }
        }

        if (data.type === "gang_leave") {
            const gang = await getPlayerGang(playerId);
            if (!gang) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du bist in keiner Bande." }));
                return;
            }

            if (gang.leader_id === playerId) {
                // Anführer verlässt -> Bande wird komplett aufgelöst
                const [members] = await db.query("SELECT player_id FROM gang_members WHERE gang_id=?", [gang.id]);
                await db.query("UPDATE territory_zones SET owner_gang_id=NULL WHERE owner_gang_id=?", [gang.id]);
                await db.query("DELETE FROM gang_members WHERE gang_id=?", [gang.id]);
                await db.query("DELETE FROM gang_invites WHERE gang_id=?", [gang.id]);
                await db.query("DELETE FROM gangs WHERE id=?", [gang.id]);
                await loadTerritoryZones();

                for (const m of members) {
                    await refreshPlayerGangInfo(m.player_id);
                    const mp = playersOnline.get(m.player_id);
                    if (mp) mp.ws.send(JSON.stringify({ type: "shop_info", msg: `Bande "${gang.name}" wurde aufgelöst.` }));
                }
                sendStateToAll();
            } else {
                await db.query("DELETE FROM gang_members WHERE gang_id=? AND player_id=?", [gang.id, playerId]);
                await refreshPlayerGangInfo(playerId);
                sendStateToAll();
                p.ws.send(JSON.stringify({ type: "shop_info", msg: `Du hast "${gang.name}" verlassen.` }));
            }
        }

        if (data.type === "gang_deposit") {
            const gang = await getPlayerGang(playerId);
            if (!gang) return;
            const amount = Math.max(0, Math.floor(Number(data.amount) || 0));
            if (amount <= 0 || amount > p.state.money) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Ungültiger Betrag." }));
                return;
            }

            p.state.money -= amount;
            await db.query("UPDATE players SET money=? WHERE id=?", [p.state.money, playerId]);
            await db.query("UPDATE gangs SET bank=bank+? WHERE id=?", [amount, gang.id]);

            sendStateToAll();
            p.ws.send(JSON.stringify({ type: "shop_info", msg: `${amount}$ in die Bandenkasse eingezahlt.` }));
        }

        if (data.type === "gang_withdraw") {
            const gang = await getPlayerGang(playerId);
            if (!gang || gang.leader_id !== playerId) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Nur der Anführer kann abheben." }));
                return;
            }
            const amount = Math.max(0, Math.floor(Number(data.amount) || 0));
            if (amount <= 0 || amount > gang.bank) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Ungültiger Betrag." }));
                return;
            }

            await db.query("UPDATE gangs SET bank=bank-? WHERE id=?", [amount, gang.id]);
            p.state.money += amount;
            await db.query("UPDATE players SET money=? WHERE id=?", [p.state.money, playerId]);

            sendStateToAll();
            p.ws.send(JSON.stringify({ type: "shop_info", msg: `${amount}$ aus der Bandenkasse abgehoben.` }));
        }

        if (data.type === "gang_info") {
            const gang = await getPlayerGang(playerId);
            if (!gang) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du bist in keiner Bande." }));
                return;
            }
            const [members] = await db.query(
                "SELECT p.username, gm.role FROM gang_members gm JOIN players p ON p.id = gm.player_id WHERE gm.gang_id=?",
                [gang.id]
            );
            addSystemLine(p, `📛 Bande: ${gang.name} [${gang.tag}] - Kasse: ${gang.bank}$`);
            addSystemLine(p, `Mitglieder: ${members.map(m => `${m.username}${m.role === "leader" ? " 👑" : ""}`).join(", ")}`);
        }

        // -----------------------------------------------------
        // PVP-INFO (kein Schalter mehr - PvP ist standardmäßig an,
        // außer auf als "sicher" markierten Kacheln)
        // -----------------------------------------------------
        if (data.type === "toggle_pvp") {
            const safe = isInSafeZone(p.state.world, p.state.x, p.state.y);
            p.ws.send(JSON.stringify({
                type: "shop_info",
                msg: safe
                    ? "🛡️ Du bist in einer Sicherheitszone - hier kann dich niemand angreifen."
                    : "⚔️ PvP ist hier aktiv - du kannst angegriffen werden und andere angreifen."
            }));
        }

        // -----------------------------------------------------
        // ANGRIFF (Nahkampf/Fernkampf mit Waffen-Item)
        // -----------------------------------------------------
        if (data.type === "attack") {
            if (p.state.health <= 0) return;
            if (p.state.cuffed) return; // gefesselt, kann nicht angreifen
            if (isInSafeZone(p.state.world, p.state.x, p.state.y)) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "🛡️ In einer Sicherheitszone kannst du nicht angreifen." }));
                return;
            }

            const weaponId = data.weaponId;
            const invItem = p.state.inventory.find(i => i.id === weaponId && i.amount > 0);
            if (!invItem) return;

            const [itemRows] = await db.query("SELECT * FROM items WHERE id=?", [weaponId]);
            if (itemRows.length === 0) return;
            const weapon = itemRows[0];

            if (weapon.type !== "weapon_melee" && weapon.type !== "weapon_ranged") {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Das ist keine Waffe." }));
                return;
            }

            const now = Date.now();
            const ATTACK_COOLDOWN = 900;
            if (p.lastAttackAt && now - p.lastAttackAt < ATTACK_COOLDOWN) return;
            p.lastAttackAt = now;

            const range = weapon.type === "weapon_melee" ? 45 : Math.max(45, weapon.weapon_range || 300);

            let target = null;
            let targetId = null;
            for (const [pid, other] of playersOnline) {
                if (pid === playerId) continue;
                if (other.state.world !== p.state.world) continue;
                if (isInSafeZone(other.state.world, other.state.x, other.state.y)) continue;
                if (other.state.health <= 0) continue;
                const dist = Math.hypot(other.state.x - p.state.x, other.state.y - p.state.y);
                if (dist <= range) {
                    target = other;
                    targetId = pid;
                    break;
                }
            }

            if (!target) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Kein Ziel in Reichweite (oder es steht in einer Sicherheitszone)." }));
                return;
            }

            const damage = Math.max(1, weapon.damage || 10);
            target.state.health = Math.max(0, target.state.health - damage);

            await db.query("UPDATE players SET health=? WHERE id=?", [target.state.health, targetId]);
            sendStateToAll();

            p.ws.send(JSON.stringify({ type: "shop_info", msg: `Treffer! -${damage} Leben (${weapon.name})` }));
            target.ws.send(JSON.stringify({ type: "shop_error", msg: `Du wurdest von ${p.username} getroffen! -${damage} Leben` }));

            if (target.state.health <= 0 && !target.isDying) {
                target.isDying = true;
                await killPlayer(targetId, target, p.username, playerId);
                setTimeout(() => { target.isDying = false; }, 3500);

                p.state.wantedLevel = Math.min(5, (p.state.wantedLevel || 0) + 2);
                await db.query("UPDATE players SET wanted_level=? WHERE id=?", [p.state.wantedLevel, playerId]);
                sendStateToAll();
            }
        }

        // -----------------------------------------------------
        // SERVER-NEUSTART MIT ANKÜNDIGUNG (nur Admins)
        // -----------------------------------------------------
        if (data.type === "admin_restart_server") {
            if (!p.isAdmin || !p.adminDuty) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Du musst dazu im Admin-Dienst sein." }));
                return;
            }

            if (data.cancel) {
                if (cancelServerRestartCountdown()) {
                    // Meldung wird schon innerhalb von cancelServerRestartCountdown() gebroadcastet
                } else {
                    p.ws.send(JSON.stringify({ type: "shop_error", msg: "Es läuft aktuell kein Neustart-Countdown." }));
                }
                return;
            }

            if (activeRestartInterval) {
                p.ws.send(JSON.stringify({ type: "shop_error", msg: "Es läuft bereits ein Neustart-Countdown." }));
                return;
            }

            const seconds = Math.max(5, Math.min(600, Math.floor(Number(data.seconds) || 60)));
            triggerServerRestartCountdown(seconds, p.username);
            p.ws.send(JSON.stringify({ type: "shop_info", msg: `Neustart in ${seconds}s eingeleitet.` }));
        }

        // -----------------------------------------------------
        // CHAT
        // -----------------------------------------------------
        if (data.type === "chat_send") {
            const text = String(data.text || "").trim().slice(0, 300);
            if (!text) return;

            if (p.state.mutedUntil && Date.now() < p.state.mutedUntil) {
                const remain = Math.ceil((p.state.mutedUntil - Date.now()) / 1000);
                p.ws.send(JSON.stringify({ type: "shop_error", msg: `Du bist noch ${remain}s stummgeschaltet.` }));
                return;
            }

            const chatMsg = JSON.stringify({
                type: "chat_message",
                from: p.username || "???",
                id: playerId,
                text,
                system: false
            });

            for (const [, other] of playersOnline) {
                other.ws.send(chatMsg);
            }
        }
    });

    ws.on("close", async () => {
        if (!playerId) return;

        const p = playersOnline.get(playerId);

        // Nur aufräumen, wenn dieser Socket noch die aktuell registrierte
        // Verbindung für diesen Spieler ist (schützt vor Race Conditions
        // bei schnellem Reconnect/Neuladen)
        if (p && p.ws === ws) {
            if (p.drivingCarId) {
                const c = cars.get(p.drivingCarId);
                if (c) c.driverId = null;
            }
            if (p.passengerCarId) {
                const c = cars.get(p.passengerCarId);
                if (c) c.passengerId = null;
            }

            // Laufende Anrufe (klingelnd) für diesen Spieler aufräumen, damit die
            // "klingelt bereits"-Sperre nicht ewig hängen bleibt
            for (const [callId, call] of pendingCalls) {
                if (call.callerId === playerId || call.targetId === playerId) {
                    clearTimeout(call.timeoutHandle);
                    pendingCalls.delete(callId);
                    const other = playersOnline.get(call.callerId === playerId ? call.targetId : call.callerId);
                    if (other) other.ws.send(JSON.stringify({ type: "call_cancelled" }));
                }
            }

            // Position sichern, falls sie seit dem letzten Batch-Speichern noch nicht in der DB steht
            if (p.positionDirty) {
                await db.query("UPDATE players SET x=?, y=?, world=? WHERE id=?", [p.state.x, p.state.y, p.state.world, playerId]);
            }

            playersOnline.delete(playerId);
            leitstelleViewers.delete(playerId);
            sendStateToAll();
            sendCarsToAll();
            maybeUpdateDiscordOnlineStatus();
            broadcastLeitstelleUpdate();
        }
    });
});

// -------------------------------------------------------------
// BEDÜRFNISSE (Hunger/Durst/Leben)
// -------------------------------------------------------------
function updatePlayerNeeds(p) {
    p.state.hunger = Number(p.state.hunger) || 0;
    p.state.thirst = Number(p.state.thirst) || 0;
    p.state.health = Number(p.state.health) || 100;

    p.state.hunger = round2(p.state.hunger - getSetting("hunger_decay_per_tick"));
    p.state.thirst = round2(p.state.thirst - getSetting("thirst_decay_per_tick"));

    if (p.state.hunger < 0) p.state.hunger = 0;
    if (p.state.thirst < 0) p.state.thirst = 0;

    // Leben reagiert auf Hunger/Durst-Zustand: über 50 beide -> Regeneration,
    // unter 50 eines von beiden -> Verfall, Geschwindigkeit jeweils einstellbar
    if (p.state.hunger > 50 && p.state.thirst > 50) {
        p.state.health = round2(p.state.health + getSetting("health_regen_per_tick"));
        if (p.state.health > 100) p.state.health = 100;
    } else if (p.state.hunger < 50 || p.state.thirst < 50) {
        p.state.health = round2(p.state.health - getSetting("health_decay_per_tick"));
        if (p.state.health < 0) p.state.health = 0;
    }

    p.state.hunger = Math.max(0, Number(p.state.hunger) || 0);
    p.state.thirst = Math.max(0, Number(p.state.thirst) || 0);
    p.state.health = Math.max(0, Number(p.state.health) || 100);
}

setInterval(async () => {
    const updatePromises = [];

    for (const [playerId, p] of playersOnline) {
        const wasAlive = p.state.health > 0;
        updatePlayerNeeds(p);

        updatePromises.push(
            db.query(
                "UPDATE players SET health=?, hunger=?, thirst=?, inventory=? WHERE id=?",
                [
                    p.state.health,
                    p.state.hunger,
                    p.state.thirst,
                    JSON.stringify(p.state.inventory),
                    playerId
                ]
            )
        );

        if (wasAlive && p.state.health <= 0 && !p.isDying) {
            p.isDying = true;
            updatePromises.push(
                killPlayer(playerId, p, null).then(() => {
                    setTimeout(() => { p.isDying = false; }, 3500);
                })
            );
        }
    }

    // Alle Spieler-Updates parallel statt nacheinander abwarten - bei einer
    // entfernten Datenbank summiert sich sequentielle Wartezeit sonst schnell auf
    await Promise.all(updatePromises);

    if (playersOnline.size > 0) sendStateToAll();
}, 1000);

// -------------------------------------------------------------
// ACHIEVEMENT-CHECK FÜR VERMÖGEN (alle 30 Sekunden reicht)
// -------------------------------------------------------------
setInterval(async () => {
    for (const [playerId, p] of playersOnline) {
        await checkAchievements(playerId, p);
    }
}, 30000);

// -------------------------------------------------------------
// DROGEN: Verkaufsstellen auch ohne Verkauf gelegentlich neu würfeln (alle 8 Minuten)
// -------------------------------------------------------------
setInterval(() => {
    for (const [drugId, drug] of drugTypes) {
        rotateDealerSpot(drugId, drug);
    }
}, 8 * 60 * 1000);

// -------------------------------------------------------------
// FEUER: Schaden für nahestehende Spieler (alle 2 Sekunden)
// -------------------------------------------------------------
setInterval(async () => {
    if (activeFires.size === 0) return;

    for (const [, fire] of activeFires) {
        for (const [playerId, p] of playersOnline) {
            if (p.state.world !== fire.world) continue;
            if (p.state.health <= 0) continue;

            const dist = Math.hypot(p.state.x - fire.x, p.state.y - fire.y);
            if (dist > FIRE_DAMAGE_RADIUS) continue;

            const wasAlive = p.state.health > 0;
            p.state.health = Math.max(0, p.state.health - 5);
            await db.query("UPDATE players SET health=? WHERE id=?", [p.state.health, playerId]);

            p.ws.send(JSON.stringify({ type: "shop_error", msg: "🔥 Du wirst vom Feuer verletzt! Geh weg davon!" }));

            if (wasAlive && p.state.health <= 0 && !p.isDying) {
                p.isDying = true;
                await killPlayer(playerId, p, null);
                setTimeout(() => { p.isDying = false; }, 3500);
            }
        }
    }
    sendStateToAll();
}, 2000);

// -------------------------------------------------------------
// SERVER-EVENTS: Ablauf erkennen und allen Bescheid geben (jede Minute)
// -------------------------------------------------------------
setInterval(() => {
    for (const type of Object.keys(activeEvents)) {
        const ev = activeEvents[type];
        if (ev.active && Date.now() > ev.endsAt) {
            ev.active = false;
            broadcastToAll(`⏰ ${ev.label} ist beendet.`, true);
            broadcastEventUpdate();
        }
    }
}, 30000);

// -------------------------------------------------------------
// ZUFALLS-EVENTS: Boden-Funde (Prüfung jede Minute, Auslösung nach
// konfiguriertem Intervall + Zufallschance)
// -------------------------------------------------------------
let lastCargoDropAttemptAt = Date.now();
setInterval(async () => {
    const intervalMs = getSetting("cargo_drop_interval_minutes") * 60 * 1000;
    if (Date.now() - lastCargoDropAttemptAt < intervalMs) return;
    lastCargoDropAttemptAt = Date.now();

    if (Math.random() * 100 < getSetting("cargo_drop_chance_percent")) {
        await trySpawnCargoDrop();
    }
}, 60000);

// -------------------------------------------------------------
// SPIELZEIT: zählt für alle Online-Spieler jede Minute hoch
// -------------------------------------------------------------
setInterval(async () => {
    const promises = [];
    for (const [playerId, p] of playersOnline) {
        p.state.playtimeSeconds = (p.state.playtimeSeconds || 0) + 60;
        promises.push(db.query("UPDATE players SET playtime_seconds=? WHERE id=?", [p.state.playtimeSeconds, playerId]));
    }
    await Promise.all(promises);
}, 60000);

// -------------------------------------------------------------
// FAHRZEUGVERSICHERUNG: Gebühr wird periodisch vom Besitzer abgebucht
// (Prüfung jede Minute, Abbuchung nach konfiguriertem Intervall)
// -------------------------------------------------------------
setInterval(async () => {
    const intervalMs = getSetting("insurance_fee_interval_minutes") * 60 * 1000;
    const fee = getSetting("insurance_fee_amount");
    const now = Date.now();

    for (const [, c] of cars) {
        if (!c.insured || !c.ownerId) continue;
        if (!c.lastInsuranceFeeAt) c.lastInsuranceFeeAt = now;
        if (now - c.lastInsuranceFeeAt < intervalMs) continue;
        c.lastInsuranceFeeAt = now;

        await adjustBank(c.ownerId, -fee);

        const owner = playersOnline.get(c.ownerId);
        if (owner) {
            owner.ws.send(JSON.stringify({
                type: "chat_message",
                system: true,
                text: `🚗 Versicherungsgebühr abgebucht: -${fee}$ (${c.model})`
            }));
        }
    }
}, 60000);

// -------------------------------------------------------------
// STEUERN: periodischer Abzug von der Bank aller Online-Spieler
// (Prüfung jede Minute, Abzug nach konfiguriertem Intervall)
// -------------------------------------------------------------
setInterval(async () => {
    const intervalMs = getSetting("tax_interval_minutes") * 60 * 1000;
    const taxPercent = getSetting("tax_percent");
    const now = Date.now();

    for (const [playerId, p] of playersOnline) {
        if (!p.lastTaxAt) p.lastTaxAt = now;
        if (now - p.lastTaxAt < intervalMs) continue;
        p.lastTaxAt = now;

        const taxAmount = round2(Number(p.state.bank) * (taxPercent / 100));
        if (taxAmount <= 0) continue;

        await adjustBank(playerId, -taxAmount);
        p.ws.send(JSON.stringify({
            type: "chat_message",
            system: true,
            text: `🏛️ Steuer abgezogen: -${taxAmount.toFixed(2)}$ (${taxPercent}% von deiner Bank)`
        }));
    }
}, 60000);

// -------------------------------------------------------------
// TANKSTELLEN: Spritpreis pendelt leicht (alle 5 Minuten, ±3% pro Schritt,
// bleibt innerhalb eines vernünftigen Rahmens)
// -------------------------------------------------------------
const GAS_PRICE_MIN = 1.20;
const GAS_PRICE_MAX = 3.00;

setInterval(async () => {
    for (const [, g] of gasStations) {
        const change = 1 + (Math.random() * 0.06 - 0.03); // ±3%
        let newPrice = Math.round(g.price * change * 100) / 100;
        newPrice = Math.max(GAS_PRICE_MIN, Math.min(GAS_PRICE_MAX, newPrice));

        g.price = newPrice;
        await db.query("UPDATE gas_stations SET price=? WHERE id=?", [newPrice, g.id]);
    }
}, 5 * 60000);

// -------------------------------------------------------------
// DISCORD: Online-Status regelmäßig auffrischen, unabhängig von
// Logins/Logouts (Sicherheitsnetz, z.B. gegen Timestamp-Veralten)
// -------------------------------------------------------------
setInterval(() => {
    updateDiscordOnlineStatus();
}, 5 * 60000);

// -------------------------------------------------------------
// IMMOBILIEN-VERMIETUNG: fällige Mieten automatisch einziehen,
// bei Zahlungsunfähigkeit wird der Mieter gekündigt (jede Minute geprüft)
// -------------------------------------------------------------
setInterval(async () => {
    const now = Date.now();

    for (const [, house] of houses) {
        if (!house.renter_id || !house.rent_due_at) continue;
        if (new Date(house.rent_due_at).getTime() > now) continue;

        const renterId = house.renter_id;
        const renterOnline = playersOnline.get(renterId);
        const intervalMinutes = getSetting("house_rent_interval_minutes");

        // Aktuellen Kontostand ermitteln (online: RAM-Stand, offline: aus der DB)
        let currentMoney;
        if (renterOnline) {
            currentMoney = renterOnline.state.money;
        } else {
            const [rows] = await db.query("SELECT money FROM players WHERE id=?", [renterId]);
            currentMoney = rows.length ? rows[0].money : 0;
        }

        if (currentMoney >= house.rent_price) {
            const newMoney = currentMoney - house.rent_price;
            if (renterOnline) {
                renterOnline.state.money = newMoney;
                renterOnline.ws.send(JSON.stringify({ type: "shop_info", msg: `🏠 Miete für "${house.name}" abgebucht: -${house.rent_price}$` }));
            }
            await db.query("UPDATE players SET money=? WHERE id=?", [newMoney, renterId]);

            const ownerOnline = playersOnline.get(house.owner_id);
            if (ownerOnline) {
                ownerOnline.state.bank += house.rent_price;
                await db.query("UPDATE players SET bank=? WHERE id=?", [ownerOnline.state.bank, house.owner_id]);
            } else {
                await db.query("UPDATE players SET bank = bank + ? WHERE id=?", [house.rent_price, house.owner_id]);
            }

            house.rent_due_at = new Date(now + intervalMinutes * 60000);
            await db.query("UPDATE houses SET rent_due_at=? WHERE id=?", [house.rent_due_at, house.id]);
        } else {
            // Miete kann nicht bezahlt werden - Mieter fliegt raus
            house.renter_id = null;
            house.rent_due_at = null;
            await db.query("UPDATE houses SET renter_id=NULL, rent_due_at=NULL WHERE id=?", [house.id]);
            await db.query("DELETE FROM house_keys WHERE house_id=? AND player_id=?", [house.id, renterId]);

            if (renterOnline) {
                renterOnline.ws.send(JSON.stringify({ type: "shop_error", msg: `🏠 Du konntest die Miete für "${house.name}" nicht bezahlen und wurdest gekündigt.` }));
            }
            const ownerOnline = playersOnline.get(house.owner_id);
            if (ownerOnline) {
                ownerOnline.ws.send(JSON.stringify({ type: "shop_info", msg: `Mieter von "${house.name}" konnte nicht zahlen und wurde gekündigt - wieder frei zur Vermietung.` }));
            }
            await logEvent("house_rent", "system", `Mieter #${renterId} aus Haus "${house.name}" wegen Zahlungsunfähigkeit gekündigt`);
        }
    }

    sendStateToAll();
}, 60000);

// -------------------------------------------------------------
// AUKTIONSHAUS: abgelaufene Angebote abwickeln (jede Minute)
// -------------------------------------------------------------
setInterval(async () => {
    const [rows] = await db.query("SELECT * FROM auctions WHERE status='active' AND ends_at <= NOW()");

    for (const auction of rows) {
        if (auction.current_bidder_id) {
            await addItemToInventory(auction.current_bidder_id, auction.item_id, auction.amount, true);
            await adjustMoney(auction.seller_id, auction.current_bid);
            await db.query("UPDATE auctions SET status='sold' WHERE id=?", [auction.id]);
        } else {
            await addItemToInventory(auction.seller_id, auction.item_id, auction.amount, true);
            await db.query("UPDATE auctions SET status='expired' WHERE id=?", [auction.id]);
        }
    }
}, 60000);

// -------------------------------------------------------------
// IMMOBILIENMARKT: abgelaufene Angebote abwickeln (jede Minute)
// -------------------------------------------------------------
setInterval(async () => {
    const [rows] = await db.query("SELECT * FROM house_listings WHERE status='active' AND ends_at <= NOW()");

    for (const listing of rows) {
        if (listing.current_bidder_id) {
            await db.query("UPDATE houses SET owner_id=? WHERE id=?", [listing.current_bidder_id, listing.house_id]);
            await db.query("DELETE FROM house_keys WHERE house_id=?", [listing.house_id]);
            await adjustMoney(listing.seller_id, listing.current_bid);
            await db.query("UPDATE house_listings SET status='sold' WHERE id=?", [listing.id]);
            await loadHouses();
        } else {
            await db.query("UPDATE house_listings SET status='expired' WHERE id=?", [listing.id]);
        }
    }
}, 60000);

// -------------------------------------------------------------
// GEHALTSAUSZAHLUNG (Prüfung jede Minute, Auszahlung nach konfiguriertem Intervall)
// -------------------------------------------------------------
setInterval(async () => {
    let changed = false;
    const intervalMs = getSetting("salary_interval_minutes") * 60 * 1000;
    const now = Date.now();

    for (const [playerId, p] of playersOnline) {
        if (!p.state.jobRankId) continue;

        const rank = jobRanks.get(p.state.jobRankId);
        if (!rank || !rank.salary) continue;

        if (!p.lastSalaryAt) p.lastSalaryAt = now;
        if (now - p.lastSalaryAt < intervalMs) continue;
        p.lastSalaryAt = now;

        p.state.bank += rank.salary;
        changed = true;

        await db.query("UPDATE players SET bank=? WHERE id=?", [p.state.bank, playerId]);

        p.ws.send(JSON.stringify({
            type: "chat_message",
            system: true,
            text: `💰 Gehalt erhalten: +${rank.salary}$ (${rank.title}) - jetzt auf deinem Bankkonto`
        }));
    }

    if (changed) sendStateToAll();
}, 60000);

// -------------------------------------------------------------
// UHRZEIT FORTSCHREIBEN (jede Sekunde ein kleines Stück weiter)
// -------------------------------------------------------------
setInterval(() => {
    gameHour = (gameHour + 24 / (DAY_LENGTH_MINUTES * 60)) % 24;
}, 1000);

// -------------------------------------------------------------
// FAHNDUNGSLEVEL BAUT MIT DER ZEIT AB
// -------------------------------------------------------------
setInterval(async () => {
    let changed = false;
    for (const [playerId, p] of playersOnline) {
        if (!p.state.wantedLevel) continue;
        p.state.wantedLevel = Math.max(0, p.state.wantedLevel - 1);
        await db.query("UPDATE players SET wanted_level=? WHERE id=?", [p.state.wantedLevel, playerId]);
        changed = true;
    }
    if (changed) sendStateToAll();
}, 2 * 60 * 1000); // alle 2 Minuten ein Stern weniger

// -------------------------------------------------------------
// UHRZEIT + WETTER AN ALLE SENDEN (alle 5 Sekunden reicht)
// -------------------------------------------------------------
setInterval(() => {
    if (playersOnline.size === 0) return;
    const msg = JSON.stringify({ type: "world_info", hour: gameHour, weather: currentWeather });
    for (const [, p] of playersOnline) p.ws.send(msg);
}, 5000);

// -------------------------------------------------------------
// AUTO-PHYSIK (schneller Tick)
// -------------------------------------------------------------
let lastTickDurationMs = 0;

setInterval(async () => {
    const tickStart = process.hrtime.bigint();
    let changed = false;

    for (const [, c] of cars) {
        // Auf einen Anhänger geladenes Auto: sitzt fest verschweißt auf der Ladefläche,
        // dreht/bewegt sich exakt 1:1 mit dem Anhänger mit (kein Schwingen wie beim Ziehen -
        // ein aufgeladenes Auto ist ja festgezurrt)
        if (c.loadedOnTrailerId) {
            const trailer = cars.get(c.loadedOnTrailerId);
            if (trailer) {
                c.x = trailer.x;
                c.y = trailer.y;
                c.angle = trailer.angle;
                c.world = trailer.world;
            }
            c.speed = 0;
            continue;
        }

        // Abgeschlepptes Fahrzeug / Anhänger: hängt an einer "Deichsel" hinter dem
        // Zugfahrzeug - der Winkel ergibt sich aus der Richtung zum Kupplungspunkt,
        // dadurch schwingt es in Kurven natürlich nach, statt starr mitzudrehen
        if (c.towedByCarId) {
            const truck = cars.get(c.towedByCarId);
            if (truck) {
                const HITCH_OFFSET = 20; // Abstand der Kupplung von der Fahrzeugmitte des Zugfahrzeugs
                const LINK_LENGTH = 32;  // "Deichsel"-Länge zwischen Kupplung und Anhänger/abgeschlepptem Auto

                const hitchX = truck.x - Math.cos(truck.angle) * HITCH_OFFSET;
                const hitchY = truck.y - Math.sin(truck.angle) * HITCH_OFFSET;

                const dx = hitchX - c.x;
                const dy = hitchY - c.y;
                const dist = Math.hypot(dx, dy);

                if (dist > 0.01) {
                    const dirX = dx / dist;
                    const dirY = dy / dist;
                    c.angle = Math.atan2(dy, dx);
                    c.x = hitchX - dirX * LINK_LENGTH;
                    c.y = hitchY - dirY * LINK_LENGTH;
                }
                c.world = truck.world;

                // Anhänger übernimmt Blinker/Warnblinker vom Zugfahrzeug (echte Autos, die
                // nur abgeschleppt werden, haben ja keine funktionierende Elektrik mehr)
                if (c.isTrailer) {
                    c.leftBlinker = truck.leftBlinker;
                    c.rightBlinker = truck.rightBlinker;
                    c.hazard = truck.hazard;
                    c.brakeLight = truck.brakeLight;
                }
            }
            c.speed = 0;
            continue;
        }

        if (!c.driverId && !c.isNpc) continue;
        const cfg = carConfigs[c.model] || carConfigs.sedan;
        if (!cfg) continue;

        // NPC-KI: statt Spieler-Eingabe (throttle/steer) selbst ein Ziel ansteuern
        if (c.isNpc) {
            if (!c.npcTargetX || Math.hypot(c.npcTargetX - c.x, c.npcTargetY - c.y) < 40) {
                pickNewNpcTarget(c);
            }
            if (c.npcTargetX !== undefined) {
                const desiredAngle = Math.atan2(c.npcTargetY - c.y, c.npcTargetX - c.x);
                let diff = desiredAngle - c.angle;
                while (diff > Math.PI) diff -= Math.PI * 2;
                while (diff < -Math.PI) diff += Math.PI * 2;
                c.angle += Math.max(-0.05, Math.min(0.05, diff));
                c.throttle = 1;
                c.steer = 0; // Lenkung übernimmt oben direkt den Winkel
            } else {
                c.throttle = 0;
            }
        }

        const hasFuel = c.isNpc ? true : (c.fuel ?? 0) > 0;
        const healthFactor = Math.max(0.15, (c.health ?? 100) / 100); // stark beschädigt = deutlich langsamer, nie ganz 0

        // Tuning-Boni: pro Stufe ein konfigurierbarer Prozentsatz mehr (NPCs bleiben unangetastet)
        const bonusPerLevel = getSetting("tuning_bonus_per_level_percent") / 100;
        const speedBonus = c.isNpc ? 1 : (1 + (c.tuningSpeed || 0) * bonusPerLevel);
        const accelBonus = c.isNpc ? 1 : (1 + (c.tuningAccel || 0) * bonusPerLevel);
        const brakeBonus = c.isNpc ? 1 : (1 + (c.tuningBrake || 0) * bonusPerLevel);

        // Regen: deutlich längerer Bremsweg, etwas rutschigere Kurven.
        // Schnee: noch spürbar stärker (glatte Fahrbahn), Nebel hat
        // bewusst keine Fahrphysik-Auswirkung (nur Sichtweite optisch)
        const rainBrakeFactor = currentWeather === "rain" ? 0.5 : currentWeather === "snow" ? 0.35 : 1;
        const rainTurnFactor = currentWeather === "rain" ? 0.85 : currentWeather === "snow" ? 0.7 : 1;

        if (hasFuel && c.throttle > 0) c.speed += cfg.accel * accelBonus * (c.isNpc ? 0.5 : 1) * healthFactor;
        else if (c.throttle < 0) c.speed -= cfg.brake * brakeBonus * rainBrakeFactor;
        else c.speed *= (1 - cfg.friction);

        // Tank leer: kein Antrieb mehr, Auto rollt nur noch aus
        if (!hasFuel && c.throttle > 0) c.speed *= (1 - cfg.friction);

        const effectiveMaxSpeed = cfg.maxSpeed * speedBonus * healthFactor * (c.isNpc ? 0.5 : 1);
        c.speed = Math.max(-effectiveMaxSpeed / 2, Math.min(effectiveMaxSpeed, c.speed));

        if (!c.isNpc && Math.abs(c.speed) > 0.05) {
            c.angle += c.steer * cfg.turnSpeed * rainTurnFactor * (c.speed > 0 ? 1 : -1);
        }

        const nx = c.x + Math.cos(c.angle) * c.speed;
        const ny = c.y + Math.sin(c.angle) * c.speed;
        const speedBeforeCollision = c.speed;

        // AUTO-GEGEN-AUTO-KOLLISION
        let hitCar = null;
        for (const [, other] of cars) {
            if (other === c) continue;
            if (other.world !== c.world) continue;
            if (other.towedByCarId === c.id || c.towedByCarId === other.id) continue; // Abschlepp-Gespann ignorieren
            if (Math.hypot(other.x - nx, other.y - ny) < 32) { hitCar = other; break; }
        }

        if (hitCar) {
            if (Math.abs(speedBeforeCollision) > 1) {
                const dmg = Math.abs(speedBeforeCollision) * getSetting("car_collision_damage_factor");
                const otherWasAlive = (hitCar.health ?? 100) > 0;
                const selfWasAlive = (c.health ?? 100) > 0;

                hitCar.health = Math.max(0, (hitCar.health ?? 100) - dmg);
                c.health = Math.max(0, (c.health ?? 100) - dmg);
                hitCar.speed = (hitCar.speed || 0) + speedBeforeCollision * 0.25; // leichter Schubs

                if (otherWasAlive && hitCar.health <= 0 && Math.random() < 0.4) spawnFire(hitCar.world, hitCar.x, hitCar.y);
                if (selfWasAlive && c.health <= 0 && Math.random() < 0.4) spawnFire(c.world, c.x, c.y);
            }
            c.speed = 0;
            continue;
        }

        // AUTO-GEGEN-FUSSGÄNGER-KOLLISION (nur zu Fuß gehende Spieler, keine Insassen)
        let hitPedId = null;
        for (const [pid, pl] of playersOnline) {
            if (pl.state.world !== c.world) continue;
            if (pl.drivingCarId || pl.passengerCarId) continue;
            if (pl.state.health <= 0) continue;
            if (Math.hypot(pl.state.x - nx, pl.state.y - ny) < 20) { hitPedId = pid; break; }
        }

        if (hitPedId && Math.abs(speedBeforeCollision) > 1) {
            const victim = playersOnline.get(hitPedId);
            const dmg = Math.abs(speedBeforeCollision) * getSetting("pedestrian_damage_factor");
            const wasAlive = victim.state.health > 0;

            victim.state.health = Math.max(0, victim.state.health - dmg);
            await db.query("UPDATE players SET health=? WHERE id=?", [victim.state.health, hitPedId]);

            if (c.driverId) {
                const driver = playersOnline.get(c.driverId);
                if (driver) {
                    driver.state.wantedLevel = Math.min(5, (driver.state.wantedLevel || 0) + 2);
                    await db.query("UPDATE players SET wanted_level=? WHERE id=?", [driver.state.wantedLevel, c.driverId]);
                    driver.ws.send(JSON.stringify({ type: "shop_error", msg: `🚗 ${victim.username} angefahren!` }));
                }
            }
            victim.ws.send(JSON.stringify({ type: "shop_error", msg: "🚗 Von einem Auto erwischt!" }));

            if (wasAlive && victim.state.health <= 0 && !victim.isDying) {
                victim.isDying = true;
                await killPlayer(hitPedId, victim, null);
                setTimeout(() => { victim.isDying = false; }, 3500);
            }
            changed = true;
        }

        if (canMove({ state: { world: c.world } }, nx, ny)) {
            c.odometer = (c.odometer || 0) + Math.abs(c.speed) / 1000; // grobe "km"-Einheit
            c.x = nx;
            c.y = ny;
        } else if (c.isNpc) {
            // NPC bleibt nicht stecken/nimmt keinen Schaden - sucht sich einfach ein neues Ziel
            c.speed = 0;
            pickNewNpcTarget(c);
        } else {
            // Kollision: Schaden proportional zur Aufprallgeschwindigkeit
            if (Math.abs(speedBeforeCollision) > 1.5) {
                const wasAlive = (c.health ?? 100) > 0;
                const damage = Math.abs(speedBeforeCollision) * 2.5;
                c.health = Math.max(0, (c.health ?? 100) - damage);

                // Auto durch den Aufprall zerstört -> Chance auf Feuer
                if (wasAlive && c.health <= 0 && Math.random() < 0.4) {
                    spawnFire(c.world, c.x, c.y);
                }
            }
            c.speed = 0;
        }

        // Benzinverbrauch: nur wenn tatsächlich Gas gegeben wird und das Auto rollt
        if (!c.isNpc && hasFuel && c.throttle > 0 && Math.abs(c.speed) > 0.05) {
            c.fuel = Math.max(0, (c.fuel ?? 0) - (cfg.consumption || 0.02));
        }

        c.brakeLight = c.throttle < 0;

        const driver = playersOnline.get(c.driverId);
        if (driver) {
            driver.state.x = c.x;
            driver.state.y = c.y;

            const driverLink = findHighwayLinkNear(driver.state.world, c.x, c.y);
            if (driverLink) {
                teleportPlayerViaHighway(driver, c.driverId, driverLink);
            }
        }

        if (c.passengerId) {
            const passenger = playersOnline.get(c.passengerId);
            if (passenger) {
                passenger.state.x = c.x;
                passenger.state.y = c.y;
            }
        }
        changed = true;
    }

    if (changed) {
        scheduleCarsBroadcast();
    }

    lastTickDurationMs = Number(process.hrtime.bigint() - tickStart) / 1e6;
}, 50);

// -------------------------------------------------------------
// SERVER-METRIKEN: sammelt alle 60 Sekunden einen Datenpunkt
// (Spielerzahl, Tick-Dauer, RAM) in einem Ringpuffer - hält die
// letzten 24 Stunden vor, damit man Probleme wie Server-Ruckeln
// im Nachhinein nachvollziehen kann, statt blind zu suchen
// -------------------------------------------------------------
const METRICS_HISTORY_MAX = 1440; // 24h bei 1 Punkt/Minute
const metricsHistory = [];

setInterval(() => {
    const mem = process.memoryUsage();
    metricsHistory.push({
        t: Date.now(),
        players: playersOnline.size,
        cars: cars.size,
        tickMs: Math.round(lastTickDurationMs * 100) / 100,
        ramMb: Math.round(mem.rss / 1024 / 1024 * 10) / 10,
        heapMb: Math.round(mem.heapUsed / 1024 / 1024 * 10) / 10
    });
    if (metricsHistory.length > METRICS_HISTORY_MAX) metricsHistory.shift();
}, 60000);

app.get("/api/admin/metrics", requirePermission("view_metrics"), async (req, res) => {
    res.json({
        ok: true,
        current: {
            players: playersOnline.size,
            cars: cars.size,
            tickMs: Math.round(lastTickDurationMs * 100) / 100,
            uptimeSeconds: Math.round(process.uptime())
        },
        history: metricsHistory
    });
});

// -------------------------------------------------------------
// SERVER STARTEN
// -------------------------------------------------------------
server.listen(5555, () => {
    console.log("Server läuft auf Port 5555");
});