let dotenv, express, db, Economy, GameTime, pathLib, fs, virtualWorldPool, new_quest_Base;
try {
  dotenv = require('dotenv').config();
  console.log('dotenv успешно импортирован');
} catch (e) {
  console.error('Ошибка при импорте dotenv:', e);
  console.log('Продолжаем без .env файла');
}

// Устанавливаем fallback значения для критических переменных окружения
if (!process.env.JWT_SECRET) {
  process.env.JWT_SECRET = 'fallback-secret-key-for-development';
  console.warn('JWT_SECRET не найден, используем fallback ключ (НЕ ДЛЯ ПРОДАКШЕНА!)');
}

if (!process.env.DATABASE_URL) {
  process.env.DATABASE_URL = 'postgresql://postgres:password@localhost:5432/revproj';
  console.warn('DATABASE_URL не найден, используем fallback НЕ ДЛЯ ПРОДАКШЕНА!');
}

if (!process.env.DATABASE_QUEST_NEW_QUESTS) {
  process.env.DATABASE_QUEST_NEW_QUESTS = 'postgresql://postgres:password@localhost:5432/quest_system';
  console.warn('DATABASE_QUEST_NEW_QUESTS не найден, используем fallback НЕ ДЛЯ ПРОДАКШЕНА!');
}
try {
  express = require('express');
  console.log('express успешно импортирован');
} catch (e) {
  console.error('Ошибка при импорте express:', e);
  throw e;
}

try {
    virtualWorldPool = require('./db1');
    console.log('db1 - virtualWorld - успешно импротирован');
}
catch (e) {
    console.error('Ошибка при импорте db1 - virtual_World:', e);
    throw e;
}

try {
    new_quest_Base = require('./db2');
    console.log('db2 - new_quest_Base - успешно импортирован');
}
catch (e) {
    console.error('Ошибка при импорте db2 - new_quest_Base: ', e);
    throw e;
}

try {
  db = require('./db');
  console.log('db успешно импортирован');
} catch (e) {
  console.error('Ошибка при импорте db:', e);
  throw e;
}
try {
  Economy = require('./economy');
  console.log('Economy успешно импортирован');
} catch (e) {
  console.error('Ошибка при импорте economy:', e);
  throw e;
}
try {
  GameTime = require('./gameTime');
  console.log('GameTime успешно импортирован');
} catch (e) {
  console.error('Ошибка при импорте gameTime:', e);
  throw e;
}
try {
  pathLib = require('path');
  console.log('path успешно импортирован');
} catch (e) {
  console.error('Ошибка при импорте path:', e);
  throw e;
}
try {
  fs = require('fs');
  console.log('fs успешно импортирован');
} catch (e) {
  console.error('Ошибка при импорте fs:', e);
  throw e;
}

const app = express();

app.use(express.json());
app.use(express.urlencoded({ extended: true }));


const http = require('http').createServer(app);
const io = require('socket.io')(http, {
  cors: {
    origin: [
      'http://localhost:4000',
      'http://rltn.online',
      'https://rltn.online',
      'http://www.rltn.online',
      'https://www.rltn.online'
    ],
    methods: ['GET', 'POST']
  }
});

let onlineUsers = new Map();
let lastSeenTimes = new Map(); // Добавляем отслеживание времени последнего онлайн

const organizationsRouter = require('./server/organizations')(io, onlineUsers);
app.use('/api/organizations', organizationsRouter);

// Инициализация игрового времени (ускорение 8x) и вещание клиентам
try {
  if (GameTime && typeof GameTime === 'function') {
    global.__gameTimeInstance = global.__gameTimeInstance || new GameTime(io, 8);
    console.log('GameTime таймер запущен');
  }
} catch (e) {
  console.error('GameTime не запущен:', e);
}

io.use((socket, next) => {
  const token = socket.handshake.auth.token;
  if (!token) return next(new Error('No token'));
  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET);
    socket.userId = payload.id;
    onlineUsers.set(socket.userId, socket.id); // Добавить пользователя в онлайн
    
    // Обновляем время последнего онлайн
    lastSeenTimes.set(socket.userId, new Date());
    console.log(`Пользователь ${socket.userId} стал онлайн, время: ${lastSeenTimes.get(socket.userId)}`);
    
    // Уведомляем всех клиентов о том, что пользователь стал онлайн
    socket.broadcast.emit('userStatusChanged', { 
      userId: socket.userId, 
      isOnline: true 
    });
    console.log(`Отправлено событие userStatusChanged для пользователя ${socket.userId} (онлайн)`);
    
    next();
  } catch (err) {
    next(new Error('Invalid token'));
  }
});

const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');

function authenticate(req, res, next) {
  const auth = req.headers.authorization?.split(' ');
  console.log('Проверка авторизации:', { 
    hasAuth: !!auth, 
    authType: auth?.[0], 
    path: req.path 
  });
  try {
    if (!auth || auth[0] !== 'Bearer') {
      console.log('Ошибка: нет токена или неправильный формат');
      return res.status(401).send('No token');
    }
    const payload = jwt.verify(auth[1], process.env.JWT_SECRET);
    req.user = payload;
    console.log('Токен валиден, пользователь:', payload.id);
    next();
  } catch (error) {
    console.log('Ошибка валидации токена:', error.message);
    res.status(401).send('Invalid token');
  }
}

app.use(express.static(pathLib.join(__dirname, 'build')));
app.use(
  '/models',
  express.static(pathLib.join(__dirname, 'public', 'models'))
);

let players = {};

// --- Расширяем players для поддержки городов ---
let playersByCity = {};

io.on('connection', socket => {
  console.log('Player connected:', socket.id);

  // Получаем город игрока из БД
  (async () => {
    const { rows } = await db.query('SELECT last_city_id, last_pos_x, last_pos_z FROM users WHERE id = $1', [socket.userId]);
    const cityId = rows[0]?.last_city_id || 1;
    const x = rows[0]?.last_pos_x || 0;
    const z = rows[0]?.last_pos_z || 0;
    if (!playersByCity[cityId]) playersByCity[cityId] = {};
    playersByCity[cityId][socket.id] = {
      socketId: socket.id,
      userId: socket.userId,
      x,
      y: 0,
      z,
      cityId,
      avatarURL: null,
      gender: null,
      firstName: null,
      lastName: null
    };
    players[socket.id] = playersByCity[cityId][socket.id];
    socket.cityId = cityId;
    socket.x = x;
    socket.y = 0;
    socket.z = z;
    // Отправляем только игроков этого города
    socket.emit('currentPlayers', playersByCity[cityId]);
  })();

  // --- Новый игрок ---
  socket.on('newPlayer', data => {
    const cityId = socket.cityId || 1;
    if (!playersByCity[cityId]) playersByCity[cityId] = {};
    const p = playersByCity[cityId][socket.id] || {};
    Object.assign(p, {
      x: data.x,
      y: data.y ?? p.y ?? 0,
      z: data.z,
      cityId,
      avatarURL: data.avatarURL || null,
      gender: data.gender || null,
      firstName: data.firstName || '',
      lastName: data.lastName || ''
    });
    playersByCity[cityId][socket.id] = p;
    players[socket.id] = p;
    socket.cityId = cityId;
    // Сообщаем всем игрокам этого города (без учёта интерьера)
    for (const id in playersByCity[cityId]) {
      if (id === socket.id) continue;
      io.to(id).emit('newPlayer', {
        playerId: socket.id,
        x: p.x,
        y: p.y ?? 0,
        z: p.z,
        avatarURL: p.avatarURL,
        gender: p.gender,
        firstName: p.firstName,
        lastName: p.lastName
      });
    }
  });

  // --- Перемещение игрока ---
  socket.on('playerMovement', movementData => {
    const cityId = socket.cityId;
    if (playersByCity[cityId] && playersByCity[cityId][socket.id]) {
      playersByCity[cityId][socket.id].x = movementData.x;
      if (typeof movementData.y === 'number') {
        playersByCity[cityId][socket.id].y = movementData.y;
      }
      playersByCity[cityId][socket.id].z = movementData.z;
      // Сообщаем только игрокам этого города (без учёта интерьера)
      for (const id in playersByCity[cityId]) {
        if (id !== socket.id) {
          io.to(id).emit('playerMoved', {
            playerId: socket.id,
            x: movementData.x,
            y: typeof movementData.y === 'number' ? movementData.y : (playersByCity[cityId][socket.id].y ?? 0),
            z: movementData.z
          });
        }
      }
      // Voice chat nearby только в этом городе
      const norm = v => (v == null ? null : String(v));
      const sender = playersByCity[cityId][socket.id];
      for (const [id, other] of Object.entries(playersByCity[cityId])) {
        if (id === socket.id) continue;
        // Голосовой чат больше не фильтруем по интерьеру
        const dx = sender.x - other.x;
        const dz = sender.z - other.z;
        const dist = Math.sqrt(dx * dx + dz * dz);
        if (dist <= 50) {
          io.to(id).emit('voiceChatNearby', { playerId: socket.id });
          io.to(socket.id).emit('voiceChatNearby', { playerId: id });
        }
      }
    }
  });

  // --- Смена интерьера (вход/выход) ---
  socket.on('interiorChange', ({ interiorId }) => {
    const cityId = socket.cityId;
    if (!playersByCity[cityId] || !playersByCity[cityId][socket.id]) return;
    const norm = v => (v == null ? null : String(v));
    const prevInterior = playersByCity[cityId][socket.id].interiorId ?? null;
    const nextInterior = norm(interiorId);
    playersByCity[cityId][socket.id].interiorId = nextInterior;
    socket.interiorId = nextInterior;

    // Сообщаем игрокам старого интерьера, что этот игрок ушёл
    for (const id in playersByCity[cityId]) {
      if (id === socket.id) continue;
      const other = playersByCity[cityId][id];
      const samePrev = norm(other?.interiorId) === norm(prevInterior);
      if (samePrev && prevInterior !== nextInterior) {
        io.to(id).emit('playerDisconnected', socket.id);
      }
    }

    // Сообщаем всем игрокам города о появлении (без учёта интерьера)
    for (const id in playersByCity[cityId]) {
      if (id === socket.id) continue;
      const p = playersByCity[cityId][socket.id];
      io.to(id).emit('newPlayer', {
        playerId: socket.id,
        x: p.x,
        z: p.z,
        avatarURL: p.avatarURL,
        gender: p.gender,
        firstName: p.firstName,
        lastName: p.lastName
      });
    }

    // Отправляем текущий список видимых игроков только для этого сокета
    // Отправляем полный список игроков города
    socket.emit('currentPlayers', playersByCity[cityId]);
  });

  socket.on('sendMessage', async ({ receiverId, message }, callback) => {
        try {
            const senderId = socket.userId;
            const recvId = parseInt(receiverId, 10);

            // Проверка получателя
            const receiverCheck = await db.query('SELECT id FROM users WHERE id = $1', [recvId]);
            if (receiverCheck.rows.length === 0) {
                return callback({ error: 'Пользователь не найден' });
            }

            // Сохранение сообщения
            const result = await virtualWorldPool.query(
                `INSERT INTO messages (sender_id, receiver_id, message)
       VALUES ($1, $2, $3)
       RETURNING id, created_at, is_read`,
                [senderId, recvId, message]
            );

            const newMessage = result.rows[0];
            const receiverSocketId = onlineUsers.get(recvId);

            // Отправка получателю
            if (receiverSocketId) {
                io.to(receiverSocketId).emit('newMessage', {
                    id: newMessage.id,
                    text: message,
                    senderId,
                    timestamp: newMessage.created_at,
                    isRead: newMessage.is_read
                });
            }

            callback({ success: true, message: newMessage });
        } catch (err) {
            callback({ error: 'Ошибка отправки сообщения' });
        }
    });

  // --- Чат ---
  socket.on('chatMessage', ({ message, name }) => {
    const cityId = socket.cityId;
    const sender = playersByCity[cityId]?.[socket.id];
    if (!sender) return;
    for (const [id, other] of Object.entries(playersByCity[cityId])) {
      const dx = sender.x - other.x;
      const dz = sender.z - other.z;
      const dist = Math.sqrt(dx * dx + dz * dz);
      if (dist <= 50 || id === socket.id) {
        io.to(id).emit('chatMessage', {
          playerId: socket.id,
          position: { x: sender.x, z: sender.z },
          name: name || '???',
          message: message
        });
      }
    }
  });

  // --- WebRTC signaling ---
  socket.on('voiceChatOffer', ({ to, offer }) => {
    io.to(to).emit('voiceChatOffer', { from: socket.id, offer });
  });

  socket.on('voiceChatAnswer', ({ to, answer }) => {
    io.to(to).emit('voiceChatAnswer', { from: socket.id, answer });
  });

  socket.on('voiceChatIceCandidate', ({ to, candidate }) => {
    io.to(to).emit('voiceChatIceCandidate', { from: socket.id, candidate });
  });

  socket.on('voiceChatToggle', ({ enabled }) => {
    if (players[socket.id]) {
      players[socket.id].voiceEnabled = enabled;
    }
    socket.broadcast.emit('voiceChatStatus', { playerId: socket.id, enabled });
  });

  // --- Смена города ---
  socket.on('cityChange', async ({ cityId }) => {
    const oldCity = socket.cityId;
    if (playersByCity[oldCity]) {
      delete playersByCity[oldCity][socket.id];
      // Сообщаем игрокам старого города о выходе
      for (const id in playersByCity[oldCity]) {
        io.to(id).emit('playerDisconnected', socket.id);
      }
    }
    if (!playersByCity[cityId]) playersByCity[cityId] = {};
    playersByCity[cityId][socket.id] = {
      socketId: socket.id,
      userId: socket.userId,
      x: 0,
      z: 0,
      cityId,
      avatarURL: null,
      gender: null,
      firstName: null,
      lastName: null
    };
    players[socket.id] = playersByCity[cityId][socket.id];
    socket.cityId = cityId;
    // Отправляем новых игроков этого города
    socket.emit('currentPlayers', playersByCity[cityId]);
  });

  // --- Отключение ---
  socket.on('disconnect', async () => {
    // Обновляем время последнего онлайн
    lastSeenTimes.set(socket.userId, new Date());
    console.log(`Пользователь ${socket.userId} стал офлайн, время: ${lastSeenTimes.get(socket.userId)}`);
    
    // Уведомляем всех клиентов о том, что пользователь стал офлайн
    socket.broadcast.emit('userStatusChanged', { 
      userId: socket.userId, 
      isOnline: false 
    });
    console.log(`Отправлено событие userStatusChanged для пользователя ${socket.userId} (офлайн)`);
    
    onlineUsers.delete(socket.userId);
    const cityId = socket.cityId;
    const player = playersByCity[cityId]?.[socket.id];
    if (player) {
      // Сохраняем координаты и город выхода
      await db.query(
        'UPDATE users SET last_city_id = $1, last_pos_x = $2, last_pos_z = $3 WHERE id = $4',
        [cityId, player.x, player.z, player.userId]
      );
      delete playersByCity[cityId][socket.id];
      delete players[socket.id];
      // Сообщаем игрокам города о выходе
      for (const id in playersByCity[cityId]) {
        io.to(id).emit('playerDisconnected', socket.id);
      }
    }
  });
});

