import { Router } from "express";
import { pool } from './db.js';
import Joi from 'joi';
import j2s from "joi-to-swagger";

export class ApiError extends Error {
    constructor(status, message, cause) {
        super(message);
        this.status = status;
        this.cause = cause;
    }
}

const router = Router();
const asyncHandler = fn => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);

const userSchema = Joi.object({
    username: Joi.string().min(3).max(32).required()
});

const createGameSchema = Joi.object({
    user: Joi.number().integer().positive().required(),
    is_local: Joi.boolean().default(false)
});

const addPlayerSchema = Joi.object({
    user: Joi.number().integer().positive().required()
});

const lockGameSchema = Joi.object({
    user: Joi.number().integer().positive().required()
});

const turnSchema = Joi.object({
    user: Joi.number().integer().positive().required(),
    first_throw: Joi.number().integer().min(0).required(),
    second_throw: Joi.number().integer().min(0).required(),
    third_throw: Joi.number().integer().min(0).required(),
});

const endGameSchema = Joi.object({
  winner: Joi.number().integer().positive()
});

const schemasByPath = {
    "/api/users": userSchema,
    "/api/games": createGameSchema,
    "/api/games/:id/players": addPlayerSchema,
    "/api/games/:id/lock": lockGameSchema,
    "/api/games/:id/turns": turnSchema,
    "/api/games/:id/end": endGameSchema,
};

export function generateRequestBodies() {
  const requestBodies = {};

  for (const path in schemasByPath) {
    const { swagger } = j2s(schemasByPath[path]);
    requestBodies[path] = {
      content: {
        "application/json": {
          schema: swagger
        }
      }
    };
  }

  return requestBodies;
}

/**
 * @swagger
 * /api/healthcheck:
 *   get:
 *     summary: Check if the API is running and if the database is reachable
 *     responses:
 *       200:
 *         description: API and DB are healthy
 *         content:
 *           application/json:
 *             example:
 *               status: "ok"
 *               db: "ok"
 *       500:
 *         description: Database unreachable
 *         content:
 *           application/json:
 *             example:
 *               status: "error"
 *               db: "down"
 */
router.get("/healthcheck", async (req, res) => {
  try {
    await pool.query("SELECT 1"); // basic DB check
    res.json({ status: "ok", db: "ok" });
  } catch {
    res.status(500).json({ status: "error", db: "down" });
  }
});

/**
 * @swagger
 * /api/users/{id}:
 *   get:
 *     summary: Get a user by ID
 *     parameters:
 *       - name: id
 *         in: path
 *         required: true
 *         schema:
 *           type: integer
 *     responses:
 *       200:
 *         description: User data
 *         content:
 *           application/json:
 *             example:
 *               id: 1
 *               username: "john_doe"
 *       400:
 *         description: Invalid user ID
 *       404:
 *         description: User not found
 */
router.get("/users/:id", asyncHandler(async (req, res) => {
    const userId = parseInt(req.params.id, 10);
    if (isNaN(userId)) throw new ApiError(400, "Invalid user ID");

    const [rows] = await pool.query("SELECT * FROM users WHERE id = ?", [userId]);
    if (!rows.length) throw new ApiError(404, "User not found");

    res.json(rows[0]);
}));

/**
 * @swagger
 * /api/users:
 *   post:
 *     summary: Create a new user or return existing user if username already exists
 *     responses:
 *       200:
 *         description: User created or existing user returned
 *         content:
 *           application/json:
 *             example:
 *               id: 1
 *               username: "john_doe"
 *       400:
 *         description: Validation error
 */
router.post("/users", asyncHandler(async (req, res) => {
    const { error, value } = userSchema.validate(req.body);
    if (error) throw new ApiError(400, error.details[0].message);

    const { username } = value;
    const [result] = await pool.query(
        `INSERT INTO users (username)
         VALUES (?)
         ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id)`,
        [username]
    );

    const [userRow] = await pool.query("SELECT * FROM users WHERE id = ?", [result.insertId]);
    res.status(200).json(userRow[0]);
}));

