/**
 * Crossword Puzzle Controller
 *
 * Routes:
 *   GET    /api/crossword/games                          - list games (visibility filter)
 *   POST   /api/crossword/games                          - create draft + clues, run generator, set ready
 *   GET    /api/crossword/games/:id                      - single game + clues (hides answers from students)
 *   PUT    /api/crossword/games/:id                      - owner/admin metadata update
 *   DELETE /api/crossword/games/:id                      - soft delete (owner/admin)
 *   POST   /api/crossword/games/:id/publish              - owner/admin: set is_published=1
 *   POST   /api/crossword/sessions                       - student: start a session
 *   PUT    /api/crossword/sessions/:id                   - student: debounced save of grid state
 *   POST   /api/crossword/sessions/:id/submit            - student: submit, server validates + awards points
 *   GET    /api/crossword/sessions/me                    - student's own sessions
 *   GET    /api/crossword/leaderboard                    - top students by total points_earned
 *
 * Design notes
 * ------------
 * 1. The grid is ALWAYS computed server-side via services/crosswordGenerator. Clients
 *    only receive `cells` (the populated board) and `placements` (without `answer`
 *    for students). The server re-validates submissions against the stored grid.
 * 2. word_count and difficulty are cached on the game record at create time so the
 *    list endpoint can filter/sort without parsing grid_layout JSON.
 * 3. Session start is idempotent: if a student already has an in-progress session for
 *    a game, the existing session is returned. This makes accidental double-clicks safe.
 * 4. Award formula: points_earned = floor(correctCount * reward_points / totalCount).
 *    For partial completion the student still gets credit for the cells they got right.
 */

'use strict';

const express = require('express');
const router = express.Router();
const path = require('path');
const Database = require('better-sqlite3');
const { v4: uuidv4 } = require('uuid');

const dbPath = process.env.DB_PATH || path.join(__dirname, '../../data/school.db');
const db = new Database(dbPath);
db.pragma('foreign_keys = ON');

// Auth middleware (matches the inline pattern in other controllers)
const auth = (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) return res.status(401).json({ error: 'No token provided' });
  try {
    req.user = require('jsonwebtoken').verify(
      token,
      require('../config').jwtSecret
    );
    next();
  } catch (err) {
    res.status(401).json({ error: 'Invalid token' });
  }
};

const setSyncPending = (table, id) => {
  try {
    db.prepare(`UPDATE ${table} SET sync_status = 'pending' WHERE id = ?`).run(id);
  } catch (e) {
    /* swallow - never let sync bookkeeping break a write */
  }
};

const TEACHER_ROLES = ['teacher', 'school_admin', 'systems_admin', 'principal'];
const ADMIN_ROLES = ['school_admin', 'systems_admin', 'principal'];

const isOwnerOrAdmin = (game, user) => {
  if (!game || !user) return false;
  if (ADMIN_ROLES.includes(user.role)) return true;
  return game.created_by === user.id;
};

// ============ GAMES ============