// Маршрут для получения списка пользователей
// Получить список пользователей (кроме текущего)
app.get('/api/users', authenticate, async (req, res) => {
    try {
        const { rows } = await db.query(`
      SELECT id, first_name AS "firstName", last_name AS "lastName", avatar_url AS "avatarURL"
      FROM users
      WHERE id != $1
    `, [req.user.id]);
        res.json(rows);
    } catch (e) {
        console.error('Ошибка получения списка пользователей', e);
        res.status(500).json({ error: 'Ошибка сервера' });
    }
});

// API endpoint для получения статуса пользователей для Telegram
app.get('/api/users/status', authenticate, async (req, res) => {
    try {
        console.log(`Запрос статуса пользователей от пользователя ${req.user.id}`);
        
        const { rows } = await db.query(`
            SELECT id, first_name AS "firstName", last_name AS "lastName", avatar_url AS "avatarURL"
            FROM users
            WHERE id != $1
        `, [req.user.id]);
        
        // Добавляем статус online и время последнего онлайн для каждого пользователя
        const usersWithStatus = rows.map(user => ({
            ...user,
            isOnline: onlineUsers.has(user.id),
            lastSeen: lastSeenTimes.get(user.id) || null
        }));
        
        console.log(`Возвращено ${usersWithStatus.length} пользователей с статусом`);
        console.log('Онлайн пользователи:', Array.from(onlineUsers.keys()));
        
        res.json(usersWithStatus);
    } catch (e) {
        console.error('Ошибка получения статуса пользователей', e);
        res.status(500).json({ error: 'Ошибка сервера' });
    }
});

// API endpoint to get user information by ID
app.get('/api/users/:userId', authenticate, async (req, res) => {
    const userId = parseInt(req.params.userId, 10);
    try {
        const { rows } = await db.query(`
            SELECT id, first_name AS "firstName", last_name AS "lastName", avatar_url AS "avatarURL"
            FROM users
            WHERE id = $1
        `, [userId]);
        if (rows.length === 0) {
            return res.status(404).json({ error: 'User not found' });
        }
        const user = rows[0];
        const isOnline = onlineUsers.has(user.id);
        const lastSeen = lastSeenTimes.get(user.id) || new Date();
        res.json({
            id: user.id,
            firstName: user.firstName,
            lastName: user.lastName,
            avatarURL: user.avatarURL,
            isOnline: isOnline,
            lastSeen: lastSeen
        });
    } catch (e) {
        console.error('Ошибка получения информации о пользователе по ID', e);
        res.status(500).json({ error: 'Ошибка сервера' });
    }
});

// API endpoint to get unread message count for a specific contact
app.get('/api/messages-read/:contactId', authenticate, async (req, res) => {
    const userId = req.user.id;
    const contactId = parseInt(req.params.contactId, 10);
    
    try {
        const { rows } = await db.query(`
            SELECT COUNT(*) as unread_count
            FROM messages 
            WHERE sender_id = $1 AND recipient_id = $2 AND is_read = false
        `, [contactId, userId]);
        
        res.json({ unreadCount: parseInt(rows[0].unread_count) });
    } catch (e) {
        console.error('Ошибка получения количества непрочитанных сообщений:', e);
        res.status(500).json({ error: 'Ошибка сервера' });
    }
});

// Новый маршрут для получения сообщений с конкретным контактом
app.get('/api/messages/:contactId', authenticate, async (req, res) => {
  const userId = req.user.id;
  const contactId = parseInt(req.params.contactId, 10);
  try {
    // Ensure table exists
      await virtualWorldPool.query(`
      CREATE TABLE IF NOT EXISTS messages (
        id SERIAL PRIMARY KEY,
        sender_id INT NOT NULL,
        receiver_id INT NOT NULL,
        message TEXT NOT NULL,
        created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
        is_read BOOLEAN NOT NULL DEFAULT FALSE
      )`);
  } catch (e) {
    console.warn('[GET /api/messages/:contactId] ensure table failed:', e.message);
  }

  try {
    const sql = `SELECT * FROM messages
                 WHERE (sender_id = $1 AND receiver_id = $2)
                    OR (sender_id = $2 AND receiver_id = $1)
                 ORDER BY created_at ASC`;
      const messagesRes = await virtualWorldPool.query(sql, [userId, contactId]);
    res.json(messagesRes.rows);
  } catch (err) {
    console.error('[GET /api/messages/:contactId] error:', err);
    res.status(500).json({ error: 'Ошибка получения сообщений' });
  }
});

app.get('/api/messages/:contactId', authenticate, async (req, res) => {
    const userId = req.user.id;
    const contactId = parseInt(req.params.contactId, 10);
    try {
        // Ensure table exists
        await virtualWorldPool.query(`
      CREATE TABLE IF NOT EXISTS messages (
        id SERIAL PRIMARY KEY,
        sender_id INT NOT NULL,
        receiver_id INT NOT NULL,
        message TEXT NOT NULL,
        created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
        is_read BOOLEAN NOT NULL DEFAULT FALSE
      )`);
    } catch (e) {
        console.warn('[GET /api/messages/:contactId] ensure table failed:', e.message);
    }

    try {
        const sql = `SELECT * FROM messages
                 WHERE (sender_id = $1 AND receiver_id = $2)
                    OR (sender_id = $2 AND receiver_id = $1)
                 ORDER BY created_at ASC`;
        const messagesRes = await virtualWorldPool.query(sql, [userId, contactId]);
        res.json(messagesRes.rows);
    } catch (err) {
        console.error('[GET /api/messages/:contactId] error:', err);
        res.status(500).json({ error: 'Ошибка получения сообщений' });
    }
});

app.get('/api/messages-read/:contactId', authenticate, async (req, res) => {
    const userId = req.user.id;
    const contactId = parseInt(req.params.contactId, 10);

    try {
        // Проверяем есть ли НЕпрочитанные сообщения от этого контакта
        const sql = `SELECT EXISTS (
            SELECT 1 FROM messages 
            WHERE sender_id = $1 
            AND receiver_id = $2 
            AND is_read = false
            AND sender_id != receiver_id
        ) as has_unread`;

        const result = await virtualWorldPool.query(sql, [contactId, userId]);

        // Если есть непрочитанные - возвращаем "true", иначе "false"
        const hasUnread = result.rows[0].has_unread;
        res.json(hasUnread ? "true" : "false");

    } catch (err) {
        console.error('[GET /api/messages-read/:contactId] error:', err);
        res.status(500).json({ error: 'Ошибка проверки состояния' });
    }
});




app.post('/api/messages-read-true-false', authenticate, async (req, res) => {
    try {
        const userId = req.user.id;
        const { contactId } = req.body;

        if (!contactId) {
            return res.status(400).json({ error: 'contactId required' });
        }

        // Проверяем что contactId не равен ID текущего пользователя
        if (parseInt(contactId) === userId) {
            return res.json({ success: true, skipped: 'Нельзя отмечать свои собственные сообщения' });
        }

        // Отмечаем сообщения как прочитанные ТОЛЬКО если отправитель ≠ получатель
        await virtualWorldPool.query(
            `UPDATE messages 
             SET is_read = true 
             WHERE sender_id = $1 AND receiver_id = $2 
             AND sender_id != receiver_id`,  // Добавляем проверку
            [contactId, userId]
        );

        res.json({ success: true });

    } catch (error) {
        console.error('Error updating read status:', error);
        res.status(500).json({ error: 'Internal server error' });
    }
});


