/**
 * Tile-effect resolution: given a board space and the roll value(s) it
 * needs, compute the stat delta to apply. Pure — no randomness happens here;
 * the caller (server/rooms.js) pre-rolls via rollsNeededFor() and passes the
 * results in, the exact same pattern shared/game.js already uses for the
 * movement die. This is what keeps reduce() itself 100% deterministic.
 *
 * Two real gaps in the parsed content, both flagged `todo: true` on every
 * resolution they touch so it's visible in the game log, not just docs:
 *   - 85 inline_table tiles have a die size but zero table content anywhere
 *     in tile-inventory.js/roll-tables.js.
 *   - payday/cash_bonus tiles (PAY DAY, 100K, 10K, 401k) have no amount
 *     defined anywhere either.
 * Both resolve against SYNTHESIZED_TABLES — one generic banded placeholder
 * table per die size (not per tile), built the same way the real 48 tables
 * are shaped (banded ranges -> a cash effect), so swapping in real content
 * later is a matter of replacing one table, not touching this file's logic.
 */

import rollTablesData from './rollTables.js';

const tablesById = Object.fromEntries(rollTablesData.tables.map((t) => [t.source_doc_id, t]));

export const DIE_SIZES = { D2: 2, D6: 6, D8: 8, D10: 10, D20: 20, D100: 100 };

const PLACEHOLDER_PAYDAY_CASH = 2000;
const PLACEHOLDER_CASH_BONUS = { '100K': 100000, '10K': 10000 };

const SYNTHESIZED_TABLES = Object.fromEntries(
  Object.entries(DIE_SIZES).map(([die, max]) => [die, { die, entries: bandedPlaceholderEntries(max) }])
);

/** Same worst→jackpot banded shape as the 48 real placeholder tables, scaled
 * to the die's range. Cash-only so it composes safely no matter what the
 * tile actually wants to touch — a real replacement table can touch anything. */
function bandedPlaceholderEntries(max) {
  const bands = [
    { frac: 0.10, cash: -500, label: 'worst outcome' },
    { frac: 0.30, cash: -200, label: 'bad outcome' },
    { frac: 0.55, cash: -50, label: 'mediocre outcome' },
    { frac: 0.75, cash: 100, label: 'decent outcome' },
    { frac: 0.90, cash: 300, label: 'good outcome' },
    { frac: 0.99, cash: 600, label: 'great outcome' },
    { frac: 1.00, cash: 1200, label: 'jackpot' },
  ];
  const entries = [];
  let lo = 1;
  for (const band of bands) {
    if (lo > max) break; // die too small to hold this many distinct bands
    const hi = Math.min(max, Math.max(lo, Math.round(max * band.frac)));
    entries.push({
      range: lo === hi ? `${lo}` : `${lo}-${hi}`,
      result: `PLACEHOLDER: ${band.label}`,
      effect: { cash: band.cash },
    });
    lo = hi + 1;
  }
  return entries;
}

function lookupBand(entries, roll) {
  for (const entry of entries) {
    const [loStr, hiStr] = entry.range.split('-');
    const lo = Number(loStr);
    const hi = hiStr !== undefined ? Number(hiStr) : lo;
    if (roll >= lo && roll <= hi) return entry;
  }
  return entries[entries.length - 1];
}

function tableDieFor(tableId, fallbackDie) {
  return tablesById[tableId]?.die ?? fallbackDie;
}

/** What die(s) landing on `space` needs rolled, in the order resolveTileEffect
 * expects them back. The server calls this BEFORE constructing the action. */
export function rollsNeededFor(space) {
  switch (space.type) {
    case 'action_space':
    case 'dice_space':
      return [tableDieFor(space.externalTables[0], space.die)];
    case 'roll_table_ref':
      return space.externalTables.map((tableId) => tableDieFor(tableId, space.die));
    case 'inline_table':
    case 'stop':
      return [space.die];
    case 'cash_bonus':
    case 'event':
      return space.die ? [space.die] : [];
    case 'payday':
    case 'choice':
    default:
      return [];
  }
}

/** Pure: given `space` and the roll values rollsNeededFor(space) asked for,
 * return { statDelta, description, todo }. */
export function resolveTileEffect(space, rolls = []) {
  switch (space.type) {
    case 'payday':
      return {
        statDelta: { cash: PLACEHOLDER_PAYDAY_CASH },
        description: `${space.label}: +$${PLACEHOLDER_PAYDAY_CASH} (PLACEHOLDER amount)`,
        todo: true,
      };

    case 'cash_bonus': {
      if (space.label in PLACEHOLDER_CASH_BONUS) {
        const amount = PLACEHOLDER_CASH_BONUS[space.label];
        return {
          statDelta: { cash: amount },
          description: `${space.label}: +$${amount} (PLACEHOLDER amount)`,
          todo: true,
        };
      }
      return resolveSynthesized(space, rolls[0]);
    }

    case 'action_space':
    case 'dice_space':
    case 'roll_table_ref':
      return resolveExternalTables(space, rolls);

    case 'inline_table':
      return resolveSynthesized(space, rolls[0]);

    case 'event':
      return space.die ? resolveSynthesized(space, rolls[0]) : resolveFixedEvent(space);

    case 'choice':
      return { statDelta: {}, description: `${space.label}: choice options not yet defined (TODO)`, todo: true };

    case 'stop': {
      const roll = rolls[0] ?? 0;
      return { statDelta: { age: roll }, description: `${space.label}: age +${roll}`, todo: false };
    }

    default:
      return { statDelta: {}, description: `${space.label}: unhandled tile type "${space.type}" (TODO)`, todo: true };
  }
}

function resolveExternalTables(space, rolls) {
  const statDelta = {};
  const parts = [];
  space.externalTables.forEach((tableId, i) => {
    const table = tablesById[tableId];
    const roll = rolls[i];
    if (!table) {
      parts.push(`${space.label}: missing table ${tableId} (TODO)`);
      return;
    }
    const entry = lookupBand(table.entries, roll);
    mergeStatDelta(statDelta, entry.effect);
    parts.push(`${space.label} → ${table.display_name} (${roll}): ${entry.result}`);
  });
  return { statDelta, description: parts.join(' | '), todo: false };
}

function resolveSynthesized(space, roll) {
  const table = SYNTHESIZED_TABLES[space.die];
  if (!table) {
    return { statDelta: {}, description: `${space.label}: no die to roll against (TODO)`, todo: true };
  }
  const entry = lookupBand(table.entries, roll);
  return {
    statDelta: { ...entry.effect },
    description: `${space.label} (${roll}): ${entry.result} (PLACEHOLDER table)`,
    todo: true,
  };
}

function resolveFixedEvent(space) {
  const statDelta = {};
  for (const stat of space.statsTouched) statDelta[stat] = stat === 'cash' ? 100 : 1;
  const hasEffect = Object.keys(statDelta).length > 0;
  return {
    statDelta,
    description: `${space.label}${hasEffect ? ' (PLACEHOLDER amount)' : ''}`,
    todo: hasEffect,
  };
}

function mergeStatDelta(target, effect) {
  for (const [stat, delta] of Object.entries(effect ?? {})) {
    target[stat] = (target[stat] ?? 0) + delta;
  }
}