/**
 * @openapi
 * /api/crossword/games:
 *   get:
 *     summary: List crossword games
 *     description: |
 *       Returns up to 200 crossword games, ordered by creation
 *       time (most recent first). Visibility depends on the
 *       caller's role:
 *         - Students see only `is_published = 1` games.
 *         - Teachers see games they created plus published
 *           games.
 *         - Admins (school/systems) and principals see
 *           everything.
 *
 *       Optional filters: `status`, `difficulty`, `subject_id`.
 *     tags:
 *       - Crossword
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: query
 *         name: status
 *         schema: { type: string }
 *         description: Game status (e.g. `ready`, `draft`).
 *       - in: query
 *         name: difficulty
 *         schema: { type: string }
 *         description: Difficulty label as stored on the game.
 *       - in: query
 *         name: subject_id
 *         schema: { type: integer }
 *     responses:
 *       200:
 *         description: Game list
 *         content:
 *           application/json:
 *             schema:
 *               type: array
 *               items: { type: object }
 *       401: { description: Unauthorized }
 *       500: { description: Server error }
 *   post:
 *     summary: Create crossword game
 *     description: |
 *       Creates a draft game, runs the crossword generator over
 *       the supplied clues, persists the resulting grid, and
 *       stores one `crossword_clues` row per placement. The new
 *       game is saved with `status = 'ready'` and
 *       `is_published = 0`. Restricted to `teacher`,
 *       `school_admin`, `systems_admin`, and `principal` roles.
 *     tags:
 *       - Crossword
 *     security:
 *       - bearerAuth: []
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             required:
 *               - title
 *               - clues
 *             properties:
 *               title: { type: string }
 *               description: { type: string }
 *               subject_id: { type: integer }
 *               class_id: { type: integer }
 *               reward_points:
 *                 type: integer
 *                 description: Defaults to 10 when omitted.
 *               time_limit_seconds:
 *                 type: integer
 *                 nullable: true
 *               clues:
 *                 type: array
 *                 items:
 *                   type: object
 *                   required:
 *                     - clue_text
 *                     - answer
 *                   properties:
 *                     clue_text: { type: string }
 *                     answer: { type: string }
 *                     direction:
 *                       type: string
 *                       enum: [across, down]
 *                     hint: { type: string }
 *     responses:
 *       201:
 *         description: Created game record
 *         content:
 *           application/json:
 *             schema: { type: object }
 *       400:
 *         description: Validation or generator error
 *       401: { description: Unauthorized }
 *       403:
 *         description: Teacher or admin role required
 */
router.get('/games', auth, (req, res) => {
  const { status, difficulty, subject_id } = req.query;
  const user = req.user;

  let query = 'SELECT * FROM crossword_games WHERE is_deleted = 0';
  const params = [];

  // Visibility filter
  if (user.role === 'student') {
    query += ' AND is_published = 1';
  } else if (user.role === 'teacher') {
    query += ' AND (created_by = ? OR is_published = 1)';
    params.push(user.id);
  }
  // admins see all (no extra clause)

  if (status) {
    query += ' AND status = ?';
    params.push(status);
  }
  if (difficulty) {
    query += ' AND difficulty = ?';
    params.push(difficulty);
  }
  if (subject_id) {
    query += ' AND subject_id = ?';
    params.push(subject_id);
  }

  query += ' ORDER BY created_at DESC LIMIT 200';

  const games = db.prepare(query).all(...params);
  res.json(games);
});

/**
 * @openapi
 * /api/crossword/games/{id}:
 *   get:
 *     summary: Get crossword game by id
 *     description: |
 *       Returns a single game together with its clues and the
 *       parsed `grid_layout`. Students never receive the
 *       `answer` field on placements or clues; authors and
 *       admins always do. The response includes a
 *       `_permissions` block describing what the caller may
 *       do with the game.
 *     tags:
 *       - Crossword
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: id
 *         required: true
 *         schema: { type: integer }
 *     responses:
 *       200:
 *         description: Game detail with grid, clues, and permissions
 *         content:
 *           application/json:
 *             schema: { type: object }
 *       401: { description: Unauthorized }
 *       403: { description: Game not published (student only) }
 *       404: { description: Game not found }
 *       500: { description: Corrupted grid_layout }
 *   put:
 *     summary: Update crossword game
 *     description: |
 *       Updates editable metadata on the game. Only the game's
 *       owner or an admin (`school_admin`, `systems_admin`,
 *       `principal`) may update. Unspecified fields are left
 *       unchanged via `COALESCE`.
 *     tags:
 *       - Crossword
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: id
 *         required: true
 *         schema: { type: integer }
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             properties:
 *               title: { type: string }
 *               description: { type: string }
 *               reward_points: { type: integer }
 *               time_limit_seconds:
 *                 type: integer
 *                 nullable: true
 *               is_published: { type: boolean }
 *     responses:
 *       200:
 *         description: Updated game
 *         content:
 *           application/json:
 *             schema: { type: object }
 *       401: { description: Unauthorized }
 *       403: { description: Owner or admin only }
 *       404: { description: Game not found }
 *   delete:
 *     summary: Soft-delete crossword game
 *     description: |
 *       Marks the game and its clues as deleted. Sessions
 *       referencing the game are left intact as a historical
 *       record. Owner or admin only.
 *     tags:
 *       - Crossword
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: id
 *         required: true
 *         schema: { type: integer }
 *     responses:
 *       200:
 *         description: Deleted
 *         content:
 *           application/json:
 *             schema: { type: object }
 *       401: { description: Unauthorized }
 *       403: { description: Owner or admin only }
 *       404: { description: Game not found }
 */