app.post('/api/messages/send', authenticate, async (req, res) => {
  const senderId = req.user.id;
  const { receiverId, message } = req.body || {};
  const recvId = parseInt(receiverId, 10);
  console.log('[POST /api/messages/send] sender:', senderId, 'receiver:', recvId);
  try {
      const receiverCheck = await db.query('SELECT id FROM users WHERE id = $1', [recvId]);
    if (receiverCheck.rows.length === 0) {
      return res.status(404).json({ error: 'Пользователь не найден' });
    }
    try {
        await virtualWorldPool.query(`
        CREATE TABLE IF NOT EXISTS messages (
          id SERIAL PRIMARY KEY,
          sender_id INT NOT NULL,
          receiver_id INT NOT NULL,
          message TEXT NOT NULL,
          created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
          is_read BOOLEAN NOT NULL DEFAULT FALSE
        )`);
    } catch (e) {
      console.warn('[POST /api/messages/send] ensure table failed:', e.message);
    }

      const result = await virtualWorldPool.query(
      `INSERT INTO messages (sender_id, receiver_id, message, created_at)
       VALUES ($1, $2, $3, NOW())
       RETURNING id, created_at, is_read`,
      [senderId, recvId, message]
    );
    const newMessage = result.rows[0];

            const receiverSocketId = onlineUsers.get(recvId);
    if (receiverSocketId) {
      io.to(receiverSocketId).emit('newMessage', {
        id: newMessage.id,
        text: message,
        senderId,
        timestamp: newMessage.created_at,
        isRead: newMessage.is_read
      });
    }
    res.status(201).json(newMessage);
  } catch (err) {
    console.error('[POST /api/messages/send] error:', err);
    res.status(500).json({ error: 'Ошибка отправки сообщения' });
  }
});

app.get('/api/messages', authenticate, async (req, res) => {
  const userId = req.user.id;
    try {
        await virtualWorldPool.query(`
      CREATE TABLE IF NOT EXISTS messages (
        id SERIAL PRIMARY KEY,
        sender_id INT NOT NULL,
        receiver_id INT NOT NULL,
        message TEXT NOT NULL,
        created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
        is_read BOOLEAN NOT NULL DEFAULT FALSE
      )`);
  } catch (e) {
    console.warn('[GET /api/messages] ensure table failed:', e.message);
  }
  try {
      const messagesRes = await virtualWorldPool.query(
      `SELECT * FROM messages
       WHERE sender_id = $1 OR receiver_id = $1
       ORDER BY created_at DESC`,
      [userId]
    );
    if (messagesRes.rows.length === 0) {
      return res.json([]);
    }
    const userIds = new Set();
    messagesRes.rows.forEach(msg => { userIds.add(msg.sender_id); userIds.add(msg.receiver_id); });
      const usersRes = await virtualWorldPool.query(
      `SELECT id, first_name, last_name, avatar_url FROM users WHERE id = ANY($1)`,
      [Array.from(userIds)]
    );
    const userMap = {};
    usersRes.rows.forEach(user => {
      userMap[user.id] = { name: `${user.first_name} ${user.last_name}`, avatar: user.avatar_url };
    });
    const messages = messagesRes.rows.map(msg => ({
      id: msg.id,
      text: msg.message,
      senderId: msg.sender_id,
      receiverId: msg.receiver_id,
      sender: userMap[msg.sender_id] || { name: 'Неизвестный', avatar: null },
      receiver: userMap[msg.receiver_id] || { name: 'Неизвестный', avatar: null },
      timestamp: msg.created_at,
      isRead: msg.is_read
    }));
    res.json(messages);
  } catch (err) {
    console.error('[GET /api/messages] error:', err);
    res.status(500).json({ error: 'Ошибка получения сообщений' });
  }
});

app.patch('/api/messages/:id/read', authenticate, async (req, res) => {
  const messageId = req.params.id;
  const userId = req.user.id;
  try {
      await virtualWorldPool.query(`
      CREATE TABLE IF NOT EXISTS messages (
        id SERIAL PRIMARY KEY,
        sender_id INT NOT NULL,
        receiver_id INT NOT NULL,
        message TEXT NOT NULL,
        created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
        is_read BOOLEAN NOT NULL DEFAULT FALSE
      )`);
  } catch (e) {
    console.warn('[PATCH /api/messages/:id/read] ensure table failed:', e.message);
  }
  try {
      const checkRes = await virtualWorldPool.query(`SELECT id FROM messages WHERE id = $1 AND receiver_id = $2`, [messageId, userId]);
    if (checkRes.rows.length === 0) {
      return res.status(404).json({ error: 'Сообщение не найдено или доступ запрещен' });
    }
      await virtualWorldPool.query(`UPDATE messages SET is_read = true WHERE id = $1`, [messageId]);
    res.status(204).end();
  } catch (err) {
    console.error('[PATCH /api/messages/:id/read] error:', err);
    res.status(500).json({ error: 'Ошибка обновления сообщения' });
  }
});

app.get('/api/me', authenticate, async (req, res) => {
  const userId = req.user.id;
  const { rows } = await db.query(`
    SELECT
      email,
      first_name    AS "firstName",
      last_name     AS "lastName",
      gender,
      age,
      city,
      avatar_url    AS "avatarURL",
      balance,
      hours_played  AS "hoursPlayed",
      reputation,
      phone,
      sportiness,
      health_level  AS "healthLevel",
      stress_level  AS "stressLevel",
      satiety,
      thirst,
      diseases
    FROM users
    WHERE id = $1
  `, [userId]);
  if (!rows.length) return res.status(404).json({ error: 'User not found' });
  
  const user = rows[0];
  
  // Автоматически исправляем неправильный avatarURL
  if (!user.avatarURL || user.avatarURL === 'try' || user.avatarURL === 'undefined' || user.avatarURL === 'null') {
    console.log(`Исправляем неправильный avatarURL для пользователя ${userId}: ${user.avatarURL} -> /models/character.glb`);
    
    try {
      await db.query(
        'UPDATE users SET avatar_url = $1 WHERE id = $2',
        ['/models/character.glb', userId]
      );
      user.avatarURL = '/models/character.glb';
    } catch (e) {
      console.error('Ошибка обновления avatarURL:', e);
    }
  }
  
  res.json(user);
});

app.get('/api/players/:socketId', authenticate, async (req, res) => {
  const socketId = req.params.socketId;
  let p = players[socketId];
  if (!p) {
    for (const city of Object.values(playersByCity)) {
      if (city[socketId]) {
        p = city[socketId];
        break;
      }
    }
  }
  if (!p) return res.status(404).json({ error: 'Player not found' });

  const dbId = p.userId;
  if (!dbId) return res.status(404).json({ error: 'User profile missing' });

  const { rows } = await db.query(`
     SELECT
       first_name    AS "firstName",
       last_name     AS "lastName",
       gender,
       age,
       city,
       avatar_url    AS "avatarURL",
       balance,
       hours_played  AS "hoursPlayed",
       reputation,
       phone,
       sportiness,
       health_level  AS "healthLevel",
      stress_level  AS "stressLevel",
      satiety,
      thirst,
      diseases
     FROM users
     WHERE id = $1
   `, [dbId]);

  if (!rows.length) return res.status(404).json({ error: 'User not found in database' });
  
  const user = rows[0];
  
  // Автоматически исправляем неправильный avatarURL
  if (!user.avatarURL || user.avatarURL === 'try' || user.avatarURL === 'undefined' || user.avatarURL === 'null') {
    console.log(`Исправляем неправильный avatarURL для игрока ${dbId}: ${user.avatarURL} -> /models/character.glb`);
    
    try {
      await db.query(
        'UPDATE users SET avatar_url = $1 WHERE id = $2',
        ['/models/character.glb', dbId]
      );
      user.avatarURL = '/models/character.glb';
    } catch (e) {
      console.error('Ошибка обновления avatarURL:', e);
    }
  }
  
  res.json(user);
});

app.post('/api/register', async (req, res) => {
  try {
    console.log('register request:', req.body?.email);
   const { email, password, firstName, lastName, gender, age, city, avatarURL } = req.body || {};

    if (!email || !password || !firstName || !lastName) {
      return res.status(400).json({ error: 'Не заполнены обязательные поля' });
    }

    const { rowCount } = await db.query(`SELECT 1 FROM users WHERE email = $1`, [email]);
    if (rowCount) return res.status(400).json({ error: 'Почта уже занята' });
    const hash = await bcrypt.hash(password, 10);
    const insertSQL = `
      INSERT INTO users(email, password_hash, first_name, last_name, gender, age, city, avatar_url)
      VALUES($1,$2,$3,$4,$5,$6,$7,$8)
      RETURNING id, email, created_at
    `;
    const result = await db.query(insertSQL, [
     email, hash, firstName, lastName, gender ?? null, age ?? null, city ?? null, avatarURL ?? null
    ]);

   const user = result.rows[0];
   // Не даём регистрации упасть, если экономика не завелась
    try {
      await Economy.createAccount(user.id, 'USD');
    } catch (e) {
      console.error('Economy.createAccount failed:', e);
    }

    if (!process.env.JWT_SECRET) {
      console.error('JWT_SECRET не задан в окружении (.env)');
      return res.status(500).json({ error: 'Ошибка конфигурации сервера' });
    }
    const token = jwt.sign({ id: user.id, email: user.email }, process.env.JWT_SECRET, { expiresIn: '12h' });
    res.json({ success: true, token });
  } catch (e) {
    console.error('Ошибка регистрации:', e);
    res.status(500).json({ error: 'Внутренняя ошибка регистрации' });
  }
});

app.post('/api/login', async (req, res) => {
  const { email, password } = req.body;
  const { rows } = await db.query(
    `SELECT id, password_hash,
            first_name   AS "firstName",
            last_name    AS "lastName",
            gender,
            age,
            city,
            avatar_url   AS "avatarURL"
     FROM users
     WHERE email = $1`,
    [email]
  );
  if (!rows.length) {
    return res.status(401).json({ error: 'Неверный логин или пароль' });
  }

  const user = rows[0];
  const ok = await bcrypt.compare(password, user.password_hash);
  if (!ok) {
    return res.status(401).json({ error: 'Неверный логин или пароль' });
  }

  const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET, {
    expiresIn: '12h'
  });

  res.json({
    token,
    profile: {
      id: user.id,
      email,
      firstName: user.firstName,
      lastName: user.lastName,
      gender: user.gender,
      age: user.age,
      city: user.city,
      avatarURL: user.avatarURL
    }
  });
});

// Получить список доступных моделей
app.get('/api/models', authenticate, async (req, res) => {
  try {
    const fs = require('fs');
    const path = require('path');
    
    const modelsDir = path.join(__dirname, 'public', 'models', 'copied');
    
    if (!fs.existsSync(modelsDir)) {
      return res.json([]);
    }
    
    const files = fs.readdirSync(modelsDir);
    const modelFiles = files.filter(file => 
      file.toLowerCase().endsWith('.glb') || 
      file.toLowerCase().endsWith('.gltf')
    );
    
    console.log('📁 Найдено моделей:', modelFiles.length);
    res.json(modelFiles);
  } catch (error) {
    console.error('Ошибка получения списка моделей:', error);
    res.status(500).json({ error: 'Ошибка получения списка моделей' });
  }
});

// Получить объекты города по cityId
app.get('/api/cities/:cityId/objects', authenticate, async (req, res) => {
  const cityId = req.params.cityId;
  try {
    const { rows } = await db.query(`
      SELECT id,
             name,
             model_url,
             pos_x, pos_y, pos_z,
             rot_x, rot_y, rot_z,
             COALESCE(scale_x, 1) AS scale_x,
             COALESCE(scale_y, 1) AS scale_y,
             COALESCE(scale_z, 1) AS scale_z,
             organization_id,
             COALESCE(rent, 0) AS rent,
             COALESCE(tax, 0) AS tax,
             COALESCE(collidable, false) AS collidable,
             COALESCE(interior_id, 101) AS interior_id,
             COALESCE(textures, '-') AS textures
      FROM city_objects
      WHERE city_id = $1
    `, [cityId]);
    res.json(rows);
  } catch (e) {
    console.error('Ошибка в /api/cities/:cityId/objects:', e);
    res.status(500).json({ error: 'Ошибка получения объектов города' });
  }
});