/**
 * @swagger
 * /api/games:
 *   get:
 *     summary: Get all current games
 *     responses:
 *       200:
 *         description: List of current games
 *         content:
 *           application/json:
 *             example:
 *               - id: 1
 *                 is_open: true
 *                 is_local: false
 *                 current_playing_user: 1
 *                 turn_order: null
 */
router.get("/games", async (req, res) => {
    const [rows] = await pool.query("SELECT * FROM games");
    res.json(rows);
});

/**
 * @swagger
 * /api/games/{id}:
 *   get:
 *     summary: Get a specific game by ID
 *     parameters:
 *       - name: id
 *         in: path
 *         required: true
 *         schema:
 *           type: integer
 *     responses:
 *       200:
 *         description: Game data
 *         content:
 *           application/json:
 *             example:
 *               id: 1
 *               is_open: true
 *               is_local: false
 *               current_playing_user: 1
 *               turn_order: null
 *               creator: 1
 */
router.get("/games/:id", asyncHandler(async (req, res) => {
    const gameId = parseInt(req.params.id, 10);
    if (isNaN(gameId)) throw new ApiError(400, "Invalid game ID");

    const [games] = await pool.query("SELECT * FROM games WHERE id = ?", [gameId]);
    if (!games.length) throw new ApiError(404, "Game not found");

    const game = games[0];

    const [creatorRows] = await pool.query(
        `SELECT u.id
         FROM game_players gp
         JOIN users u ON gp.user = u.id
         WHERE gp.game = ? AND gp.is_creator = TRUE
         LIMIT 1`,
        [gameId]
    );
    const creator = creatorRows.length ? creatorRows[0].id : null;

    const [players] = await pool.query(
        `SELECT u.id, u.username
         FROM game_players gp
         JOIN users u ON gp.user = u.id
         WHERE gp.game = ?`,
        [gameId]
    );

    const pointsPromises = players.map(async p => {
        const [lastTurn] = await pool.query(
            "SELECT end_points FROM turns WHERE game = ? AND user = ? ORDER BY id DESC LIMIT 1",
            [gameId, p.id]
        );
        return {
            ...p,
            points: lastTurn.length ? lastTurn[0].end_points : 501
        };
    });
    const playersWithPoints = await Promise.all(pointsPromises);

    res.json({
        ...game,
        creator,
        players: playersWithPoints
    });
}));

/**
 * @swagger
 * /api/games:
 *   post:
 *     summary: Create a new game
 *     responses:
 *       201:
 *         description: Game created
 *         content:
 *           application/json:
 *             example:
 *               game:
 *                 id: 1
 *                 is_open: true
 *                 is_local: false
 *                 current_playing_user: 1
 *                 turn_order: null
 *               players:
 *                 - id: 1
 *                   username: "john_doe"
 */
router.post("/games", asyncHandler(async (req, res) => {
    const { error, value } = createGameSchema.validate(req.body);
    if (error) throw new ApiError(400, error.details[0].message);

    const { user, is_local } = value;
    const conn = await pool.getConnection();

    try {
        await conn.beginTransaction();

        const [gameResult] = await conn.query(
            "INSERT INTO games (is_open, is_local) VALUES (?, ?)",
            [true, is_local]
        );

        const gameId = gameResult.insertId;

        await conn.query(
            "INSERT INTO game_players (game, user, is_creator) VALUES (?, ?, ?)",
            [gameId, user, true]
        );

        const [gameRow] = await conn.query("SELECT * FROM games WHERE id = ?", [gameId]);
        const [players] = await conn.query(
            `SELECT u.id, u.username FROM game_players gp
             JOIN users u ON gp.user = u.id
             WHERE gp.game = ?`,
            [gameId]
        );

        await conn.commit();
        res.status(201).json({ game: gameRow[0], players });
    } catch (err) {
        await conn.rollback();
        throw new ApiError(500, "Failed to create game", err);
    } finally {
        conn.release();
    }
}));