router.get('/games/:id', auth, (req, res) => {
  const gameId = req.params.id;
  const game = db.prepare(
    'SELECT * FROM crossword_games WHERE id = ? AND is_deleted = 0'
  ).get(gameId);

  if (!game) return res.status(404).json({ error: 'Game not found' });

  const user = req.user;
  const isStudent = user.role === 'student';
  // Students never see answers pre-submit. Owners and admins always do.
  const canSeeAnswers = !isStudent || isOwnerOrAdmin(game, user);

  // Visibility: students can only GET published games they don't own
  if (isStudent && !game.is_published) {
    return res.status(403).json({ error: 'This game is not published' });
  }

  const clues = db.prepare(
    'SELECT * FROM crossword_clues WHERE game_id = ? AND is_deleted = 0 ORDER BY clue_number, direction'
  ).all(gameId);

  let gridLayout = null;
  try {
    gridLayout = game.grid_layout ? JSON.parse(game.grid_layout) : null;
  } catch (e) {
    return res.status(500).json({ error: 'Corrupted grid_layout for game ' + gameId });
  }

  // Parse placements from grid_layout (we keep them inside grid_layout for portability)
  const placements = (gridLayout && Array.isArray(gridLayout.placements)) ? gridLayout.placements : [];

  // For students, strip the answers from placements
  const safePlacements = canSeeAnswers
    ? placements
    : placements.map((p) => ({ ...p, answer: undefined }));

  // For students, also strip answers from clues (they only see clue_text + direction + number)
  const safeClues = canSeeAnswers
    ? clues
    : clues.map((c) => ({ ...c, answer: undefined }));

  res.json({
    ...game,
    grid_layout: gridLayout
      ? {
          rows: gridLayout.rows,
          cols: gridLayout.cols,
          cells: gridLayout.cells,
          placements: safePlacements,
        }
      : null,
    clues: safeClues,
    _permissions: {
      canEdit: isOwnerOrAdmin(game, user),
      canSeeAnswers,
    },
  });
});