// Получить список доступных моделей из public/models/copied
app.get('/api/models', authenticate, async (req, res) => {
  try {
    const dir = pathLib.join(__dirname, 'public', 'models', 'copied');
    const files = await fs.promises.readdir(dir);
    const glbs = files.filter(f => f.toLowerCase().endsWith('.glb'));
    res.json(glbs);
  } catch (e) {
    res.status(500).json({ error: 'Ошибка чтения списка моделей' });
  }
});

// Обновить avatarURL пользователя
app.put('/api/profile/avatar', authenticate, async (req, res) => {
  const { avatarURL } = req.body;
  const userId = req.user.id;
  
  try {
    // Проверяем, что avatarURL не пустой и валидный
    if (!avatarURL || avatarURL === 'try' || avatarURL === 'undefined' || avatarURL === 'null') {
      return res.status(400).json({ error: 'Неправильный avatarURL' });
    }
    
    await db.query(
      'UPDATE users SET avatar_url = $1 WHERE id = $2',
      [avatarURL, userId]
    );
    
    res.json({ success: true, avatarURL });
  } catch (e) {
    console.error('Ошибка обновления avatarURL:', e);
    res.status(500).json({ error: 'Ошибка обновления avatarURL' });
  }
});

// Регистрируем маршрут на старте приложения:
app.get(
  '/api/city_objects/:objectId/interior',
  authenticate,
  async (req, res) => {
    const objectId = parseInt(req.params.objectId, 10);
    try {
      const { rows } = await db.query(
        'SELECT interior_id FROM city_objects WHERE id = $1',
        [objectId]
      );
      if (rows.length === 0) {
        return res.status(404).json({ error: 'Объект с таким id не найден' });
      }
      res.json({ interiorId: rows[0].interior_id });
    } catch (e) {
      console.error('Ошибка в /api/city_objects/:objectId/interior', e);
      res.status(500).json({ error: 'Не удалось получить interior_id' });
    }
  }
);

// Новый эндпоинт для входа в интерьер:
app.post('/api/interiors/:interiorId/enter', authenticate, async (req, res) => {
  const interiorId = parseInt(req.params.interiorId, 10);
  try {
    const base = await db.query(
      'SELECT spawn_x, spawn_y, spawn_z, spawn_rot, exit_x, exit_y, exit_z, exit_rot FROM interiors WHERE id = $1',
      [interiorId]
    );
    const interior = base.rows[0];
    if (!interior) return res.status(404).json({ error: 'Интерьер не найден' });

    let exitInt = null;
    try {
      const extra = await db.query(
        'SELECT exit_int_x, exit_int_y, exit_int_z FROM interiors WHERE id = $1',
        [interiorId]
      );
      if (extra.rows[0] && (extra.rows[0].exit_int_x != null || extra.rows[0].exit_int_y != null || extra.rows[0].exit_int_z != null)) {
        exitInt = {
          x: extra.rows[0].exit_int_x,
          y: extra.rows[0].exit_int_y,
          z: extra.rows[0].exit_int_z
        };
      }
    } catch (e2) {
      // Колонки exit_int_* могут отсутствовать — это не ошибка для клиента
      console.warn('exit_int_* columns are not available yet:', e2.message);
    }

    // Эффективная точка входа: если spawn_x/y/z не заданы (или равны 0),
    // пробуем использовать внутреннюю точку выхода (exit_int_*),
    // иначе в крайнем случае используем координаты внешнего выхода.
    const rawSpawn = {
      x: interior.spawn_x,
      y: interior.spawn_y,
      z: interior.spawn_z,
      rot: interior.spawn_rot
    };
    const isZeroOrNull = v => v == null || Number(v) === 0;
    const spawnLooksEmpty = isZeroOrNull(rawSpawn.x) && isZeroOrNull(rawSpawn.y) && isZeroOrNull(rawSpawn.z);

    let effectiveSpawn = { ...rawSpawn };
    if (spawnLooksEmpty) {
      if (exitInt && typeof exitInt.x === 'number' && typeof exitInt.z === 'number') {
        effectiveSpawn = { x: exitInt.x, y: exitInt.y ?? 0, z: exitInt.z, rot: rawSpawn.rot };
      } else if (!isZeroOrNull(interior.exit_x) || !isZeroOrNull(interior.exit_y) || !isZeroOrNull(interior.exit_z)) {
        // Фолбэк на внешние координаты выхода, если они заданы
        effectiveSpawn = { x: interior.exit_x, y: interior.exit_y, z: interior.exit_z, rot: rawSpawn.rot };
      }
    }

    // Логируем вычисленные координаты для отладки
    try {
      console.log('[INTERIOR ENTER]', {
        interiorId,
        spawn: effectiveSpawn,
        rawSpawn,
        exit: { x: interior.exit_x, y: interior.exit_y, z: interior.exit_z, rot: interior.exit_rot },
        exitInt
      });
    } catch (_) {}

    res.json({
      spawn: effectiveSpawn,
      exit: {
        x: interior.exit_x,
        y: interior.exit_y,
        z: interior.exit_z,
        rot: interior.exit_rot
      },
      exitInt
    });
  } catch (e) {
    console.error(e);
    res.status(500).json({ error: 'Не удалось получить координаты интерьера' });
  }
});

// server.js, после маршрута /api/city_objects/:objectId/interior
app.get('/api/interiors/:interiorId/definition', authenticate, async (req, res) => {
  const interiorId = parseInt(req.params.interiorId, 10);
  try {
    const interior = (await db.query(
      'SELECT glb_filename, pos_x, pos_y, pos_z, spawn_x, spawn_y, spawn_z, spawn_rot FROM interiors WHERE id = $1',
      [interiorId]
    )).rows[0];
    if (!interior) return res.status(404).json({ error: 'Интерьер не найден' });

    const objects = (await db.query(
      `SELECT type, model_url, x, y, z, rot_x, rot_y, rot_z, scale
         FROM interior_objects
        WHERE interior_id = $1
        ORDER BY id`,
      [interiorId]
    )).rows;

    res.json({
      glb: `/models/interiors/${interior.glb_filename}`,
      position: { x: interior.pos_x, y: interior.pos_y, z: interior.pos_z },
      spawn: { x: interior.spawn_x, y: interior.spawn_y, z: interior.spawn_z, rot: interior.spawn_rot },
      objects
    });
  } catch (e) {
    console.error(e);
    res.status(500).json({ error: 'Не удалось загрузить определение интерьера' });
  }
});

// Начало копи
app.post('/api/listen', authenticate, async (req, res) => {
  try {
    console.log('[API /api/listen] Request body:', req.body);
    const { player_id: bodyPlayerId, json_filename } = req.body || {};
    const authUser = req.user || {};
    const effectivePlayerId = bodyPlayerId || authUser.email || authUser.id || authUser.userId;
    if (!effectivePlayerId || !json_filename) {
      return res.status(400).json({ success: false, error: 'player_id and json_filename are required' });
    }

    // Выбираем пул: если есть отдельный пул, берём его, иначе общий db
    try {
      // Создаём таблицу при необходимости
        await virtualWorldPool.query(`
        CREATE TABLE IF NOT EXISTS json_listened (
          id SERIAL PRIMARY KEY,
          player_id TEXT NOT NULL,
          json_filename TEXT NOT NULL,
          listened_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
        )`);
    } catch (e) {
      console.warn('[API /api/listen] ensure table failed:', e.message);
    }

    const q = `INSERT INTO json_listened (player_id, json_filename, listened_at) VALUES ($1, $2, NOW())`;
      await virtualWorldPool.query(q, [String(effectivePlayerId), String(json_filename)]);
    console.log('[API /api/listen] Saved listened:', { player: effectivePlayerId, json: json_filename });
    return res.status(200).json({ success: true });
  } catch (err) {
    console.error('[API /api/listen] error:', err);
    return res.status(500).json({ success: false, error: 'Database operation failed', details: err.message });
  }
});
//Конец копи
//Начало копи
function generateTransactions() {
    const transactions = [];
    const suspiciousCount = Math.min(3 + Math.floor(level / 3), 5);
    const suspiciousIds = [];

    while (suspiciousIds.length < suspiciousCount) {
        const id = Math.floor(Math.random() * 15);
        if (!suspiciousIds.includes(id)) {
            suspiciousIds.push(id);
        }
    }
    // Генерируем 15 транзакций
    for (let i = 0; i < 15; i++) {
        const isSuspicious = suspiciousIds.includes(i);
        // ... остальной код генерации транзакции ...
    }

    return transactions;
}

// Завершение игры
app.post('/api/cleanup-game/finish', authenticate, async (req, res) => {
    try {
        const { success, markedTransactions, personalArchive } = req.body;
        const userId = req.user.id;

        // Здесь должна быть логика обновления прогресса игрока
        // Например, увеличение репутации, разблокировка квестов и т.д.

        res.json({ success: true });
    } catch (e) {
        res.status(500).json({ error: 'Ошибка сохранения результата игры' });
    }
});

// Вспомогательная функция для генерации транзакций
function getRandomCity() {
    const cities = ["Москва", "Санкт-Петербург", "Новосибирск", "Екатеринбург", "Казань", "Самара", "Омск", "Челябинск", "Ростов-на-Дону", "Уфа"];
    return cities[Math.floor(Math.random() * cities.length)];
}

function getRandomIP() {
    return `${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}`;
}

function getRandomDevice() {
    const devices = ["Chrome Win", "Safari iOS", "Android", "Firefox Mac", "Edge Win", "Opera Win"];
    return devices[Math.floor(Math.random() * devices.length)];
}

function getRandomPurpose() {
    const purposes = [
        "Покупка продуктов",
        "Оплата услуг",
        "Перевод другу",
        "Оплата аренды",
        "Покупка техники",
        "Благотворительность",
        "Образовательные курсы"
    ];
    return purposes[Math.floor(Math.random() * purposes.length)];
}

function getRandomRecipient() {
    const recipients = [
        "Пятёрочка №17",
        "ИП Сидоров И.И.",
        "OOO 'Комплекс-С'",
        "ИП Петрова А.А.",
        "Магнит №45",
        "Ашан Супермаркет",
        "ООО 'ТехноПрофи'"
    ];
    return recipients[Math.floor(Math.random() * recipients.length)];
}

function getSuspiciousIP() {
    // Генерируем IP из известных VPN диапазонов или Tor exit nodes
    const vpnRanges = [
        "185.2.33.", "213.42.12.", "172.16.", "192.168.", "10.0."
    ];
    const range = vpnRanges[Math.floor(Math.random() * vpnRanges.length)];
    return range + Math.floor(Math.random() * 255);
}

function getSuspiciousDevice() {
    // Подозрительные устройства - одинаковые для разных транзакций
    const suspiciousDevices = [
        "Tor Browser",
        "Android 4.4.2 (старая версия)",
        "iPhone 6 (iOS 10)",
        "Emulator Android"
    ];
    return suspiciousDevices[Math.floor(Math.random() * suspiciousDevices.length)];
}