/**
 * @swagger
 * /api/games/{id}/players:
 *   post:
 *     summary: Add a player to a game
 *     parameters:
 *       - name: id
 *         in: path
 *         required: true
 *         schema:
 *           type: integer
 *     responses:
 *       201:
 *         description: Player added
 *         content:
 *           application/json:
 *             example:
 *               game: 1
 *               players:
 *                 - id: 1
 *                   username: "john_doe"
 *                 - id: 2
 *                   username: "jane_doe"
 */
router.post("/games/:id/players", asyncHandler(async (req, res) => {
    const gameId = parseInt(req.params.id, 10);
    if (isNaN(gameId)) throw new ApiError(400, "Invalid game ID");

    const { error, value } = addPlayerSchema.validate(req.body);
    if (error) throw new ApiError(400, error.details[0].message);

    const { user } = value;
    const conn = await pool.getConnection();

    try {
        await conn.beginTransaction();

        const [alreadyJoined] = await conn.query(
            "SELECT 1 FROM game_players WHERE game = ? AND user = ?",
            [gameId, user]
        );
        if (alreadyJoined.length) throw new ApiError(400, "User already joined this game");

        const [gameRows] = await conn.query(
            "SELECT * FROM games WHERE id = ? AND is_open = TRUE",// AND is_local = FALSE",   TODO: wehn im body der richtige creator angegeben wurde wir user zu dem local game hinzugefügt
            [gameId]
        );

        if (!gameRows.length) throw new ApiError(400, "Game is not open or is local");

        await conn.query(
            "INSERT IGNORE INTO game_players (game, user, is_creator) VALUES (?, ?, ?)",
            [gameId, user, false]
        );

        const [players] = await conn.query(
            `SELECT u.id, u.username FROM game_players gp
             JOIN users u ON gp.user = u.id
             WHERE gp.game = ?`,
            [gameId]
        );

        await conn.commit();
        res.status(201).json({ game: gameId, players });
    } catch (err) {
        await conn.rollback();
        throw new ApiError(500, "Failed to add player", err);
    } finally {
        conn.release();
    }
}));

/**
 * @swagger
 * /api/games/{id}/lock:
 *   patch:
 *     summary: Lock a game and generate turn order
 *     parameters:
 *       - name: id
 *         in: path
 *         required: true
 *         schema:
 *           type: integer
 *     responses:
 *       200:
 *         description: Game locked with turn order
 *         content:
 *           application/json:
 *             example:
 *               message: "Game locked"
 *               turnOrder: [1,2,3]
 *       400:
 *         description: Invalid game ID / Validation error
 *       403:
 *         description: Only the creator can lock the game
 *       500:
 *         description: Failed to lock game
 */
router.patch("/games/:id/lock", asyncHandler(async (req, res) => {
    const gameId = parseInt(req.params.id, 10);
    if (isNaN(gameId)) throw new ApiError(400, "Invalid game ID");

    const { error, value } = lockGameSchema.validate(req.body);
    if (error) throw new ApiError(400, error.details[0].message);

    const { user } = value;
    const conn = await pool.getConnection();

    try {
        await conn.beginTransaction();

        const [creatorRows] = await conn.query(
            "SELECT * FROM game_players WHERE game = ? AND user = ? AND is_creator = TRUE",
            [gameId, user]
        );

        if (!creatorRows.length) throw new ApiError(403, "Only the creator can lock the game");

        const [players] = await conn.query(
            "SELECT user FROM game_players WHERE game = ?",
            [gameId]
        );

        const turnOrder = players.map(p => p.user);
        for (let i = turnOrder.length - 1; i > 0; i--) {
            const j = Math.floor(Math.random() * (i + 1));
            [turnOrder[i], turnOrder[j]] = [turnOrder[j], turnOrder[i]];
        }

        const currentPlayingUser = turnOrder[0];
        await conn.query(
            "UPDATE games SET is_open = FALSE, turn_order = ?, current_playing_user = ? WHERE id = ?",
            [JSON.stringify(turnOrder), currentPlayingUser, gameId]
        );

        await conn.commit();
        res.json({ message: "Game locked", turnOrder });
    } catch (err) {
        await conn.rollback();
        throw new ApiError(500, "Failed to lock game", err);
    } finally {
        conn.release();
    }
}));

