/**
 * Life Journey — pure game reducer.
 *
 * reduce(state, action) -> newState is the ONLY way state changes, and it never
 * generates randomness itself: every roll a tile needs is generated by the
 * server and travels inside the action payload (`rolls`), so server and
 * client can both apply the exact same action through this exact same
 * function and land on identical state. Illegal transitions throw rather
 * than no-op — the server is expected to gate intents with
 * getLegalIntents()/isPlayersTurn() before ever constructing an action, so a
 * throw here means that gate was bypassed.
 *
 * Movement is one tile per turn (board.js's `next` pointer) — there is no
 * movement die; every `die` a tile carries is for resolving that tile's own
 * effect via shared/tileEffects.js, not how far a player travels.
 */

import { board } from './board.js';
import { resolveTileEffect } from './tileEffects.js';

const DEFAULT_CONFIG = {
  minPlayers: 2,
  maxPlayers: 8,
  startingStats: { cash: 0, love: 0, education: 0, wealth: 0, age: 18 },
};
const LOG_LIMIT = 50;

export function createInitialState(config = {}) {
  return {
    boardId: board.id,
    config: { ...DEFAULT_CONFIG, ...config },
    status: 'lobby',
    turnOrder: [],
    turnIndex: 0,
    currentTurn: null,
    winnerId: null,
    players: {},
    log: [],
  };
}

export function reduce(state, action) {
  switch (action.type) {
    case 'JOIN':
      return applyJoin(state, action);
    case 'START_GAME':
      return applyStart(state, action);
    case 'ROLL':
      return applyRoll(state, action);
    case 'CHOOSE':
      return applyChoose(state, action);
    default:
      throw new Error(`Unknown action type: ${action.type}`);
  }
}

/** What intents (if any) `playerId` may legally send right now — the single
 * source of truth used by both server-side validation and client-side UI. */
export function getLegalIntents(state, playerId) {
  const player = state.players[playerId];
  if (!player) return [];
  if (state.status === 'lobby') {
    const count = Object.keys(state.players).length;
    return count >= state.config.minPlayers ? ['REQUEST_START'] : [];
  }
  if (state.status !== 'active' || state.currentTurn !== playerId) return [];
  return player.pendingChoice ? ['REQUEST_CHOOSE'] : ['REQUEST_ROLL'];
}

export function isPlayersTurn(state, playerId) {
  return state.status === 'active' && state.currentTurn === playerId;
}

// --- internals -------------------------------------------------------------

function assertLobby(state) {
  if (state.status !== 'lobby') throw new Error('Game is not in lobby');
}

function assertActive(state) {
  if (state.status !== 'active') throw new Error('Game is not active');
}

function assertPlayersTurn(state, playerId) {
  if (state.currentTurn !== playerId) throw new Error('Not your turn');
}

function applyJoin(state, { playerId, name, seat, color }) {
  assertLobby(state);
  if (state.players[playerId]) throw new Error('Player already joined');
  if (Object.keys(state.players).length >= state.config.maxPlayers) {
    throw new Error('Game is full');
  }
  if (Object.values(state.players).some((p) => p.seat === seat)) {
    throw new Error('Seat already taken');
  }
  const player = {
    id: playerId,
    name,
    seat,
    color,
    position: board.startSpaceId,
    ...state.config.startingStats,
    pendingChoice: null,
    finished: false,
  };
  return { ...state, players: { ...state.players, [playerId]: player } };
}

function applyStart(state) {
  assertLobby(state);
  const players = Object.values(state.players).sort((a, b) => a.seat - b.seat);
  if (players.length < state.config.minPlayers) {
    throw new Error('Not enough players to start');
  }
  const turnOrder = players.map((p) => p.id);
  return { ...state, status: 'active', turnOrder, turnIndex: 0, currentTurn: turnOrder[0] };
}

function applyRoll(state, { playerId, rolls }) {
  assertActive(state);
  assertPlayersTurn(state, playerId);
  const player = state.players[playerId];
  if (player.pendingChoice) throw new Error('Resolve pending choice before rolling');
  const nextSpaceId = board.spaces[player.position].next;
  if (!nextSpaceId) throw new Error('No next space defined — already at the end of the board');
  return landOn(state, playerId, nextSpaceId, rolls, { type: 'ROLL' });
}

function applyChoose(state, { playerId, spaceId, rolls }) {
  assertActive(state);
  assertPlayersTurn(state, playerId);
  const player = state.players[playerId];
  const pending = player.pendingChoice;
  if (!pending) throw new Error('No pending choice');
  if (!pending.options.includes(spaceId)) throw new Error('Illegal choice');
  return landOn(state, playerId, spaceId, rolls, { type: 'CHOOSE' });
}

/** Shared landing logic for both a ROLL's terminal space and a CHOOSE's
 * resolved branch: resolve the space's tile effect, then either leave the
 * turn open (a real choice is pending — only true once board-graph.json
 * populates `choices`), end the game (finish), or advance to the next player. */
function landOn(state, playerId, spaceId, rolls, logMeta) {
  const space = board.spaces[spaceId];
  const { statDelta, description, todo } = resolveTileEffect(space, rolls ?? []);
  const priorPlayer = state.players[playerId];
  const nextPlayer = {
    ...applyStatDelta(priorPlayer, statDelta),
    position: spaceId,
    pendingChoice: space.choices?.length ? { atSpace: spaceId, options: space.choices } : null,
    finished: space.type === 'finish',
  };

  let newState = { ...state, players: { ...state.players, [playerId]: nextPlayer } };
  newState.log = appendLog(state.log, {
    playerId,
    landedOn: spaceId,
    label: space.label,
    statDelta,
    description,
    todo,
    ...logMeta,
  });

  if (space.type === 'finish') {
    newState.status = 'finished';
    newState.winnerId = playerId;
    newState.currentTurn = null;
    return newState;
  }

  if (nextPlayer.pendingChoice) return newState; // turn stays with this player

  return advanceTurn(newState);
}

function applyStatDelta(player, statDelta) {
  const next = { ...player };
  for (const [stat, delta] of Object.entries(statDelta ?? {})) {
    next[stat] = (next[stat] ?? 0) + delta;
  }
  return next;
}

function advanceTurn(state) {
  const turnIndex = (state.turnIndex + 1) % state.turnOrder.length;
  return { ...state, turnIndex, currentTurn: state.turnOrder[turnIndex] };
}

function appendLog(log, entry) {
  const next = [...log, entry];
  return next.length > LOG_LIMIT ? next.slice(next.length - LOG_LIMIT) : next;
}