// Обновленная функция генерации транзакций
function generateTransactions() {
    const transactions = [];
    const suspiciousIds = [0, 5, 10]; // Фиксированные индексы подозрительных транзакций

    // Генерируем 15 транзакций
    for (let i = 0; i < 15; i++) {
        const isSuspicious = suspiciousIds.includes(i);
        const baseDate = new Date(2023, 6, 25); // 25 июля 2023

        let date, amount, purpose, ip, city, device, recipient;
        let anomalyType = isSuspicious ? i % 3 : null; // 0, 1 или 2 для подозрительных

        if (isSuspicious) {
            date = new Date(baseDate.getTime() + Math.floor(i / 5) * 24 * 60 * 60 * 1000);
            amount = `₽${Math.floor(200000 + Math.random() * 800000).toLocaleString()}`;

            switch (anomalyType) {
                case 0: // Географический прыжок
                    date.setHours(date.getHours() + 1);
                    ip = getSuspiciousIP();
                    city = i % 2 === 0 ? "Москва" : "Самара";
                    device = getRandomDevice();
                    purpose = getRandomPurpose();
                    recipient = getRandomRecipient();
                    break;

                case 1: // Пустое назначение + VPN
                    ip = getSuspiciousIP();
                    city = getRandomCity();
                    device = getSuspiciousDevice();
                    purpose = '';
                    recipient = getRandomRecipient();
                    break;

                case 2: // Повтор получателя
                    ip = getRandomIP();
                    city = getRandomCity();
                    device = getRandomDevice();
                    purpose = getRandomPurpose();
                    recipient = "ООО 'Сомнительные Переводы'";
                    break;
            }
        } else {
            // Нормальные транзакции
            date = new Date(baseDate.getTime() + Math.floor(i / 5) * 24 * 60 * 60 * 1000);
            amount = `₽${Math.floor(1000 + Math.random() * 20000).toLocaleString()}`;
            purpose = getRandomPurpose();
            ip = getRandomIP();
            city = getRandomCity();
            device = getRandomDevice();
            recipient = getRandomRecipient();
        }

        transactions.push({
            id: i,
            date: date.toLocaleDateString(),
            time: date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
            amount,
            purpose: isSuspicious && Math.random() > 0.5 ? '' : purpose,
            ip: isSuspicious && Math.random() > 0.7 ? '' : ip,
            city,
            device: isSuspicious && Math.random() > 0.5 ? '' : device,
            recipient,
            _realIp: isSuspicious ? getSuspiciousIP() : ip,
            _realDevice: isSuspicious ? getSuspiciousDevice() : device,
            _isSuspicious: isSuspicious,
            _anomalyType: anomalyType
        });
    }

    return transactions;
}



// Начало кода для квестов

// Маршрут для получения информации о квестах игрока
app.get('/api/quests/player-status', authenticate, async (req, res) => {
    try {
        const userId = req.user.id;
        console.log(`[QUESTS] Запрос статуса квестов для пользователя ${userId}`);

        // Получаем уровень игрока (по умолчанию 1)
        const playerLevel = 1; // Пока используем фиксированный уровень

        // Получаем все активные квесты с их статусами для игрока
        const questsData = await getPlayerQuestsData(userId, playerLevel);

        console.log(`[QUESTS] Возвращаем данные для пользователя ${userId}: ${questsData.length} квестов`);
        res.json({
            success: true,
            playerLevel: playerLevel,
            quests: questsData
        });

    } catch (error) {
        console.error('Ошибка получения статуса квестов игрока:', error);
        res.status(500).json({
            success: false,
            error: 'Ошибка получения данных о квестах'
        });
    }
});

// Вспомогательная функция для получения уровня игрока
async function getPlayerLevel(userId) {
    try {
        // Пока используем уровень по умолчанию 1
        // В будущем можно добавить логику расчета уровня на основе опыта
        return 1;
    } catch (error) {
        console.error('Ошибка получения уровня игрока:', error);
        return 1;
    }
}

// Функция для получения мок-данных квестов
function getMockQuestsData() {
    return [
        {
            id: 1,
            title: "Добро пожаловать в игру!",
            description: "Пройдите обучение и изучите основы игры",
            kind: "tutorial",
            status: "available",
            hasAccess: true,
            currentStep: {
                id: 1,
                stepIndex: 1,
                title: "Начало приключения",
                description: "Поговорите с гидом в центре города",
                playerStatus: "not_started"
            },
            steps: [
                {
                    id: 1,
                    stepIndex: 1,
                    title: "Начало приключения",
                    description: "Поговорите с гидом в центре города",
                    actionType: "talk_to_npc",
                    actionPayload: { npc_id: 1 },
                    dialogueScene: "tutorial_start",
                    isOptional: false,
                    playerStatus: "not_started",
                    startedAt: null,
                    completedAt: null
                },
                {
                    id: 2,
                    stepIndex: 2,
                    title: "Изучение интерфейса",
                    description: "Откройте инвентарь и изучите интерфейс",
                    actionType: "open_inventory",
                    actionPayload: {},
                    dialogueScene: null,
                    isOptional: false,
                    playerStatus: "not_started",
                    startedAt: null,
                    completedAt: null
                }
            ],
            metadata: {},
            startedAt: null,
            completedAt: null
        },
        {
            id: 2,
            title: "Первое задание",
            description: "Выполните простое задание для получения опыта",
            kind: "main",
            status: "locked",
            hasAccess: false,
            currentStep: null,
            steps: [
                {
                    id: 3,
                    stepIndex: 1,
                    title: "Найти предмет",
                    description: "Найдите потерянный предмет в городе",
                    actionType: "find_item",
                    actionPayload: { item_id: 1 },
                    dialogueScene: null,
                    isOptional: false,
                    playerStatus: "not_started",
                    startedAt: null,
                    completedAt: null
                }
            ],
            metadata: {},
            startedAt: null,
            completedAt: null
        }
    ];
}

// Основная функция для получения данных о квестах
async function getPlayerQuestsData(userId, playerLevel) {
    try {
        // Проверяем подключение к базе данных
        if (!new_quest_Base) {
            console.error('[QUESTS] База данных квестов не инициализирована');
            return getMockQuestsData();
        }

        // Пробуем получить данные из базы данных
        try {
            const availableQuests = await new_quest_Base.query(`
                SELECT q.id, q.title, q.description, q.kind, q.is_active, q.metadata,
                       pq.status as player_status, pq.current_step_id,
                       pq.started_at, pq.completed_at
                FROM quests q
                LEFT JOIN player_quests pq ON q.id = pq.quest_id AND pq.player_id = $1
                WHERE q.is_active = true
                ORDER BY q.id
            `, [userId]);

        const questsData = [];

        for (const quest of availableQuests.rows) {
            // Проверяем условия доступа к квесту
            const hasAccess = await checkQuestAccess(quest.id, playerLevel, userId);

            // Получаем информацию о текущем шаге
            const currentStepInfo = await getCurrentStepInfo(quest.id, quest.current_step_id, userId);

            // Получаем все шаги квеста
            const questSteps = await new_quest_Base.query(`
                SELECT qs.id, qs.step_index, qs.title, qs.description, 
                       qs.action_type, qs.action_payload, qs.dialogue_scene,
                       qs.is_optional, ps.status as player_step_status,
                       ps.started_at as step_started, ps.completed_at as step_completed
                FROM quest_steps qs
                LEFT JOIN player_steps ps ON qs.id = ps.quest_step_id AND ps.player_id = $1
                WHERE qs.quest_id = $2
                ORDER BY qs.step_index
            `, [userId, quest.id]);

            // Определяем статус квеста для игрока
            let questStatus = 'available'; // доступен
            if (quest.player_status === 'completed') {
                questStatus = 'completed';
            } else if (quest.player_status === 'in_progress') {
                questStatus = 'in_progress';
            } else if (!hasAccess) {
                questStatus = 'locked';
            }

            questsData.push({
                id: quest.id,
                title: quest.title,
                description: quest.description,
                kind: quest.kind,
                status: questStatus,
                hasAccess: hasAccess,
                currentStep: currentStepInfo,
                steps: questSteps.rows.map(step => ({
                    id: step.id,
                    stepIndex: step.step_index,
                    title: step.title,
                    description: step.description,
                    actionType: step.action_type,
                    actionPayload: step.action_payload,
                    dialogueScene: step.dialogue_scene,
                    isOptional: step.is_optional,
                    playerStatus: step.player_step_status || 'not_started',
                    startedAt: step.step_started,
                    completedAt: step.step_completed
                })),
                metadata: quest.metadata,
                startedAt: quest.started_at,
                completedAt: quest.completed_at
            });
        }

        return questsData;

        } catch (dbError) {
            console.error('Ошибка получения данных квестов из базы данных:', dbError.message);
            console.log('[QUESTS] Используем мок-данные для квестов');
            return getMockQuestsData();
        }
    } catch (error) {
        console.error('Ошибка получения данных квестов:', error);
        // Возвращаем мок-данные вместо пустого массива
        return getMockQuestsData();
    }
}

// Функция проверки доступа к квесту
async function checkQuestAccess(questId, playerLevel, userId) {
    try {
        console.log('Проверка доступа к квесту:', { questId, playerLevel, userId });
        
        // Проверяем подключение к базе данных
        if (!new_quest_Base) {
            console.error('База данных квестов не инициализирована');
            return false;
        }
        
        // Проверяем группы условий
        let prerequisiteGroups;
        try {
            prerequisiteGroups = await new_quest_Base.query(`
                SELECT qpg.id, qpg.group_index
                FROM quest_prereq_groups qpg
                WHERE qpg.quest_id = $1
                ORDER BY qpg.group_index
            `, [questId]);
        } catch (dbError) {
            console.log('Ошибка доступа к таблице условий квестов, разрешаем доступ:', dbError.message);
            // Если база данных недоступна, разрешаем доступ
            return true;
        }

        // Если нет групп условий - квест доступен
        if (prerequisiteGroups.rows.length === 0) {
            console.log('Квест доступен (нет условий доступа)');
            return true;
        }

        // Проверяем каждую группу условий
        for (const group of prerequisiteGroups.rows) {
            const groupConditions = await new_quest_Base.query(`
                SELECT qpc.condition_type, qpc.condition_payload
                FROM quest_prereq_conditions qpc
                WHERE qpc.group_id = $1
            `, [group.id]);

            let allConditionsMet = true;

            for (const condition of groupConditions.rows) {
                const conditionMet = await checkCondition(
                    condition.condition_type,
                    condition.condition_payload,
                    playerLevel,
                    userId
                );

                if (!conditionMet) {
                    allConditionsMet = false;
                    break;
                }
            }

            // Если хотя бы одна группа условий выполнена - квест доступен
            if (allConditionsMet) {
                return true;
            }
        }

        return false;

    } catch (error) {
        console.error('Ошибка проверки доступа к квесту:', error);
        return false;
    }
}

// Функция проверки конкретного условия
async function checkCondition(conditionType, conditionPayload, playerLevel, userId) {
    try {
        const payload = typeof conditionPayload === 'string'
            ? JSON.parse(conditionPayload)
            : conditionPayload;

        switch (conditionType) {
            case 'level_ge':
                return playerLevel >= payload.level;

            case 'quest_completed':
                // Проверяем завершенность другого квеста
                const questCheck = await new_quest_Base.query(`
                    SELECT 1 FROM player_quests 
                    WHERE player_id = $1 AND quest_id = $2 AND status = 'completed'
                `, [userId, payload.quest_id]);
                return questCheck.rows.length > 0;

            case 'step_completed':
                // Проверяем завершенность шага
                const stepCheck = await new_quest_Base.query(`
                    SELECT 1 FROM player_steps 
                    WHERE player_id = $1 AND quest_step_id = $2 AND status = 'completed'
                `, [userId, payload.step_id]);
                return stepCheck.rows.length > 0;

            default:
                console.warn(`Неизвестный тип условия: ${conditionType}`);
                return false;
        }
    } catch (error) {
        console.error('Ошибка проверки условия:', error);
        return false;
    }
}

