/**
 * Life Journey — Phase 1 server.
 *
 * The server is the sole authority over game state: it validates client
 * intents, generates the only source of randomness (the dice roll), builds
 * the real action, and applies it through the exact same shared reducer the
 * browser imports. Rooms/tokens/state are persisted to SQLite so a restart
 * (or a closed tab reconnecting later) resumes rather than resets.
 */

import http from 'node:http';
import path from 'node:path';
import fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import express from 'express';
import { WebSocketServer } from 'ws';

import { board } from '../shared/board.js';
import { createInitialState } from '../shared/game.js';
import * as db from './db.js';
import * as rooms from './rooms.js';
import { randomId, randomToken, randomJoinCode } from './ids.js';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const PORT = process.env.PORT || 3000;
const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, '..', 'data');

fs.mkdirSync(DATA_DIR, { recursive: true });
db.initDb(path.join(DATA_DIR, 'lifegame.db'));

const DEFAULT_CONFIG = {
  minPlayers: 2,
  maxPlayers: 8,
  startingStats: { cash: 0, love: 0, education: 0, wealth: 0, age: 18 },
};
// Deliberately avoids the board's own semantic colors (green=start,
// marigold=finish, red=choice) so a player's token never blends into a space.
const PLAYER_COLORS = [
  '#2f6f9f', '#7a4fae', '#2f8f8f', '#c15fa0', '#a6752c', '#5a6b8c', '#4a4a9f', '#8a8a3f',
];
const AUTH_TIMEOUT_MS = 10_000;

const app = express();
app.use(express.json());
app.use((err, _req, res, next) => {
  if (err?.type === 'entity.parse.failed') return res.status(400).json({ error: 'Invalid JSON' });
  next(err);
});

// --- Health check ---
app.get('/api/health', (_req, res) => {
  res.json({ ok: true, service: 'life-journey', phase: 1, version: '0.1.0', time: new Date().toISOString() });
});

// --- Helpers ---
function cleanName(raw, label) {
  const name = typeof raw === 'string' ? raw.trim() : '';
  if (!name) throw Object.assign(new Error(`${label} is required`), { status: 400 });
  if (name.length > 40) throw Object.assign(new Error(`${label} is too long`), { status: 400 });
  return name;
}

function uniqueJoinCode() {
  let code;
  do {
    code = randomJoinCode();
  } while (db.getGameByCode(code));
  return code;
}

function buildInviteUrl(req, code) {
  const base = (process.env.PUBLIC_URL || `${req.protocol}://${req.get('host')}`).replace(/\/$/, '');
  return `${base}/join/${code}`;
}

function colorForSeat(seat) {
  return PLAYER_COLORS[(seat - 1) % PLAYER_COLORS.length];
}

// --- REST: games/rooms ---
app.post('/api/games', (req, res) => {
  let hostName;
  try {
    hostName = cleanName(req.body?.hostName, 'hostName');
  } catch (err) {
    return res.status(err.status ?? 400).json({ error: err.message });
  }

  const gameId = randomId();
  const code = uniqueJoinCode();
  const initialState = createInitialState(DEFAULT_CONFIG);
  db.createGame({ id: gameId, code, boardId: board.id, config: DEFAULT_CONFIG, state: initialState });

  const room = rooms.loadOrCreateRoom(gameId);
  const playerId = randomId();
  const color = colorForSeat(1);
  db.createPlayer({ id: playerId, gameId, name: hostName, seat: 1, color });

  let state;
  try {
    state = rooms.applyAndBroadcast(room, { type: 'JOIN', playerId, name: hostName, seat: 1, color });
  } catch (err) {
    return res.status(500).json({ error: err.message });
  }

  const sessionToken = randomToken();
  db.createToken({ token: sessionToken, gameId, playerId, kind: 'session' });

  res.status(201).json({
    gameId,
    code,
    inviteUrl: buildInviteUrl(req, code),
    playerId,
    sessionToken,
    state,
  });
});

app.get('/api/games/:code', (req, res) => {
  const game = db.getGameByCode(req.params.code);
  if (!game) return res.status(404).json({ error: 'Game not found' });
  res.json({
    gameId: game.id,
    code: game.code,
    status: game.status,
    config: game.config,
    players: db.getPlayersByGame(game.id),
    state: game.state,
  });
});

app.post('/api/games/:code/join', (req, res) => {
  const game = db.getGameByCode(req.params.code);
  if (!game) return res.status(404).json({ error: 'Game not found' });
  if (game.status !== 'lobby') return res.status(409).json({ error: 'Game already started' });

  let name;
  try {
    name = cleanName(req.body?.name, 'name');
  } catch (err) {
    return res.status(err.status ?? 400).json({ error: err.message });
  }

  const seat = db.countPlayers(game.id) + 1;
  if (seat > game.config.maxPlayers) return res.status(409).json({ error: 'Game is full' });

  const room = rooms.loadOrCreateRoom(game.id);
  const playerId = randomId();
  const color = colorForSeat(seat);
  db.createPlayer({ id: playerId, gameId: game.id, name, seat, color });

  let state;
  try {
    state = rooms.applyAndBroadcast(room, { type: 'JOIN', playerId, name, seat, color });
  } catch (err) {
    return res.status(409).json({ error: err.message });
  }

  const sessionToken = randomToken();
  db.createToken({ token: sessionToken, gameId: game.id, playerId, kind: 'session' });

  res.status(201).json({ gameId: game.id, playerId, sessionToken, state });
});

// --- Static frontend ---
const publicDir = path.join(__dirname, '..', 'public');
app.use(express.static(publicDir));
app.use('/shared', express.static(path.join(__dirname, '..', 'shared')));

// Deep link for invite URLs — express.static won't match this path.
app.get('/join/:code', (_req, res) => {
  res.sendFile(path.join(publicDir, 'index.html'));
});

const server = http.createServer(app);

// --- WebSocket: room-aware, authenticated via a first {type:'AUTH'} message ---
// (not a query param, so the session token never lands in access/proxy logs)
const wss = new WebSocketServer({ server, path: '/ws' });

wss.on('connection', (ws) => {
  let authenticated = false;
  let room = null;
  let playerId = null;

  const authTimeout = setTimeout(() => {
    if (!authenticated) ws.close(4001, 'auth timeout');
  }, AUTH_TIMEOUT_MS);

  ws.on('message', (raw) => {
    let msg;
    try {
      msg = JSON.parse(raw.toString());
    } catch {
      return;
    }

    if (!authenticated) {
      if (msg?.type !== 'AUTH' || typeof msg.token !== 'string') {
        ws.close(4001, 'expected AUTH');
        return;
      }
      const tokenRow = db.getToken(msg.token);
      if (!tokenRow || tokenRow.kind !== 'session') {
        ws.close(4001, 'invalid token');
        return;
      }
      let targetRoom;
      try {
        targetRoom = rooms.loadOrCreateRoom(tokenRow.game_id);
      } catch {
        ws.close(4004, 'game not found');
        return;
      }
      clearTimeout(authTimeout);
      authenticated = true;
      room = targetRoom;
      playerId = tokenRow.player_id;
      rooms.attachSocket(room, ws, playerId);
      db.touchPlayerLastSeen(playerId);
      return;
    }

    if (typeof msg?.type !== 'string') return;
    rooms.handleIntent(room, ws, playerId, msg);
  });

  ws.on('close', () => clearTimeout(authTimeout));
});

server.listen(PORT, () => {
  console.log(`Life Journey (Phase 1) listening on :${PORT}`);
  console.log(`Data directory: ${DATA_DIR}`);
});
