const freezeEntries = (entries) => Object.freeze(entries.map((entry) => Object.freeze({
  ...entry,
  ...(entry.modes ? { modes: Object.freeze([...entry.modes]) } : {}),
  ...(entry.botModes ? { botModes: Object.freeze([...entry.botModes]) } : {})
})));

export const DIFFICULTIES = freezeEntries([
  { id: 0, label: 'Szeregowy' },
  { id: 1, label: 'Kapral' },
  { id: 2, label: 'Porucznik' },
  { id: 3, label: 'General' }
]);

export const MULTIPLAYER_MODES = freezeEntries([
  { id: 'DM', label: 'Deathmatch', team: false },
  { id: 'Tourney', label: 'Turniej', team: false },
  { id: 'Team DM', label: 'Druzynowy DM', team: true },
  { id: 'CTF', label: 'Capture the Flag', team: true },
  { id: 'Arena CTF', label: 'Arena CTF', team: true }
]);

const DM = Object.freeze(['DM', 'Team DM']);
const DM_TOURNEY = Object.freeze(['DM', 'Team DM', 'Tourney']);
const TEAM_CTF = Object.freeze(['Team DM', 'CTF', 'Arena CTF']);
const ALL_MODES = Object.freeze(MULTIPLAYER_MODES.map(({ id }) => id));
const DM_BOTS = Object.freeze(['DM']);
const NO_BOTS = Object.freeze([]);

// botReady retains the last staged capture set; the gateway publishes live availability.
export const MULTIPLAYER_MAPS = freezeEntries([
  {
    id: 'mp/q4web_arena',
    label: 'Q4Web Arena',
    modes: DM_TOURNEY,
    botModes: DM_BOTS,
    botReady: true,
    content: 'q4web-cc0'
  },
  { id: 'mp/q4dm11v1', label: 'The Fragging Yard 1v1', modes: DM_TOURNEY, botModes: DM_BOTS, botReady: true, content: 'retail' },
  { id: 'mp/q4dm1', label: 'The Fragging Yard', modes: DM, botModes: DM_BOTS, botReady: false, content: 'retail' },
  { id: 'mp/q4dm2', label: 'Sandstorm', modes: DM, botModes: DM_BOTS, botReady: false, content: 'retail' },
  { id: 'mp/q4dm3', label: 'The Lost Fleet', modes: DM_TOURNEY, botModes: DM_BOTS, botReady: true, content: 'retail' },
  { id: 'mp/q4dm4', label: 'Bloodwork', modes: DM, botModes: DM_BOTS, botReady: false, content: 'retail' },
  { id: 'mp/q4dm5', label: 'The Rose', modes: DM, botModes: DM_BOTS, botReady: false, content: 'retail' },
  { id: 'mp/q4dm6', label: 'No Doctors', modes: DM_TOURNEY, botModes: DM_BOTS, botReady: true, content: 'retail' },
  { id: 'mp/q4dm7', label: 'Over the Edge', modes: DM_TOURNEY, botModes: DM_BOTS, botReady: true, content: 'retail' },
  { id: 'mp/q4dm8', label: 'The Longest Day', modes: DM_TOURNEY, botModes: DM_BOTS, botReady: true, content: 'retail' },
  { id: 'mp/q4ctf1', label: 'Heartless', modes: TEAM_CTF, botModes: NO_BOTS, botReady: false, content: 'retail' },
  { id: 'mp/q4ctf2', label: 'Death Before Dishonor', modes: TEAM_CTF, botModes: NO_BOTS, botReady: false, content: 'retail' },
  { id: 'mp/q4ctf3', label: 'Speed Trap', modes: TEAM_CTF, botModes: NO_BOTS, botReady: false, content: 'retail' },
  { id: 'mp/q4ctf4', label: 'Relativity', modes: TEAM_CTF, botModes: NO_BOTS, botReady: false, content: 'retail' },
  { id: 'mp/q4ctf5', label: 'Xaero Gravity', modes: ALL_MODES, botModes: DM_BOTS, botReady: true, content: 'retail' },
  { id: 'mp/q4xctf6', label: 'Cavernous Cryonics', modes: TEAM_CTF, botModes: NO_BOTS, botReady: false, content: 'retail' },
  { id: 'mp/q4xdm10', label: 'Central Industrial', modes: DM, botModes: DM_BOTS, botReady: true, content: 'retail' },
  { id: 'mp/q4xdm11', label: 'Warforged', modes: DM, botModes: DM_BOTS, botReady: true, content: 'retail' },
  { id: 'mp/q4xdm13', label: 'Stroyent Red', modes: DM, botModes: DM_BOTS, botReady: true, content: 'retail' },
  { id: 'mp/q4xdm14', label: 'Retrophobopolis', modes: DM, botModes: DM_BOTS, botReady: true, content: 'retail' },
  { id: 'mp/q4xdm15', label: 'Firewall', modes: DM, botModes: DM_BOTS, botReady: true, content: 'retail' },
  { id: 'mp/q4xtourney1', label: 'Stroggenomenon', modes: DM_TOURNEY, botModes: DM_BOTS, botReady: false, content: 'retail' },
  { id: 'mp/q4xtourney2', label: 'Verticon', modes: DM_TOURNEY, botModes: DM_BOTS, botReady: true, content: 'retail' }
]);