// Функция получения информации о текущем шаге
async function getCurrentStepInfo(questId, currentStepId, userId) {
    try {
        if (!currentStepId) {
            // Если текущего шага нет, возвращаем первый шаг квеста
            const firstStep = await new_quest_Base.query(`
                SELECT id, step_index, title, description 
                FROM quest_steps 
                WHERE quest_id = $1 
                ORDER BY step_index ASC 
                LIMIT 1
            `, [questId]);

            return firstStep.rows.length > 0 ? {
                id: firstStep.rows[0].id,
                stepIndex: firstStep.rows[0].step_index,
                title: firstStep.rows[0].title,
                description: firstStep.rows[0].description
            } : null;
        }

        // Получаем информацию о текущем шаге
        const currentStep = await new_quest_Base.query(`
            SELECT qs.id, qs.step_index, qs.title, qs.description,
                   ps.status as player_status
            FROM quest_steps qs
            LEFT JOIN player_steps ps ON qs.id = ps.quest_step_id AND ps.player_id = $1
            WHERE qs.id = $2
        `, [userId, currentStepId]);

        return currentStep.rows.length > 0 ? {
            id: currentStep.rows[0].id,
            stepIndex: currentStep.rows[0].step_index,
            title: currentStep.rows[0].title,
            description: currentStep.rows[0].description,
            playerStatus: currentStep.rows[0].player_status
        } : null;
    } catch (error) {
        console.error('Ошибка получения информации о шаге:', error);
        return null;
    }
}

// Маршрут для старта квеста
app.post('/api/quests/:questId/start', authenticate, async (req, res) => {
    try {
        const userId = req.user.id;
        const questId = parseInt(req.params.questId);
        console.log('Запрос начала квеста:', { userId, questId });
        const playerLevel = await getPlayerLevel(userId);

        // Проверяем существование квеста в базе данных
        let questExists;
        try {
            questExists = await new_quest_Base.query(`
                SELECT id FROM quests WHERE id = $1
            `, [questId]);
        } catch (dbError) {
            console.log('Ошибка доступа к базе данных квестов, используем мок-данные:', dbError.message);
            // Если база данных недоступна, разрешаем квест с ID 1
            if (questId === 1) {
                questExists = { rows: [{ id: 1 }] };
            } else {
                return res.status(404).json({
                    success: false,
                    error: 'Квест не найден'
                });
            }
        }
        
        if (questExists.rows.length === 0) {
            console.log('Квест не найден в базе данных:', questId);
            return res.status(404).json({
                success: false,
                error: 'Квест не найден'
            });
        }
        
        // Проверяем доступность квеста
        console.log('Проверяем доступ к квесту:', { questId, playerLevel, userId });
        const hasAccess = await checkQuestAccess(questId, playerLevel, userId);
        console.log('Результат проверки доступа:', hasAccess);
        
        if (!hasAccess) {
            return res.status(403).json({
                success: false,
                error: 'Квест недоступен'
            });
        }

        // Проверяем, не начат ли уже квест
        let existingQuest;
        try {
            existingQuest = await new_quest_Base.query(`
                SELECT id FROM player_quests 
                WHERE player_id = $1 AND quest_id = $2
            `, [userId, questId]);
        } catch (dbError) {
            console.log('Ошибка доступа к таблице квестов игроков, пропускаем проверку:', dbError.message);
            existingQuest = { rows: [] }; // Предполагаем, что квест не начат
        }

        if (existingQuest.rows.length > 0) {
            return res.status(400).json({
                success: false,
                error: 'Квест уже начат'
            });
        }

        // Получаем первый шаг квеста
        let firstStep;
        try {
            firstStep = await new_quest_Base.query(`
                SELECT id FROM quest_steps 
                WHERE quest_id = $1 
                ORDER BY step_index ASC 
                LIMIT 1
            `, [questId]);
        } catch (dbError) {
            console.log('Ошибка доступа к таблице шагов квестов, создаем мок-шаг:', dbError.message);
            // Создаем мок-шаг
            firstStep = { rows: [{ id: 1 }] };
        }

        if (firstStep.rows.length === 0) {
            return res.status(400).json({
                success: false,
                error: 'Квест не имеет шагов'
            });
        }

        // Начинаем квест
        try {
            await new_quest_Base.query(`
                INSERT INTO player_quests 
                (player_id, quest_id, current_step_id, status, started_at, last_updated_at)
                VALUES ($1, $2, $3, 'in_progress', NOW(), NOW())
            `, [userId, questId, firstStep.rows[0].id]);

            // Записываем первый шаг
            await new_quest_Base.query(`
                INSERT INTO player_steps 
                (player_id, quest_step_id, status, started_at)
                VALUES ($1, $2, 'in_progress', NOW())
            `, [userId, firstStep.rows[0].id]);
        } catch (dbError) {
            console.log('Ошибка записи квеста в базу данных, но продолжаем:', dbError.message);
            // Продолжаем выполнение, даже если не удалось записать в БД
        }

        res.json({
            success: true,
            message: 'Квест начат',
            currentStepId: firstStep.rows[0].id
        });

    } catch (error) {
        console.error('Ошибка старта квеста:', error);
        res.status(500).json({
            success: false,
            error: 'Ошибка начала квеста'
        });
    }
});



// Маршрут для отметки прослушанного диалога
app.post('/api/quests/mark-dialog-listened', authenticate, async (req, res) => {
    try {
        const userId = req.user.id;
        const { npc_id, dialogue_key } = req.body;

        console.log(`[QUESTS] Отметка прослушанного диалога: user=${userId}, npc=${npc_id}, dialogue=${dialogue_key}`);

        if (!npc_id || !dialogue_key) {
            return res.status(400).json({
                success: false,
                error: 'Не указаны npc_id или dialogue_key'
            });
        }

        // 1. Записываем взаимодействие в player_npc_interactions
        const interactionResult = await new_quest_Base.query(`
            INSERT INTO player_npc_interactions 
            (player_id, npc_id, action_type, action_payload, created_at)
            VALUES ($1, $2, 'listen_npc', $3, NOW())
            RETURNING id
        `, [userId, npc_id, JSON.stringify({ dialogue_key: dialogue_key })]);

        console.log(`[QUESTS] Взаимодействие записано с ID: ${interactionResult.rows[0].id}`);

        // 2. Проверяем, относится ли это взаимодействие к активному шагу квеста
        const activeQuestStep = await new_quest_Base.query(`
            SELECT pq.id as player_quest_id, pq.current_step_id, qs.id as step_id, qs.action_payload
            FROM player_quests pq
            JOIN quest_steps qs ON pq.current_step_id = qs.id
            WHERE pq.player_id = $1 AND pq.status = 'in_progress'
            AND qs.action_type = 'talk_npc'
        `, [userId]);

        if (activeQuestStep.rows.length > 0) {
            const step = activeQuestStep.rows[0];
            const actionPayload = typeof step.action_payload === 'string'
                ? JSON.parse(step.action_payload)
                : step.action_payload;

            // Проверяем, соответствует ли NPC текущему шагу квеста
            if (actionPayload.npc_id == npc_id) {
                console.log(`[QUESTS] Диалог соответствует активному шагу квеста: step_id=${step.step_id}`);

                // 3. Проверяем требования для завершения шага
                const stepRequirements = await new_quest_Base.query(`
                    SELECT requirement_type, requirement_payload 
                    FROM step_requirements 
                    WHERE quest_step_id = $1
                    ORDER BY ord
                `, [step.step_id]);

                let stepCompleted = true;

                for (const requirement of stepRequirements.rows) {
                    const payload = typeof requirement.requirement_payload === 'string'
                        ? JSON.parse(requirement.requirement_payload)
                        : requirement.requirement_payload;

                    if (requirement.requirement_type === 'listen_npc') {
                        // Проверяем, прослушал ли игрок нужное количество диалогов с указанными NPC
                        const listenCount = await new_quest_Base.query(`
                            SELECT COUNT(*) as count
                            FROM player_npc_interactions
                            WHERE player_id = $1 
                            AND npc_id = ANY($2::int[])
                            AND action_type = 'listen_npc'
                        `, [userId, payload.npc_ids]);

                        if (listenCount.rows[0].count < payload.count) {
                            stepCompleted = false;
                            break;
                        }
                    }
                }

                if (stepCompleted) {
                    // 4. Отмечаем шаг как завершенный
                    await new_quest_Base.query(`
                        UPDATE player_steps 
                        SET status = 'completed', completed_at = NOW()
                        WHERE player_id = $1 AND quest_step_id = $2
                    `, [userId, step.step_id]);

                    // 5. Находим следующий шаг
                    const nextStep = await new_quest_Base.query(`
                        SELECT qs.id, qs.step_index
                        FROM quest_steps qs
                        WHERE qs.quest_id = (
                            SELECT quest_id FROM quest_steps WHERE id = $1
                        )
                        AND qs.step_index > (
                            SELECT step_index FROM quest_steps WHERE id = $1
                        )
                        ORDER BY qs.step_index ASC
                        LIMIT 1
                    `, [step.step_id]);

                    if (nextStep.rows.length > 0) {
                        // 6. Обновляем текущий шаг в квесте
                        await new_quest_Base.query(`
                            UPDATE player_quests 
                            SET current_step_id = $1, last_updated_at = NOW()
                            WHERE player_id = $2 AND id = $3
                        `, [nextStep.rows[0].id, userId, step.player_quest_id]);

                        // 7. Начинаем следующий шаг
                        await new_quest_Base.query(`
                            INSERT INTO player_steps 
                            (player_id, quest_step_id, status, started_at)
                            VALUES ($1, $2, 'in_progress', NOW())
                            ON CONFLICT (player_id, quest_step_id) 
                            DO UPDATE SET status = 'in_progress', started_at = NOW()
                        `, [userId, nextStep.rows[0].id]);

                        console.log(`[QUESTS] Шаг завершен, переход к шагу ${nextStep.rows[0].step_index}`);
                    } else {
                        // 8. Если это последний шаг - завершаем квест
                        await new_quest_Base.query(`
                            UPDATE player_quests 
                            SET status = 'completed', completed_at = NOW(), last_updated_at = NOW()
                            WHERE player_id = $1 AND id = $2
                        `, [userId, step.player_quest_id]);

                        console.log(`[QUESTS] Квест завершен!`);
                    }
                }
            }
        }

        res.json({
            success: true,
            message: 'Диалог отмечен как прослушанный'
        });

    } catch (error) {
        console.error('Ошибка при отметке прослушанного диалога:', error);
        res.status(500).json({
            success: false,
            error: 'Ошибка сервера при обработке диалога'
        });
    }
});



//Конец копи
//Начало копи
app.get('/api/quests/progress', authenticate, async (req, res) => {
    console.log("Загрузка на сервере. ID пользователя:", req.user.id);

    try {
        // ВМЕСТО получения email, используем напрямую ID пользователя
        const userId = req.user.id.toString(); // Преобразуем в строку для consistency

        // Получаем список всех квестов с их JSON файлами
        const questsQuery = await virtualWorldPool.query(`
            SELECT q.id, q.title, qj.json_filename
            FROM quests q
            JOIN quest_jsons qj ON q.id = qj.quest_id
            ORDER BY q.id
        `);

        // Получаем JSON файлы, которые прослушал игрок по его ID
        const listenedQuery = await virtualWorldPool.query(`
            SELECT json_filename FROM json_listened 
            WHERE player_id = $1
        `, [userId]); // Используем ID вместо email

        console.log("Результат запроса listenedQuery для ID", userId, ":", listenedQuery.rows);

        const listenedFiles = new Set(listenedQuery.rows.map(row => row.json_filename));
        console.log("Прослушанные файлы:", Array.from(listenedFiles));

        // Остальной код без изменений...
        const questsMap = new Map();
        questsQuery.rows.forEach(row => {
            if (!questsMap.has(row.id)) {
                questsMap.set(row.id, {
                    id: row.id,
                    title: row.title,
                    total: 0,
                    completed: 0,
                    files: []
                });
            }
            const quest = questsMap.get(row.id);
            quest.total++;
            quest.files.push(row.json_filename);
            if (listenedFiles.has(row.json_filename)) {
                quest.completed++;
            }
        });

        const result = Array.from(questsMap.values()).map(quest => ({
            id: quest.id,
            title: quest.title,
            progress: quest.total > 0 ? Math.round((quest.completed / quest.total) * 100) : 0,
            completed: quest.completed,
            total: quest.total
        }));

        console.log("Результат для клиента:", result);
        res.json(result);
    } catch (err) {
        console.error('Ошибка получения прогресса квестов:', err);
        res.status(500).json({ error: 'Ошибка получения прогресса квестов' });
    }
});

