/**
 * In-memory room registry, SQLite as the durable backing store. Every
 * authoritative state change flows through applyAndBroadcast(), which is the
 * only place the shared reducer is invoked server-side.
 */

import crypto from 'node:crypto';
import { reduce, getLegalIntents } from '../shared/game.js';
import { board } from '../shared/board.js';
import { rollsNeededFor, DIE_SIZES } from '../shared/tileEffects.js';
import * as db from './db.js';

export class RoomError extends Error {}

const rooms = new Map(); // gameId -> { gameId, state, sockets: Set<WebSocket> }

export function loadOrCreateRoom(gameId) {
  const existing = rooms.get(gameId);
  if (existing) return existing;
  const row = db.getGameById(gameId);
  if (!row) throw new RoomError(`Game not found: ${gameId}`);
  const room = { gameId, state: row.state, sockets: new Set() };
  rooms.set(gameId, room);
  return room;
}

export function attachSocket(room, ws, playerId) {
  ws.playerId = playerId;
  room.sockets.add(ws);
  send(ws, { type: 'state', gameId: room.gameId, state: room.state, lastAction: null });
  ws.on('close', () => room.sockets.delete(ws));
}

/** Apply an already-constructed action through the shared reducer, persist,
 * and broadcast the result to every socket in the room. Throws if the
 * reducer rejects the action. */
export function applyAndBroadcast(room, action) {
  const newState = reduce(room.state, action);
  room.state = newState;
  db.updateGameState(room.gameId, newState, newState.status);
  broadcast(room, { type: 'state', gameId: room.gameId, state: newState, lastAction: action });
  return newState;
}

/** Validate + resolve a client intent into a real action, then apply it.
 * Sends an {type:'error'} back to the originating socket on rejection. */
export function handleIntent(room, ws, playerId, intent) {
  try {
    const action = buildAction(room.state, playerId, intent);
    applyAndBroadcast(room, action);
  } catch (err) {
    send(ws, { type: 'error', message: err.message });
  }
}

function buildAction(state, playerId, intent) {
  const legal = getLegalIntents(state, playerId);
  if (!legal.includes(intent.type)) {
    throw new Error(`Not allowed: ${intent.type}`);
  }
  switch (intent.type) {
    case 'REQUEST_START':
      return { type: 'START_GAME' };
    case 'REQUEST_ROLL': {
      const player = state.players[playerId];
      const nextSpaceId = board.spaces[player.position].next;
      if (!nextSpaceId) throw new Error('No next space defined — already at the end of the board');
      return { type: 'ROLL', playerId, rolls: rollForSpace(board.spaces[nextSpaceId]) };
    }
    case 'REQUEST_CHOOSE': {
      if (typeof intent.spaceId !== 'string') throw new Error('spaceId is required');
      return { type: 'CHOOSE', playerId, spaceId: intent.spaceId, rolls: rollForSpace(board.spaces[intent.spaceId]) };
    }
    default:
      throw new Error(`Unknown intent: ${intent.type}`);
  }
}

/** Pre-roll whatever dice `space` needs to resolve its own tile effect — the
 * only place actual randomness happens. rollsNeededFor() never touches
 * randomness itself, it just says which die sizes are needed and in what
 * order; reduce() applies the results deterministically. */
function rollForSpace(space) {
  return rollsNeededFor(space).map((die) => {
    const max = DIE_SIZES[die];
    if (!max) throw new Error(`Unknown die size: ${die}`);
    return 1 + crypto.randomInt(max);
  });
}

function broadcast(room, payload) {
  for (const ws of room.sockets) send(ws, payload);
}

function send(ws, payload) {
  if (ws.readyState === ws.OPEN) ws.send(JSON.stringify(payload));
}