export const PLAYER_TEAMS = freezeEntries([
  { id: 'auto', label: 'Automatycznie' },
  { id: 'Marine', label: 'Marine' },
  { id: 'Strogg', label: 'Strogg' }
]);

export const SETTINGS_METADATA = Object.freeze({
  sensitivity: Object.freeze({ min: 0.1, max: 20, step: 0.1 }),
  fov: Object.freeze({ min: 70, max: 120, step: 1 }),
  masterVolume: Object.freeze({ min: 0, max: 1, step: 0.05 }),
  musicVolume: Object.freeze({ min: 0, max: 1, step: 0.05 })
});

export const DEFAULT_SETTINGS = Object.freeze({
  sensitivity: 5,
  invertY: false,
  fov: 90,
  masterVolume: 1,
  musicVolume: 0.5
});

export const DEFAULT_PLAYER_PROFILE = Object.freeze({
  nick: 'Gracz',
  team: 'auto',
  model: 'auto'
});

export const DEFAULT_MATCH = Object.freeze({
  map: 'mp/q4web_arena',
  mode: 'DM',
  botCount: 8,
  fragLimit: 10,
  timeLimit: 0
});

export const MODEL_METADATA_CONTRACT = Object.freeze({
  schema: 1,
  defaultModel: 'auto',
  source: 'staged-operator-metadata',
  fields: Object.freeze({
    id: Object.freeze({ required: true, pattern: '^model_player_[a-z0-9_]+$' }),
    label: Object.freeze({ required: true, type: 'string' }),
    team: Object.freeze({ required: false, values: Object.freeze(['Marine', 'Strogg']) }),
    previewUrl: Object.freeze({ required: false, type: 'same-origin-content-hashed-url' })
  })
});

export class MenuConfigError extends Error {
  constructor(code, message, details = {}) {
    super(message);
    this.name = 'MenuConfigError';
    this.code = code;
    this.details = Object.freeze({ ...details });
  }
}

const mapById = new Map(MULTIPLAYER_MAPS.map((map) => [map.id, map]));
const modeById = new Map(MULTIPLAYER_MODES.map((mode) => [mode.id, mode]));
const difficultyIds = new Set(DIFFICULTIES.map(({ id }) => id));
const modelIdPattern = /^model_player_[a-z0-9_]+$/;

export function getMultiplayerMap(id) {
  return mapById.get(id) || null;
}

export function getMultiplayerMode(id) {
  return modeById.get(id) || null;
}

function finiteNumber(value, fallback, { min, max, step }) {
  const number = typeof value === 'number' ? value : Number(value);
  if (!Number.isFinite(number)) return fallback;
  const clamped = Math.min(max, Math.max(min, number));
  const precision = Math.max(0, (String(step).split('.')[1] || '').length);
  return Number(clamped.toFixed(precision));
}

export function normalizeSettings(input = {}) {
  const source = input && typeof input === 'object' && !Array.isArray(input) ? input : {};
  return Object.freeze({
    sensitivity: finiteNumber(
      source.sensitivity,
      DEFAULT_SETTINGS.sensitivity,
      SETTINGS_METADATA.sensitivity
    ),
    invertY: source.invertY === true || source.invertY === 'true' || source.invertY === 1,
    fov: finiteNumber(source.fov, DEFAULT_SETTINGS.fov, SETTINGS_METADATA.fov),
    masterVolume: finiteNumber(
      source.masterVolume,
      DEFAULT_SETTINGS.masterVolume,
      SETTINGS_METADATA.masterVolume
    ),
    musicVolume: finiteNumber(
      source.musicVolume,
      DEFAULT_SETTINGS.musicVolume,
      SETTINGS_METADATA.musicVolume
    )
  });
}

function sanitizeNickValue(value) {
  return String(value ?? '')
    .normalize('NFKD')
    .replace(/[\u0300-\u036f]/g, '')
    .replace(/[\u0000-\u001f\u007f-\u009f]/g, ' ')
    .replace(/[^A-Za-z0-9 _.-]/g, ' ')
    .replace(/\s+/g, ' ')
    .trim()
    .replace(/^[._-]+/, '')
    .trim()
    .slice(0, 24)
    .trim();
}

export function sanitizeEngineNick(value, fallback = DEFAULT_PLAYER_PROFILE.nick) {
  const sanitized = sanitizeNickValue(value);
  if (sanitized) return sanitized;
  return sanitizeNickValue(fallback) || DEFAULT_PLAYER_PROFILE.nick;
}