app.get('/api/cleanup-game/data', authenticate, async (req, res) => {
    try {
        const level = parseInt(req.query.level) || 1;
        // Генерируем транзакции с учетом уровня
        const transactions = generateTransactions(level);

        res.json({
            success: true,
            transactions: transactions,
            level: level
        });
    } catch (e) {
        console.error('Ошибка генерации данных игры:', e);
        res.status(500).json({
            success: false,
            error: 'Ошибка генерации данных игры'
        });
    }
});
//Конец копи
// Список интерьеров с координатами для отображения на карте
app.get('/api/interiors', authenticate, async (req, res) => {
  try {
    const { rows } = await db.query(
      'SELECT id, pos_x, pos_y, pos_z FROM interiors ORDER BY id'
    );
    res.json(rows);
  } catch (e) {
    console.error('Ошибка получения списка интерьеров', e);
    res.status(500).json({ error: 'Не удалось получить список интерьеров' });
  }
});

// Получить объекты интерьера
app.get('/api/interiors/:id/objects', authenticate, async (req, res) => {
  const id = parseInt(req.params.id, 10);
  try {
    const { rows } = await db.query(
      `SELECT id, model_url, x, y, z, rot_x, rot_y, rot_z, scale
         FROM interior_objects
        WHERE interior_id = $1
        ORDER BY id`,
      [id]
    );
    res.json(rows);
  } catch (e) {
    console.error('Ошибка получения объектов интерьера', e);
    res.status(500).json({ error: 'Не удалось получить объекты интерьера' });
  }
});

// Сохранить объекты интерьера в БД
app.post('/api/interiors/:id/save', authenticate, async (req, res) => {
  const id = parseInt(req.params.id, 10);
  const { objects = [], removedIds = [] } = req.body;
  if (!Array.isArray(objects) || !Array.isArray(removedIds)) {
    return res.status(400).json({ error: 'Invalid objects' });
  }
  try {
    if (removedIds.length) {
      await db.query(
        'DELETE FROM interior_objects WHERE id = ANY($1::int[]) AND interior_id = $2',
        [removedIds, id]
      );
    }
    for (const obj of objects) {
      if (obj.id) {
        await db.query(
          `UPDATE interior_objects
              SET model_url=$1, x=$2, y=$3, z=$4,
                  rot_x=$5, rot_y=$6, rot_z=$7, scale=$8
            WHERE id=$9 AND interior_id=$10`,
          [
            obj.model_url,
            obj.x,
            obj.y,
            obj.z,
            obj.rot_x,
            obj.rot_y,
            obj.rot_z,
            obj.scale,
            obj.id,
            id
          ]
        );
      } else {
        await db.query(
          `INSERT INTO interior_objects
            (interior_id, model_url, x, y, z, rot_x, rot_y, rot_z, scale)
           VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)`,
          [
            id,
            obj.model_url,
            obj.x,
            obj.y,
            obj.z,
            obj.rot_x,
            obj.rot_y,
            obj.rot_z,
            obj.scale
          ]
        );
      }
    }
    res.json({ ok: true });
  } catch (e) {
    console.error('Ошибка сохранения интерьера', e);
    res.status(500).json({ error: 'Не удалось сохранить интерьер' });
  }
});

// Коллизии карты: загрузка/сохранение файла public/colliders.json
app.get('/api/colliders', authenticate, async (req, res) => {
  const cityId = Number(req.query.cityId) || 0;
  try {
    const fileName = cityId ? `colliders_city_${cityId}.json` : 'colliders.json';
    const filePath = pathLib.join(__dirname, 'public', fileName);
    try {
      const raw = await fs.promises.readFile(filePath, 'utf8');
      const json = JSON.parse(raw);
      return res.json(json);
    } catch (e) {
      // Если нет файла — создаём пустой
      const empty = { colliders: [] };
      await fs.promises.mkdir(pathLib.join(__dirname, 'public'), { recursive: true });
      await fs.promises.writeFile(filePath, JSON.stringify(empty, null, 2), 'utf8');
      return res.json(empty);
    }
  } catch (e) {
    console.error('Ошибка чтения colliders.json:', e);
    res.status(500).json({ error: 'Ошибка чтения коллизий' });
  }
});

app.post('/api/colliders', authenticate, async (req, res) => {
  try {
    const { colliders, cityId } = req.body || {};
    if (!Array.isArray(colliders)) {
      return res.status(400).json({ error: 'Invalid colliders' });
    }
    const fileName = cityId ? `colliders_city_${Number(cityId)}.json` : 'colliders.json';
    const filePath = pathLib.join(__dirname, 'public', fileName);
    await fs.promises.mkdir(pathLib.join(__dirname, 'public'), { recursive: true });
    await fs.promises.writeFile(filePath, JSON.stringify({ colliders }, null, 2), 'utf8');
    res.json({ ok: true });
  } catch (e) {
    console.error('Ошибка записи colliders.json:', e);
    res.status(500).json({ error: 'Ошибка сохранения коллизий' });
  }
});



// Получить организацию по objectId
app.get('/api/organizations/by-object/:objectId', authenticate, async (req, res) => {
  const objectId = parseInt(req.params.objectId, 10);
  try {
    const { rows } = await db.query(`
      SELECT 
        o.id,
        o.name,
        os.menu,
        os.work_hours
      FROM city_objects AS co
      JOIN organizations AS o
        ON co.organization_id = o.id
      JOIN organization_settings AS os
        ON os.organization_id = o.id
      WHERE co.id = $1
    `, [objectId]);

    if (rows.length === 0) {
      return res.status(404).json({ error: 'Организация не найдена для этого объекта' });
    }

    res.json(rows[0]);
  } catch (e) {
    console.error('Ошибка в /api/organizations/by-object/:objectId:', e);
    res.status(500).json({ error: 'Ошибка получения меню организации' });
  }
});




// Сохранить текущую карту в текстовый файл
app.post('/api/save-map', authenticate, async (req, res) => {
  const { cityId = 'unknown', objects, removedIds = [] } = req.body;
  if (!Array.isArray(objects) || !Array.isArray(removedIds)) {
    return res.status(400).json({ error: 'Invalid objects' });
  }
  try {
    const dir = pathLib.join(__dirname, 'saves');
    await fs.promises.mkdir(dir, { recursive: true });
    const file = `city_${cityId}_${Date.now()}.txt`;
    const filePath = pathLib.join(dir, file);
    await fs.promises.writeFile(
      filePath,
      JSON.stringify({ objects, removedIds }, null, 2),
      'utf8'
    );
    res.json({ ok: true, file });
  } catch (e) {
    console.error('Ошибка сохранения карты', e);
    res.status(500).json({ error: 'Ошибка сохранения карты' });
  }
});

// Получить список городов с названием и страной
app.get('/api/cities', authenticate, async (req, res) => {
  try {
    const { rows } = await db.query(`
      SELECT cities.id, cities.name, countries.name AS country_name
      FROM cities
      JOIN countries ON cities.country_id = countries.id
      ORDER BY countries.name, cities.name
    `);
    res.json(rows);
  } catch (e) {
    res.status(500).json({ error: 'Ошибка получения списка городов' });
  }
});

// Сохранить/обновить объект города в БД
app.post('/api/save-object', authenticate, async (req, res) => {
  try {
    const {
      id,
      city_id,
      name,
      model_url,
      pos_x,
      pos_y,
      pos_z,
      rot_x,
      rot_y,
      rot_z,
      scale_x,
      scale_y,
      scale_z,
      organization_id = 2,
      rent = 0,
      tax = 0,
      collidable = false,
      interior_id = 101,
      textures = '-'
    } = req.body;

    console.log('🔍 Получены данные объекта:', {
      id,
      name,
      collidable,
      city_id
    });

    if (!city_id || !model_url) {
      return res.status(400).json({ error: 'city_id и model_url обязательны' });
    }

    let result;
    if (id && id !== null && id !== undefined) {
      // Обновление существующего объекта
      console.log('🔄 Обновляем существующий объект с ID:', id);
      console.log('🔍 Значение collidable для UPDATE:', collidable);
      const { rows } = await db.query(`
        UPDATE city_objects SET
          name = $1,
          model_url = $2,
          pos_x = $3,
          pos_y = $4,
          pos_z = $5,
          rot_x = $6,
          rot_y = $7,
          rot_z = $8,
          scale_x = $9,
          scale_y = $10,
          scale_z = $11,
          organization_id = $12,
          rent = $13,
          tax = $14,
          collidable = $15,
          interior_id = $16,
          textures = $17
        WHERE id = $18
        RETURNING id
      `, [
        name || '',
        model_url,
        pos_x || 0,
        pos_y || 0,
        pos_z || 0,
        rot_x || 0,
        rot_y || 0,
        rot_z || 0,
        scale_x || 1,
        scale_y || 1,
        scale_z || 1,
        organization_id,
        rent,
        tax,
        collidable,
        interior_id,
        textures,
        id
      ]);
      result = rows[0];
      console.log('✅ Объект обновлен:', result);
    } else {
      // Создание нового объекта
      console.log('🆕 Создаем новый объект');
      console.log('🔍 Значение collidable для INSERT:', collidable);
      const { rows } = await db.query(`
        INSERT INTO city_objects (
          city_id, name, model_url, pos_x, pos_y, pos_z,
          rot_x, rot_y, rot_z, scale_x, scale_y, scale_z,
          organization_id, rent, tax, collidable, interior_id, textures
        ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)
        RETURNING id
      `, [
        city_id,
        name || '',
        model_url,
        pos_x || 0,
        pos_y || 0,
        pos_z || 0,
        rot_x || 0,
        rot_y || 0,
        rot_z || 0,
        scale_x || 1,
        scale_y || 1,
        scale_z || 1,
        organization_id,
        rent,
        tax,
        collidable,
        interior_id,
        textures
      ]);
      result = rows[0];
      console.log('✅ Новый объект создан:', result);
    }

    res.json({ id: result.id, success: true });
  } catch (error) {
    console.error('Ошибка сохранения объекта:', error);
    res.status(500).json({ error: 'Ошибка сохранения объекта' });
  }
});

// Удалить объект города из БД
app.delete('/api/delete-object/:id', authenticate, async (req, res) => {
  try {
    const objectId = parseInt(req.params.id, 10);
    
    if (!objectId) {
      return res.status(400).json({ error: 'ID объекта обязателен' });
    }

    const { rowCount } = await db.query(
      'DELETE FROM city_objects WHERE id = $1',
      [objectId]
    );

    if (rowCount === 0) {
      return res.status(404).json({ error: 'Объект не найден' });
    }

    res.json({ success: true, message: 'Объект удален' });
  } catch (error) {
    console.error('Ошибка удаления объекта:', error);
    res.status(500).json({ error: 'Ошибка удаления объекта' });
  }
});