router.post('/games', auth, (req, res) => {
  if (!TEACHER_ROLES.includes(req.user.role)) {
    return res.status(403).json({ error: 'Only teachers and admins can create crossword games' });
  }

  const { title, description, subject_id, class_id, reward_points, time_limit_seconds, clues } = req.body;

  if (!title || typeof title !== 'string' || !title.trim()) {
    return res.status(400).json({ error: 'title is required' });
  }
  if (!Array.isArray(clues) || clues.length === 0) {
    return res.status(400).json({ error: 'at least one clue is required' });
  }

  // Validate each clue shape
  for (const [i, c] of clues.entries()) {
    if (!c || typeof c !== 'object') {
      return res.status(400).json({ error: `clue[${i}] must be an object` });
    }
    if (!c.clue_text || typeof c.clue_text !== 'string' || !c.clue_text.trim()) {
      return res.status(400).json({ error: `clue[${i}].clue_text is required` });
    }
    if (!c.answer || typeof c.answer !== 'string') {
      return res.status(400).json({ error: `clue[${i}].answer is required` });
    }
    if (c.direction && !['across', 'down'].includes(c.direction)) {
      return res.status(400).json({ error: `clue[${i}].direction must be 'across' or 'down'` });
    }
  }

  // Run the generator (lazy require so unit tests can mock it).
  const { generateGrid } = require('../services/crosswordGenerator');
  let layout;
  try {
    layout = generateGrid(clues);
  } catch (e) {
    return res.status(400).json({ error: 'Grid generation failed: ' + e.message });
  }

  // The generator returns placements whose `clue_text` and `answer` are the
  // semantically correct pair (input clues are normalized and laid out; the
  // placement carries the answer that belongs with its own clue_text). To avoid
  // any input-index / placement-index mis-pairing, insert one clue row per
  // placement directly. No lookup table is needed.
  //
  // Each placement carries: clue_id (original input index), clue_text, answer,
  // direction, start_row, start_col, clue_number. `hint` is the only field that
  // comes from the user's input — we look it up by input index from the original
  // clues array (input index == placement.clue_id after the generator fix).

  const uid = uuidv4();
  const now = new Date().toISOString();

  // Hint lookup keyed by original input index (= placement.clue_id after the
  // generator fix). Optional: a teacher may have omitted hint on some clues.
  const hintByInputIdx = new Map();
  for (let i = 0; i < clues.length; i++) {
    hintByInputIdx.set(i, clues[i].hint || null);
  }

  // Insert in a single transaction so a failed clue insert doesn't leave a half-built game.
  const create = db.transaction(() => {
    const gameResult = db.prepare(`
      INSERT INTO crossword_games (
        uid, title, description, subject_id, class_id, created_by,
        status, grid_layout, word_count, difficulty,
        reward_points, time_limit_seconds, is_published,
        sync_status, created_at, updated_at
      )
      VALUES (?, ?, ?, ?, ?, ?, 'ready', ?, ?, ?, ?, ?, 0, 'pending', ?, ?)
    `).run(
      uid,
      title.trim(),
      description || null,
      subject_id || null,
      class_id || null,
      req.user.id,
      JSON.stringify(layout),
      layout.word_count,
      layout.difficulty,
      reward_points != null ? Number(reward_points) : 10,
      time_limit_seconds != null ? Number(time_limit_seconds) : null,
      now,
      now,
    );
    const gameId = gameResult.lastInsertRowid;

    const clueInsert = db.prepare(`
      INSERT INTO crossword_clues (
        uid, game_id, clue_text, answer, direction,
        start_row, start_col, clue_number, hint,
        sync_status, created_at, updated_at
      )
      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?)
    `);
    for (const p of layout.placements) {
      const hint = hintByInputIdx.has(p.clue_id) ? hintByInputIdx.get(p.clue_id) : null;
      clueInsert.run(
        uuidv4(),
        gameId,
        p.clue_text,
        p.answer,                  // placement carries the answer that belongs with its own clue_text
        p.direction,
        p.start_row,
        p.start_col,
        p.clue_number,
        hint,
        now,
        now,
      );
    }
    return gameId;
  });

  let gameId;
  try {
    gameId = create();
  } catch (e) {
    return res.status(400).json({ error: 'Failed to create game: ' + e.message });
  }

  const created = db.prepare(
    'SELECT * FROM crossword_games WHERE id = ?'
  ).get(gameId);
  // Avoid returning the raw grid_layout twice — controller just round-trips the game
  res.status(201).json(created);
});

// Update a game (title, description, reward, time limit, publish flag).
router.put('/games/:id', auth, (req, res) => {
  const game = db.prepare(
    'SELECT * FROM crossword_games WHERE id = ? AND is_deleted = 0'
  ).get(req.params.id);
  if (!game) return res.status(404).json({ error: 'Game not found' });
  if (!isOwnerOrAdmin(game, req.user)) {
    return res.status(403).json({ error: 'Only the owner or an admin can update this game' });
  }

  const { title, description, reward_points, time_limit_seconds, is_published } = req.body;

  db.prepare(`
    UPDATE crossword_games SET
      title              = COALESCE(?, title),
      description        = COALESCE(?, description),
      reward_points      = COALESCE(?, reward_points),
      time_limit_seconds = COALESCE(?, time_limit_seconds),
      is_published       = COALESCE(?, is_published),
      updated_at         = CURRENT_TIMESTAMP,
      sync_status        = 'pending'
    WHERE id = ?
  `).run(
    title ?? null,
    description ?? null,
    reward_points != null ? Number(reward_points) : null,
    time_limit_seconds != null ? Number(time_limit_seconds) : null,
    is_published != null ? (is_published ? 1 : 0) : null,
    req.params.id,
  );

  const updated = db.prepare(
    'SELECT * FROM crossword_games WHERE id = ?'
  ).get(req.params.id);
  res.json(updated);
});