/**
 * @swagger
 * /api/games/{id}/end:
 *   patch:
 *     summary: End a game
 *     parameters:
 *       - name: id
 *         in: path
 *         required: true
 *         schema:
 *           type: integer
 *     responses:
 *       200:
 *         description: Game marked as finished
 *         content:
 *           application/json:
 *             example:
 *               message: "Game finished"
 *               gameId: 1
 */
router.patch("/games/:id/end", asyncHandler(async (req,res) => {
    const gameId = parseInt(req.params.id, 10);
    if (isNaN(gameId)) throw new ApiError(400, "Invalid game ID");

  const { error, value } = endGameSchema.validate(req.body);
  if (error) throw new ApiError(400, error.details[0].message);

  const { winner } = value;

  const conn = await pool.getConnection();
  try {
    await conn.beginTransaction();

    const [gameRows] = await conn.query("SELECT * FROM games WHERE id = ?", [gameId]);
    if (!gameRows.length) throw new ApiError(404, "Game not found");
    if (gameRows[0].is_finished) throw new ApiError(400, "Game is already finished");

    if (winner) {
      const [playerRows] = await conn.query(
        "SELECT 1 FROM game_players WHERE game = ? AND user = ?",
        [gameId, winner]
      );
      if (!playerRows.length) throw new ApiError(400, "Winner must be a player in the game");
    }

    await conn.query(
      "UPDATE games SET is_finished = TRUE, winner = ? WHERE id = ?",
      [winner || null, gameId]
    );

    await conn.commit();
    res.json({ message: "Game finished", gameId });
  } catch (err) {
    await conn.rollback();
    throw new ApiError(500, "Failed to finish game", err);
  } finally {
    conn.release();
  }
}));

/**
 * @swagger
 * /api/games/{id}/players:
 *   get:
 *     summary: Get all players in a game
 *     parameters:
 *       - name: id
 *         in: path
 *         required: true
 *         schema:
 *           type: integer
 *     responses:
 *       200:
 *         description: List of players
 *         content:
 *           application/json:
 *             example:
 *               - id: 1
 *                 username: "john_doe"
 *               - id: 2
 *                 username: "jane_doe"
 */
router.get("/games/:id/players", asyncHandler(async (req, res) => {
    const gameId = parseInt(req.params.id, 10);
    if (isNaN(gameId)) throw new ApiError(400, "Invalid game ID");

    const [players] = await pool.query(
        `SELECT u.id, u.username FROM game_players gp
         JOIN users u ON gp.user = u.id
         WHERE gp.game = ?`,
        [gameId]
    );
    res.json(players);
}));

/**
 * @swagger
 * /api/games/{id}/turns:
 *   post:
 *     summary: Record a new turn for a player
 *     parameters:
 *       - name: id
 *         in: path
 *         required: true
 *         schema:
 *           type: integer
 *     responses:
 *       201:
 *         description: Turn recorded
 *         content:
 *           application/json:
 *             example:
 *               turnId: 1
 *               nextPlayer: 2
 *       400:
 *         description: Validation error / Invalid game ID
 *       403:
 *         description: Not your turn
 *       500:
 *         description: Failed to record turn
 */