// Тестовый эндпоинт для проверки таблицы city_objects
app.get('/api/test-city-objects', authenticate, async (req, res) => {
  try {
    const { rows } = await db.query(`
      SELECT table_name, column_name, data_type 
      FROM information_schema.columns 
      WHERE table_name = 'city_objects' 
      ORDER BY ordinal_position
    `);
    res.json({ tableExists: rows.length > 0, columns: rows });
  } catch (error) {
    console.error('Ошибка проверки таблицы:', error);
    res.status(500).json({ error: 'Ошибка проверки таблицы', details: error.message });
  }
});

// API endpoint для получения коллайдеров города из базы данных
app.get('/api/colliders/city/:cityId', authenticate, async (req, res) => {
  const cityId = parseInt(req.params.cityId, 10);
  
  try {
    const { rows } = await db.query(`
      SELECT 
        id,
        type,
        position_x as "position.x",
        position_y as "position.y", 
        position_z as "position.z",
        rotation_x as "rotation.x",
        rotation_y as "rotation.y",
        rotation_z as "rotation.z",
        scale_x as "scale.x",
        scale_y as "scale.y",
        scale_z as "scale.z",
        color_r as "color.r",
        color_g as "color.g",
        color_b as "color.b",
        opacity,
        created_at,
        updated_at
      FROM colliders 
      WHERE city_id = $1 
      ORDER BY created_at ASC
    `, [cityId]);
    
    // Преобразуем данные в формат, ожидаемый фронтендом
    const colliders = rows.map(row => ({
      id: row.id,
      type: row.type,
      position: {
        x: parseFloat(row["position.x"]),
        y: parseFloat(row["position.y"]),
        z: parseFloat(row["position.z"])
      },
      rotation: {
        x: parseFloat(row["rotation.x"]),
        y: parseFloat(row["rotation.y"]),
        z: parseFloat(row["rotation.z"])
      },
      scale: {
        x: parseFloat(row["scale.x"]),
        y: parseFloat(row["scale.y"]),
        z: parseFloat(row["scale.z"])
      },
      color: {
        r: parseFloat(row["color.r"]),
        g: parseFloat(row["color.g"]),
        b: parseFloat(row["color.b"])
      },
      opacity: parseFloat(row.opacity),
      created_at: row.created_at,
      updated_at: row.updated_at
    }));
    
    res.json({ colliders });
  } catch (error) {
    console.error('Ошибка получения коллайдеров из БД:', error);
    res.status(500).json({ error: 'Ошибка получения коллайдеров' });
  }
});

// API endpoint для сохранения коллайдеров города в базу данных
app.post('/api/colliders/city/:cityId', authenticate, async (req, res) => {
  const cityId = parseInt(req.params.cityId, 10);
  const { colliders } = req.body;
  
  if (!Array.isArray(colliders)) {
    return res.status(400).json({ error: 'Invalid colliders data' });
  }
  
  try {
    // Начинаем транзакцию
    await db.query('BEGIN');
    
    // Разделяем коллайдеры на новые и существующие
    const newColliders = colliders.filter(c => !c.id);
    const existingColliders = colliders.filter(c => c.id);
    
    console.log(`💾 Сохраняем: ${existingColliders.length} существующих, ${newColliders.length} новых коллайдеров`);
    
    // Обновляем существующие коллайдеры
    for (const collider of existingColliders) {
      await db.query(`
        UPDATE colliders SET
          type = $2,
          position_x = $3, position_y = $4, position_z = $5,
          rotation_x = $6, rotation_y = $7, rotation_z = $8,
          scale_x = $9, scale_y = $10, scale_z = $11,
          color_r = $12, color_g = $13, color_b = $14,
          opacity = $15,
          updated_at = CURRENT_TIMESTAMP
        WHERE id = $1 AND city_id = $16
      `, [
        collider.id,
        collider.type,
        collider.position?.x || 0,
        collider.position?.y || 0,
        collider.position?.z || 0,
        collider.rotation?.x || 0,
        collider.rotation?.y || 0,
        collider.rotation?.z || 0,
        collider.scale?.x || 1,
        collider.scale?.y || 1,
        collider.scale?.z || 1,
        collider.color?.r || 1,
        collider.color?.g || 0,
        collider.color?.b || 0,
        collider.opacity || 0.3,
        cityId
      ]);
    }
    
    // Вставляем новые коллайдеры
    for (const collider of newColliders) {
      await db.query(`
        INSERT INTO colliders (
          city_id, type,
          position_x, position_y, position_z,
          rotation_x, rotation_y, rotation_z,
          scale_x, scale_y, scale_z,
          color_r, color_g, color_b,
          opacity
        ) VALUES (
          $1, $2,
          $3, $4, $5,
          $6, $7, $8,
          $9, $10, $11,
          $12, $13, $14,
          $15
        )
      `, [
        cityId,
        collider.type,
        collider.position?.x || 0,
        collider.position?.y || 0,
        collider.position?.z || 0,
        collider.rotation?.x || 0,
        collider.rotation?.y || 0,
        collider.rotation?.z || 0,
        collider.scale?.x || 1,
        collider.scale?.y || 1,
        collider.scale?.z || 1,
        collider.color?.r || 1,
        collider.color?.g || 0,
        collider.color?.b || 0,
        collider.opacity || 0.3
      ]);
    }
    
    // Подтверждаем транзакцию
    await db.query('COMMIT');
    
    console.log(`✅ Коллайдеры для города ${cityId} сохранены в БД (${existingColliders.length} обновлено, ${newColliders.length} новых)`);
    res.json({ success: true, message: 'Коллайдеры сохранены успешно', updated: existingColliders.length, created: newColliders.length });
    
  } catch (error) {
    // Откатываем транзакцию в случае ошибки
    await db.query('ROLLBACK');
    console.error('Ошибка сохранения коллайдеров в БД:', error);
    res.status(500).json({ error: 'Ошибка сохранения коллайдеров' });
  }
});

// API endpoint для обновления отдельного коллайдера
app.put('/api/colliders/:colliderId', authenticate, async (req, res) => {
  const colliderId = parseInt(req.params.colliderId, 10);
  const collider = req.body;
  
  try {
    const { rowCount } = await db.query(`
      UPDATE colliders SET
        type = $2,
        position_x = $3, position_y = $4, position_z = $5,
        rotation_x = $6, rotation_y = $7, rotation_z = $8,
        scale_x = $9, scale_y = $10, scale_z = $11,
        color_r = $12, color_g = $13, color_b = $14,
        opacity = $15,
        updated_at = CURRENT_TIMESTAMP
      WHERE id = $1
    `, [
      colliderId,
      collider.type,
      collider.position?.x || 0,
      collider.position?.y || 0,
      collider.position?.z || 0,
      collider.rotation?.x || 0,
      collider.rotation?.y || 0,
      collider.rotation?.z || 0,
      collider.scale?.x || 1,
      collider.scale?.y || 1,
      collider.scale?.z || 1,
      collider.color?.r || 1,
      collider.color?.g || 0,
      collider.color?.b || 0,
      collider.opacity || 0.3
    ]);
    
    if (rowCount === 0) {
      return res.status(404).json({ error: 'Коллайдер не найден' });
    }
    
    console.log(`✅ Коллайдер ${colliderId} обновлен в БД`);
    res.json({ success: true, message: 'Коллайдер обновлен успешно' });
    
  } catch (error) {
    console.error('Ошибка обновления коллайдера в БД:', error);
    res.status(500).json({ error: 'Ошибка обновления коллайдера' });
  }
});

// API endpoint для удаления отдельного коллайдера
app.delete('/api/colliders/:colliderId', authenticate, async (req, res) => {
  const colliderId = parseInt(req.params.colliderId, 10);
  
  try {
    const { rowCount } = await db.query('DELETE FROM colliders WHERE id = $1', [colliderId]);
    
    if (rowCount === 0) {
      return res.status(404).json({ error: 'Коллайдер не найден' });
    }
    
    console.log(`✅ Коллайдер ${colliderId} удален из БД`);
    res.json({ success: true, message: 'Коллайдер удален успешно' });
    
  } catch (error) {
    console.error('Ошибка удаления коллайдера из БД:', error);
    res.status(500).json({ error: 'Ошибка удаления коллайдера' });
  }
});

app.use((req, res) => {
  res.sendFile(pathLib.join(__dirname, 'build', 'index.html'));
});

const PORT = process.env.PORT || 4000;
const server = http.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
});

// Обработка сигналов для graceful shutdown
process.on('SIGTERM', () => {
  console.log('SIGTERM received, shutting down gracefully...');
  gracefulShutdown();
});

process.on('SIGINT', () => {
  console.log('SIGINT received, shutting down gracefully...');
  gracefulShutdown();
});

function gracefulShutdown() {
  console.log('Уведомляем всех клиентов о перезагрузке сервера...');
  
  // Отправляем уведомление всем подключенным клиентам
  io.emit('serverRestart', { 
    message: 'Сервер будет перезагружен через 5 секунд. Пожалуйста, сохраните прогресс.',
    restartIn: 5000
  });
  
  // Даем время клиентам получить уведомление
  setTimeout(() => {
    console.log('Закрываем сервер...');
    server.close(() => {
      console.log('HTTP server closed');
      process.exit(0);
    });
    
    // Принудительно закрываем через 10 секунд
    setTimeout(() => {
      console.error('Could not close connections in time, forcefully shutting down');
      process.exit(1);
    }, 10000);
  }, 5000);
}

// Логирование всех маршрутов и middleware
['get', 'post', 'put', 'delete', 'use'].forEach(method => {
  const orig = app[method];
  app[method] = function(path, ...args) {
    //if (typeof path === 'string') {
    //  console.log(`Регистрируется ${method.toUpperCase()} маршрут:`, path);
    //} else if (typeof path === 'function') {
    //  console.log(`Регистрируется middleware (без пути) через ${method}`);
    //}
    return orig.call(this, path, ...args);
  };
});

// После ensureMessagesTable();
async function ensureInteriorsSpawnColumns() {
  try {
    await db.query('ALTER TABLE interiors ADD COLUMN IF NOT EXISTS spawn_x NUMERIC DEFAULT 0');
    await db.query('ALTER TABLE interiors ADD COLUMN IF NOT EXISTS spawn_y NUMERIC DEFAULT 0');
    await db.query('ALTER TABLE interiors ADD COLUMN IF NOT EXISTS spawn_z NUMERIC DEFAULT 0');
    await db.query('ALTER TABLE interiors ADD COLUMN IF NOT EXISTS spawn_rot NUMERIC DEFAULT 0');
    await db.query('ALTER TABLE interiors ADD COLUMN IF NOT EXISTS exit_x NUMERIC DEFAULT 0');
    await db.query('ALTER TABLE interiors ADD COLUMN IF NOT EXISTS exit_y NUMERIC DEFAULT 0');
    await db.query('ALTER TABLE interiors ADD COLUMN IF NOT EXISTS exit_z NUMERIC DEFAULT 0');
    await db.query('ALTER TABLE interiors ADD COLUMN IF NOT EXISTS exit_rot NUMERIC DEFAULT 0');
  } catch (e) {
    console.error('Ошибка добавления spawn/exit-колонок в interiors', e);
  }
}
ensureInteriorsSpawnColumns();

// Добавляем колонки масштаба для городских объектов, если ещё не созданы
async function ensureCityObjectsScaleColumns() {
  try {
    await db.query('ALTER TABLE city_objects ADD COLUMN IF NOT EXISTS scale_x NUMERIC DEFAULT 1');
    await db.query('ALTER TABLE city_objects ADD COLUMN IF NOT EXISTS scale_y NUMERIC DEFAULT 1');
    await db.query('ALTER TABLE city_objects ADD COLUMN IF NOT EXISTS scale_z NUMERIC DEFAULT 1');
  } catch (e) {
    console.error('Ошибка добавления scale колонок в city_objects', e);
  }
}
ensureCityObjectsScaleColumns();