// Soft-delete a game (owner or admin).
router.delete('/games/:id', auth, (req, res) => {
  const game = db.prepare(
    'SELECT * FROM crossword_games WHERE id = ? AND is_deleted = 0'
  ).get(req.params.id);
  if (!game) return res.status(404).json({ error: 'Game not found' });
  if (!isOwnerOrAdmin(game, req.user)) {
    return res.status(403).json({ error: 'Only the owner or an admin can delete this game' });
  }

  const tx = db.transaction(() => {
    db.prepare(`
      UPDATE crossword_games SET is_deleted = 1, sync_status = 'pending', updated_at = CURRENT_TIMESTAMP
      WHERE id = ?
    `).run(req.params.id);
    db.prepare(`
      UPDATE crossword_clues SET is_deleted = 1, sync_status = 'pending', updated_at = CURRENT_TIMESTAMP
      WHERE game_id = ?
    `).run(req.params.id);
    // Don't touch sessions — they remain as a historical record.
  });
  tx();

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

// Explicit publish endpoint — convenient for the UI flow.
/**
 * @openapi
 * /api/crossword/games/{id}/publish:
 *   post:
 *     summary: Publish crossword game
 *     description: |
 *       Flips `is_published` to 1 on the game. Refuses unless
 *       the game is in `ready` status. Owner or admin only.
 *     tags:
 *       - Crossword
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: id
 *         required: true
 *         schema: { type: integer }
 *     responses:
 *       200:
 *         description: Published game
 *         content:
 *           application/json:
 *             schema: { type: object }
 *       400: { description: Game not in ready status }
 *       401: { description: Unauthorized }
 *       403: { description: Owner or admin only }
 *       404: { description: Game not found }
 */
router.post('/games/:id/publish', auth, (req, res) => {
  const game = db.prepare(
    'SELECT * FROM crossword_games WHERE id = ? AND is_deleted = 0'
  ).get(req.params.id);
  if (!game) return res.status(404).json({ error: 'Game not found' });
  if (!isOwnerOrAdmin(game, req.user)) {
    return res.status(403).json({ error: 'Only the owner or an admin can publish this game' });
  }
  if (game.status !== 'ready') {
    return res.status(400).json({ error: 'Only ready games can be published' });
  }

  db.prepare(`
    UPDATE crossword_games SET
      is_published = 1, updated_at = CURRENT_TIMESTAMP, sync_status = 'pending'
    WHERE id = ?
  `).run(req.params.id);

  const updated = db.prepare(
    'SELECT * FROM crossword_games WHERE id = ?'
  ).get(req.params.id);
  res.json(updated);
});

// ============ SESSIONS ============

// Student starts a session for a published game. Idempotent — if an in-progress
// session already exists, return that one.
/**
 * @openapi
 * /api/crossword/sessions:
 *   post:
 *     summary: Start crossword session
 *     description: |
 *       Starts a new session for the calling student on a
 *       published game. Idempotent: if the student already has
 *       an in-progress session for the game, that session is
 *       returned instead (HTTP 200). Otherwise a new session is
 *       created with `correct_count = 0`, `points_earned = 0`,
 *       and an empty `current_state` (HTTP 201). Restricted to
 *       the `student` role.
 *     tags:
 *       - Crossword
 *     security:
 *       - bearerAuth: []
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             required:
 *               - game_id
 *             properties:
 *               game_id: { type: integer }
 *     responses:
 *       200:
 *         description: Existing in-progress session returned
 *         content:
 *           application/json:
 *             schema: { type: object }
 *       201:
 *         description: Newly created session
 *         content:
 *           application/json:
 *             schema: { type: object }
 *       400: { description: game_id is required }
 *       401: { description: Unauthorized }
 *       403:
 *         description: Student only, or game not published
 *       404: { description: Game not found }
 */
router.post('/sessions', auth, (req, res) => {
  if (req.user.role !== 'student') {
    return res.status(403).json({ error: 'Only students can start a crossword session' });
  }
  const { game_id } = req.body;
  if (!game_id) return res.status(400).json({ error: 'game_id is required' });

  const game = db.prepare(
    'SELECT * FROM crossword_games WHERE id = ? AND is_deleted = 0'
  ).get(game_id);
  if (!game) return res.status(404).json({ error: 'Game not found' });
  if (!game.is_published) {
    return res.status(403).json({ error: 'Game is not published' });
  }

  // Look for an existing in-progress session.
  const existing = db.prepare(`
    SELECT * FROM crossword_sessions
    WHERE game_id = ? AND student_id = ? AND completed_at IS NULL AND is_deleted = 0
    ORDER BY id DESC LIMIT 1
  `).get(game_id, req.user.id);
  if (existing) {
    return res.status(200).json(existing);
  }

  const totalCount = game.word_count || 0;
  const uid = uuidv4();
  const result = db.prepare(`
    INSERT INTO crossword_sessions (
      uid, game_id, student_id, current_state,
      correct_count, total_count, points_earned,
      sync_status, created_at, updated_at
    )
    VALUES (?, ?, ?, '{}', 0, ?, 0, 'pending', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
  `).run(uid, game_id, req.user.id, totalCount);

  const session = db.prepare(
    'SELECT * FROM crossword_sessions WHERE id = ?'
  ).get(result.lastInsertRowid);
  res.status(201).json(session);
});

// Save the student's current grid state (debounced on the client, but server
// applies every call — last write wins).
/**
 * @openapi
 * /api/crossword/sessions/{id}:
 *   put:
 *     summary: Save crossword session state
 *     description: |
 *       Persists the student's current grid state on the
 *       session. Accepts either an object or a JSON string for
 *       `current_state`. The student may only edit their own
 *       in-progress session. Server applies every call
 *       (last-write-wins).
 *     tags:
 *       - Crossword
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: id
 *         required: true
 *         schema: { type: integer }
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             required:
 *               - current_state
 *             properties:
 *               current_state:
 *                 description: |
 *                   Grid state as a JSON object, or a JSON
 *                   string. The server stores it verbatim.
 *     responses:
 *       200:
 *         description: Updated session
 *         content:
 *           application/json:
 *             schema: { type: object }
 *       400:
 *         description: current_state required, or session already completed
 *       401: { description: Unauthorized }
 *       403: { description: Not your session }
 *       404: { description: Session not found }
 */
router.put('/sessions/:id', auth, (req, res) => {
  const session = db.prepare(
    'SELECT * FROM crossword_sessions WHERE id = ? AND is_deleted = 0'
  ).get(req.params.id);
  if (!session) return res.status(404).json({ error: 'Session not found' });
  if (session.student_id !== req.user.id) {
    return res.status(403).json({ error: 'Cannot edit another student\'s session' });
  }
  if (session.completed_at) {
    return res.status(400).json({ error: 'Session already completed' });
  }
  const { current_state } = req.body;
  if (current_state === undefined || current_state === null) {
    return res.status(400).json({ error: 'current_state is required' });
  }
  // Accept either an object or a JSON string (clients sometimes serialize twice).
  const stateJson = typeof current_state === 'string' ? current_state : JSON.stringify(current_state);

  db.prepare(`
    UPDATE crossword_sessions SET
      current_state = ?,
      updated_at = CURRENT_TIMESTAMP,
      sync_status = 'pending'
    WHERE id = ?
  `).run(stateJson, req.params.id);

  const updated = db.prepare(
    'SELECT * FROM crossword_sessions WHERE id = ?'
  ).get(req.params.id);
  res.json(updated);
});

// Submit the session: validate, award points, mark completed.
/**
 * @openapi
 * /api/crossword/sessions/{id}/submit:
 *   post:
 *     summary: Submit crossword session
 *     description: |
 *       Finalizes the session. Server-side validation re-checks
 *       every cell against the stored grid; awards
 *       `points_earned = floor(correctCount * reward_points /
 *       totalCount)` and marks `completed_at`. The latest saved
 *       `current_state` is used, but the client may also pass a
 *       fresh `current_state` in this submit body. Time taken
 *       is computed from `created_at`. The student may only
 *       submit their own in-progress session.
 *     tags:
 *       - Crossword
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: id
 *         required: true
 *         schema: { type: integer }
 *     requestBody:
 *       required: false
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             properties:
 *               current_state:
 *                 description: |
 *                   Optional fresh grid state, object or JSON
 *                   string. Used when the latest debounced save
 *                   has not landed yet.
 *     responses:
 *       200:
 *         description: Graded session with per-clue breakdown
 *         content:
 *           application/json:
 *             schema: { type: object }
 *       400:
 *         description: Malformed current_state, or session already completed
 *       401: { description: Unauthorized }
 *       403: { description: Not your session }
 *       404:
 *         description: Session or game not found
 *       500: { description: Corrupted grid_layout }
 */
router.post('/sessions/:id/submit', auth, (req, res) => {
  const session = db.prepare(
    'SELECT * FROM crossword_sessions WHERE id = ? AND is_deleted = 0'
  ).get(req.params.id);
  if (!session) return res.status(404).json({ error: 'Session not found' });
  if (session.student_id !== req.user.id) {
    return res.status(403).json({ error: 'Cannot submit another student\'s session' });
  }
  if (session.completed_at) {
    return res.status(400).json({ error: 'Session already completed' });
  }

  const game = db.prepare(
    'SELECT * FROM crossword_games WHERE id = ? AND is_deleted = 0'
  ).get(session.game_id);
  if (!game) return res.status(404).json({ error: 'Game not found' });

  let gridLayout;
  try {
    gridLayout = JSON.parse(game.grid_layout);
  } catch (e) {
    return res.status(500).json({ error: 'Corrupted grid_layout' });
  }

  // Parse the saved state. Accept object or JSON string.
  // If the DB state is corrupted, log a warning but don't 400 — the client
  // may still pass a fresh state in the submit body below.
  let userState = {};
  if (session.current_state) {
    try {
      userState = typeof session.current_state === 'string'
        ? JSON.parse(session.current_state)
        : session.current_state;
    } catch (e) {
      console.warn(`crossword submit: corrupted saved current_state for session ${req.params.id}: ${e.message}`);
      userState = {};
    }
  }
  // Allow the client to also pass a fresh grid state in the submit payload
  // (in case the last debounced save hasn't landed yet, or the saved state
  // was corrupt). Guard the parse — return 400 on malformed input.
  if (req.body && req.body.current_state !== undefined && req.body.current_state !== null) {
    try {
      userState = typeof req.body.current_state === 'string'
        ? JSON.parse(req.body.current_state)
        : req.body.current_state;
    } catch (e) {
      return res.status(400).json({ error: 'Malformed current_state in submit body: ' + e.message });
    }
  }

  const { validateAnswer } = require('../services/crosswordGenerator');
  const result = validateAnswer(
    { rows: gridLayout.rows, cols: gridLayout.cols, cells: gridLayout.cells },
    gridLayout.placements,
    userState,
  );

  // Award: correctCount * reward_points / totalCount, floored.
  const reward = game.reward_points || 0;
  const totalCount = gridLayout.placements.length || 1;
  const pointsEarned = Math.floor((result.correctCount * reward) / totalCount);

  // Time taken: from created_at to now (seconds). If we have a started_at later, prefer that.
  const startedAt = new Date(session.created_at + 'Z').getTime();
  const elapsedSec = Math.max(0, Math.floor((Date.now() - startedAt) / 1000));

  const tx = db.transaction(() => {
    db.prepare(`
      UPDATE crossword_sessions SET
        correct_count = ?,
        total_count   = ?,
        points_earned = ?,
        time_taken_seconds = ?,
        completed_at  = CURRENT_TIMESTAMP,
        updated_at    = CURRENT_TIMESTAMP,
        sync_status   = 'pending'
      WHERE id = ?
    `).run(result.correctCount, result.totalCount, pointsEarned, elapsedSec, req.params.id);
  });
  tx();

  const updated = db.prepare(
    'SELECT * FROM crossword_sessions WHERE id = ?'
  ).get(req.params.id);

  res.json({
    ...updated,
    is_complete: result.isComplete,
    correct_count: result.correctCount,
    total_count: result.totalCount,
    points_earned: pointsEarned,
    per_clue: result.perClue,
    // We deliberately do NOT return the answers here unless the student completed it.
    // UI can re-fetch the game (still without answers) to show a "review" view.
  });
});

// Student's own sessions (most recent first).
/**
 * @openapi
 * /api/crossword/sessions/me:
 *   get:
 *     summary: My crossword sessions
 *     description: |
 *       Returns the calling student's most recent 100 crossword
 *       sessions joined with the parent game's title,
 *       difficulty, and word count. Restricted to the `student`
 *       role.
 *     tags:
 *       - Crossword
 *     security:
 *       - bearerAuth: []
 *     responses:
 *       200:
 *         description: Session list
 *         content:
 *           application/json:
 *             schema:
 *               type: array
 *               items: { type: object }
 *       401: { description: Unauthorized }
 *       403: { description: Student only }
 */
router.get('/sessions/me', auth, (req, res) => {
  if (req.user.role !== 'student') {
    return res.status(403).json({ error: 'Only students can list their own sessions' });
  }
  const sessions = db.prepare(`
    SELECT cs.*, cg.title as game_title, cg.difficulty, cg.word_count
    FROM crossword_sessions cs
    JOIN crossword_games cg ON cs.game_id = cg.id
    WHERE cs.student_id = ? AND cs.is_deleted = 0 AND cg.is_deleted = 0
    ORDER BY cs.created_at DESC
    LIMIT 100
  `).all(req.user.id);
  res.json(sessions);
});

// Leaderboard: top students by total points_earned (sum across all sessions).
/**
 * @openapi
 * /api/crossword/leaderboard:
 *   get:
 *     summary: Crossword leaderboard
 *     description: |
 *       Returns active students ranked by the sum of
 *       `points_earned` across their crossword sessions, with
 *       `sessions_completed` and `perfect_completions` counts.
 *       Optional `limit` query parameter (clamped to 1-100,
 *       default 20).
 *     tags:
 *       - Crossword
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: query
 *         name: limit
 *         schema: { type: integer, minimum: 1, maximum: 100 }
 *     responses:
 *       200:
 *         description: Leaderboard rows
 *         content:
 *           application/json:
 *             schema:
 *               type: array
 *               items: { type: object }
 *       401: { description: Unauthorized }
 */
router.get('/leaderboard', auth, (req, res) => {
  const { limit } = req.query;
  const maxRows = Math.min(Math.max(parseInt(limit, 10) || 20, 1), 100);

  const rows = db.prepare(`
    SELECT
      u.id            AS student_id,
      u.first_name,
      u.last_name,
      COALESCE(SUM(cs.points_earned), 0)               AS total_points,
      COUNT(cs.id)                                     AS sessions_completed,
      SUM(CASE WHEN (cs.completed_at IS NOT NULL
                     AND cs.correct_count = cs.total_count) THEN 1 ELSE 0 END) AS perfect_completions
    FROM users u
    LEFT JOIN crossword_sessions cs
      ON cs.student_id = u.id AND cs.is_deleted = 0
    WHERE u.role = 'student' AND u.is_active = 1 AND u.is_deleted = 0
    GROUP BY u.id
    HAVING total_points > 0
    ORDER BY total_points DESC, sessions_completed DESC
    LIMIT ?
  `).all(maxRows);
  res.json(rows);
});

module.exports = router;