router.post("/games/:id/turns", asyncHandler(async (req, res) => {
    const gameId = parseInt(req.params.id, 10);
    if (isNaN(gameId)) throw new ApiError(400, "Invalid game ID");

    const { error, value } = turnSchema.validate(req.body);
    if (error) throw new ApiError(400, error.details[0].message);

    const { user, first_throw, second_throw, third_throw } = value;
    const conn = await pool.getConnection();

    try {
        await conn.beginTransaction();

        const [gameRows] = await conn.query(
            "SELECT current_playing_user, turn_order FROM games WHERE id = ? FOR UPDATE",
            [gameId]
        );
        if (!gameRows.length) throw new ApiError(404, "Game not found");
        const game = gameRows[0];
        if (game.current_playing_user !== user) throw new ApiError(403, "Not your turn");

        const [lastTurn] = await conn.query(
            "SELECT end_points FROM turns WHERE game = ? AND user = ? ORDER BY id DESC LIMIT 1",
            [gameId, user]
        );

        const start_points = lastTurn.length ? lastTurn[0].end_points : 501;
        const totalScore = first_throw + second_throw + third_throw;
        let end_points = start_points - totalScore;

        if (end_points < 0) {
            end_points = start_points;
        }

        const round_number = lastTurn.length ? lastTurn[0].round_number + 1 : 1;

        const [result] = await conn.query(
            `INSERT INTO turns
             (game, user, round_number, start_points, first_throw, second_throw, third_throw, end_points)
             VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
            [gameId, user, round_number, start_points, first_throw, second_throw, third_throw, end_points]
        );

        const turnOrder = JSON.parse(game.turn_order || '[]');
        const currentIndex = turnOrder.indexOf(user);
        const nextIndex = (currentIndex + 1) % turnOrder.length;
        const nextPlayer = turnOrder[nextIndex];

        await conn.query(
            "UPDATE games SET current_playing_user = ? WHERE id = ?",
            [nextPlayer, gameId]
        );

        await conn.commit();
        res.status(201).json({ turnId: result.insertId, start_points, end_points, nextPlayer });
    } catch (err) {
        await conn.rollback();
        throw new ApiError(500, "Failed to record turn", err);
    } finally {
        conn.release();
    }
}));

/**
 * @swagger
 * /api/games/{id}/turns:
 *   get:
 *     summary: Get turns of a game
 *     parameters:
 *       - name: id
 *         in: path
 *         required: true
 *         schema:
 *           type: integer
 *     responses:
 *       201:
 *         description: The turn
 *         content:
 *           application/json:
 *             example:
 *               turnId: 1
 *               nextPlayer: 2
 */
router.get("/games/:id/turns", asyncHandler(async (req, res) => {
    const gameId = parseInt(req.params.id, 10);
    if (isNaN(gameId)) throw new ApiError(400, "Invalid game ID");

    const [rows] = await pool.query(
        "SELECT * FROM turns WHERE game = ? ORDER BY round_number",
        [gameId]
    );
    res.json(rows);
}));

/**
 * @swagger
 * /api/turns/{id}:
 *   get:
 *     summary: Get a specific turn by ID
 *     parameters:
 *       - name: id
 *         in: path
 *         required: true
 *         schema:
 *           type: integer
 *     responses:
 *       200:
 *         description: Turn data
 *         content:
 *           application/json:
 *             example:
 *               id: 1
 *               game: 1
 *               user: 1
 *               round_number: 1
 *               start_points: 501
 *               first_throw: 60
 *               second_throw: 45
 *               third_throw: 36
 *               end_points: 360
 */
router.get("/turns/:id", asyncHandler(async (req, res) => {
    const turnId = parseInt(req.params.id, 10);
    if (isNaN(turnId)) throw new ApiError(400, "Invalid turn ID");

    const [rows] = await pool.query("SELECT * FROM turns WHERE id = ?", [turnId]);
    if (!rows.length) throw new ApiError(404, "Turn not found");

    res.json(rows[0]);
}));

export default router;