export function normalizeTeam(value) {
  const key = String(value ?? '').trim().toLowerCase();
  if (key === 'marine') return 'Marine';
  if (key === 'strogg') return 'Strogg';
  return 'auto';
}

function allowedModelId(entry) {
  const id = typeof entry === 'string' ? entry : entry?.id;
  return typeof id === 'string' && modelIdPattern.test(id) ? id : null;
}

export function normalizePlayerModel(value, allowedModels = []) {
  if (value === undefined || value === null || value === '' || value === 'auto') return 'auto';
  const candidate = typeof value === 'string' && modelIdPattern.test(value) ? value : null;
  if (!candidate || !Array.isArray(allowedModels)) return 'auto';
  return allowedModels.some((entry) => allowedModelId(entry) === candidate) ? candidate : 'auto';
}

export function normalizePlayerProfile(input = {}, allowedModels = []) {
  const source = input && typeof input === 'object' && !Array.isArray(input) ? input : {};
  return Object.freeze({
    nick: sanitizeEngineNick(source.nick),
    team: normalizeTeam(source.team),
    model: normalizePlayerModel(source.model, allowedModels)
  });
}

export function normalizeDifficulty(value) {
  const difficulty = typeof value === 'number' ? value : Number(value);
  return Number.isInteger(difficulty) && difficultyIds.has(difficulty) ? difficulty : 1;
}

function integerInRange(value, fallback, field, min, max) {
  const candidate = value === undefined ? fallback : value;
  if (!Number.isInteger(candidate) || candidate < min || candidate > max) {
    throw new MenuConfigError(
      'MATCH_INTEGER_RANGE',
      `${field} must be an integer between ${min} and ${max}`,
      { field, min, max }
    );
  }
  return candidate;
}

export function normalizeMatchRequest(raw = {}) {
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
    throw new MenuConfigError('MATCH_INVALID', 'Match configuration must be an object');
  }

  const allowedFields = new Set(['map', 'mode', 'botCount', 'fragLimit', 'timeLimit']);
  const unknownField = Object.keys(raw).find((field) => !allowedFields.has(field));
  if (unknownField) {
    throw new MenuConfigError(
      'MATCH_FIELD_UNKNOWN',
      `Unknown match field: ${unknownField}`,
      { field: unknownField }
    );
  }

  const mapId = raw.map === undefined ? DEFAULT_MATCH.map : raw.map;
  const map = getMultiplayerMap(mapId);
  if (!map) {
    throw new MenuConfigError('MATCH_MAP_UNKNOWN', 'Unknown multiplayer map', { map: mapId });
  }

  const modeId = raw.mode === undefined ? DEFAULT_MATCH.mode : raw.mode;
  if (!getMultiplayerMode(modeId)) {
    throw new MenuConfigError('MATCH_MODE_UNKNOWN', 'Unknown multiplayer mode', { mode: modeId });
  }
  if (!map.modes.includes(modeId)) {
    throw new MenuConfigError(
      'MATCH_MODE_UNSUPPORTED',
      'The selected map does not support this multiplayer mode',
      { map: map.id, mode: modeId }
    );
  }

  const requestedBots = integerInRange(raw.botCount, DEFAULT_MATCH.botCount, 'botCount', 0, 8);
  const scoreLimitMinimum = modeId === 'CTF' || modeId === 'Arena CTF' ? 1 : 0;
  const fragLimit = integerInRange(
    raw.fragLimit,
    DEFAULT_MATCH.fragLimit,
    'fragLimit',
    scoreLimitMinimum,
    100
  );
  const timeLimit = integerInRange(raw.timeLimit, DEFAULT_MATCH.timeLimit, 'timeLimit', 0, 60);

  return Object.freeze({
    map: map.id,
    mode: modeId,
    botCount: map.botModes.includes(modeId) ? requestedBots : 0,
    fragLimit,
    timeLimit
  });
}

export function buildDedicatedMatchCommands(match) {
  const normalized = normalizeMatchRequest(match);
  return Object.freeze([
    `set bot_minplayers ${normalized.botCount}`,
    `set si_fragLimit ${normalized.fragLimit}`,
    `set si_captureLimit ${normalized.fragLimit}`,
    `set si_timeLimit ${normalized.timeLimit}`,
    `set si_gameType "${normalized.mode}"`,
    `set si_map ${normalized.map}`,
    `spawnServer ${normalized.map}`
  ]);
}

export const DEFAULT_LAUNCHER = Object.freeze({
  difficulty: 1,
  match: DEFAULT_MATCH,
  profile: DEFAULT_PLAYER_PROFILE,
  settings: DEFAULT_SETTINGS
});

export function normalizeLauncherState(input = {}, allowedModels = []) {
  const source = input && typeof input === 'object' && !Array.isArray(input) ? input : {};
  return Object.freeze({
    difficulty: normalizeDifficulty(source.difficulty),
    match: normalizeMatchRequest(source.match),
    profile: normalizePlayerProfile(source.profile, allowedModels),
    settings: normalizeSettings(source.settings)
  });
}
