// mission-map.jsx
// Mission Map metadata, category routing, and presentational components.
// Extracted from app.jsx on 2026-05-14. Exposes its symbols as Babel-promoted
// globals (function declarations) so app.jsx can render
// <MissionMapHeader />, <MissionMapCategoryJumps />, and
// <MissionMapActivityGroups /> without additional imports.
//
// Owned constants:
//   MISSION_MAP_ACTIVITY_META, MISSION_MAP_ACTIVITY_DETAILS,
//   MISSION_MAP_CATEGORIES, MISSION_MAP_ALL_CATEGORY_ID,
//   MISSION_MAP_CATEGORY_STORAGE_KEY
//
// Helpers used by app.jsx: missionMapCategoryIds, groupMissionMapActivities,
// createMissionMapView, bindMissionMapActivity, findMissionMapConfigIssues,
// nextMissionMapCategoryId, missionMapDomId.

const missionMapFoundation = window.SpaceExplorerFoundation || {};
const missionMapSafeArray = missionMapFoundation.safeArray || ((value) =>
  Array.isArray(value) ? value : []);
const missionMapNormalizeNarrationClip =
  missionMapFoundation.normalizeNarrationClip || ((file) =>
    typeof file === "string" ? file.trim() : "");

const MISSION_MAP_ACTIVITY_META = [
  {
    id: "maze",
    icon: "🛰",
    label: "Play Maze",
    launchType: "run",
    target: "maze",
    narration: "game_maze_start.mp3",
    sound: null,
  },
  {
    id: "moon-rover",
    icon: "🌕",
    label: "Moon Rover",
    launchType: "run",
    target: "moonRover",
    narration: "game_rover_start.mp3",
  },
  {
    id: "mars-rover",
    icon: "🔴",
    label: "Mars Rover",
    launchType: "run",
    target: "marsRover",
    narration: "game_mars_rover_start.mp3",
    sound: "whoosh",
  },
  {
    id: "mars-city",
    icon: "🏙",
    label: "Mars City",
    launchType: "panel",
    target: "marsCity",
    narration: "game_mars_city_start.mp3",
    sound: "chime",
  },
  {
    id: "lego-rover-garage",
    icon: "🚙",
    label: "Rover Garage",
    launchType: "panel",
    target: "legoRoverGarage",
    sound: "whoosh",
  },
  {
    id: "pilot-school",
    icon: "🕹",
    label: "Pilot School",
    launchType: "run",
    target: "pilot",
    narration: "game_pilot_start.mp3",
  },
  {
    id: "mission-control",
    icon: "✅",
    label: "Mission Control",
    launchType: "panel",
    target: "missionControl",
  },
  {
    id: "treasure-hunt",
    icon: "⭐",
    label: "Treasure Hunt",
    launchType: "custom",
    target: "treasureHunt",
    narration: "game_treasure_start.mp3",
    sound: null,
  },
  {
    id: "planet-sort",
    icon: "🪐",
    label: "Planet Sort",
    launchType: "panel",
    target: "planetSort",
  },
  {
    id: "build-rocket",
    icon: "🚀",
    label: "Build Rocket",
    launchType: "custom",
    target: "buildRocket",
  },
  {
    id: "engine-match",
    icon: "🔥",
    label: "Engine Match",
    launchType: "panel",
    target: "engineMatch",
  },
  {
    id: "spaceship-wire-fix",
    icon: "🔌",
    label: "Engine Wires",
    launchType: "panel",
    target: "wireFix",
  },
  {
    id: "control-board-memory",
    icon: "🎛",
    label: "Control Pattern",
    launchType: "panel",
    target: "controlBoardMemory",
  },
  {
    id: "rocket-lab",
    icon: "↕",
    label: "Rocket Push Lab",
    launchType: "panel",
    target: "rocketCause",
  },
  {
    id: "ufo-builder",
    icon: "🛸",
    label: "UFO Builder",
    launchType: "panel",
    target: "ufoBuilder",
  },
  {
    id: "mashup-maker",
    icon: "＋",
    label: "Mashup Maker",
    launchType: "panel",
    target: "mashupMaker",
  },
  {
    id: "connection-quest",
    icon: "↔",
    label: "Connection Quest",
    launchType: "panel",
    target: "connectionQuest",
  },
  {
    id: "asteroid-dodge",
    icon: "☄",
    label: "Asteroid Dodge",
    launchType: "panel",
    target: "asteroidDodge",
  },
  {
    id: "gravity-jump",
    icon: "🦘",
    label: "Gravity Jump Lab",
    launchType: "panel",
    target: "gravityJump",
  },
  {
    id: "gravity-goldfish",
    icon: "🐠",
    label: "Gravity Goldfish",
    launchType: "panel",
    target: "gravityGoldfish",
  },
  {
    id: "comet-curling",
    icon: "☄",
    label: "Comet Curling",
    launchType: "panel",
    target: "cometCurling",
  },
  {
    id: "solar-sail-skimmer",
    icon: "☀",
    label: "Solar Sail Skimmer",
    launchType: "panel",
    target: "solarSail",
  },
  {
    id: "physics-play-lab",
    icon: "🧪",
    label: "Tiny Physics Lab",
    launchType: "panel",
    target: "physicsPlayLab",
    narration: "physics-play-lab/push-force-instruction.mp3",
  },
  {
    id: "galileo-eyepiece",
    icon: "🔭",
    label: "Galileo's Eyepiece",
    launchType: "custom",
    target: "galileoEyepiece",
  },
  {
    id: "venus-rain",
    icon: "☁",
    label: "Venus Clouds",
    launchType: "custom",
    target: "venusRain",
  },
  {
    id: "alien-language",
    icon: "👽",
    label: "Alien Dance",
    launchType: "panel",
    target: "alienLanguage",
  },
  {
    id: "baby-rockets",
    icon: "🚼",
    label: "Baby Rockets",
    launchType: "panel",
    target: "babyRockets",
  },
  {
    id: "moon-aliens",
    icon: "👾",
    label: "Moon Aliens",
    launchType: "panel",
    target: "moonAliens",
  },
  {
    id: "moon-garden",
    icon: "🌱",
    label: "Moon Garden",
    launchType: "panel",
    target: "moonGarden",
  },
  {
    id: "meteor-shield",
    icon: "🛡️",
    label: "Meteor Shield",
    launchType: "panel",
    target: "meteorShield",
  },
  {
    id: "shooting-star-ride",
    icon: "🌠",
    label: "Shooting Star",
    launchType: "panel",
    target: "shootingStar",
  },
  {
    id: "planet-pet-feeder",
    icon: "🍴",
    label: "Planet Pet-Feeder",
    launchType: "panel",
    target: "petFeeder",
  },
  {
    id: "pack-mars-suitcase",
    icon: "🎒",
    label: "Mars Suitcase",
    launchType: "panel",
    target: "marsSuitcase",
  },
  {
    id: "space-memory",
    icon: "▦",
    label: "Space Memory",
    launchType: "panel",
    target: "spaceMemory",
    narration: "memory_intro.mp3",
  },
  {
    id: "trappist-system",
    icon: "✦",
    label: "TRAPPIST-1",
    launchType: "custom",
    target: "trappistSystem",
  },
  {
    id: "kepler-system",
    icon: "✧",
    label: "Kepler-90",
    launchType: "custom",
    target: "keplerSystem",
  },
  {
    id: "story-books",
    icon: "📚",
    label: "Story Books",
    launchType: "panel",
    target: "storyShelf",
  },
  {
    id: "space-reading",
    icon: "Aa",
    label: "Space Reading",
    launchType: "panel",
    target: "spaceReading",
  },
  {
    id: "black-hole",
    icon: "🕳️",
    label: "Black Hole",
    launchType: "run",
    target: "blackHole",
    narration: "game_blackhole_start.mp3",
    sound: null,
  },
  {
    id: "helmet-lab",
    icon: "🪖",
    label: "Helmet Lab",
    launchType: "panel",
    target: "helmetPage",
    narration: "game_helmet_start.mp3",
  },
  {
    id: "mars-story",
    icon: "🎬",
    label: "Mars Story",
    launchType: "custom",
    target: "marsStory",
  },
  {
    id: "space-flight",
    icon: "✨",
    label: "Space Flight",
    launchType: "custom",
    target: "spaceFlight",
  },
];

const MISSION_MAP_ACTIVITY_DETAILS = {
  maze: {
    kind: "navigation",
    summary: "Steer through a planet path.",
  },
  "moon-rover": {
    kind: "driving",
    summary: "Collect moon samples.",
  },
  "mars-rover": {
    kind: "driving",
    summary: "Cross mountains and dust storms.",
  },
  "mars-city": {
    kind: "builder",
    summary: "Balance resident needs as mayor.",
  },
  "lego-rover-garage": {
    kind: "driving",
    summary: "Drive the Lego rover to adventure stops.",
  },
  "pilot-school": {
    kind: "controls",
    summary: "Practice ship steering.",
  },
  "mission-control": {
    kind: "operations",
    summary: "Check launch tasks.",
  },
  "treasure-hunt": {
    kind: "search",
    summary: "Find hidden space treasures.",
  },
  "planet-sort": {
    kind: "sorting",
    summary: "Match planets by clues.",
  },
  "build-rocket": {
    kind: "builder",
    summary: "Snap a rocket together.",
  },
  "engine-match": {
    kind: "matching",
    summary: "Pair rockets and engines.",
  },
  "spaceship-wire-fix": {
    kind: "systems",
    summary: "Match colorful wires to start the ship.",
  },
  "control-board-memory": {
    kind: "patterns",
    summary: "Repeat glowing buttons and sound clues.",
  },
  "rocket-lab": {
    kind: "cause-effect",
    summary: "Change fuel, fins, and cargo to see force and motion.",
  },
  "ufo-builder": {
    kind: "builder",
    summary: "Mix parts into a UFO.",
  },
  "mashup-maker": {
    kind: "creativity",
    summary: "Combine silly space ideas.",
  },
  "connection-quest": {
    kind: "problem-solving",
    summary: "Connect helpers to problems.",
  },
  "asteroid-dodge": {
    kind: "reflex",
    summary: "Move past falling rocks.",
  },
  "gravity-jump": {
    kind: "physics",
    summary: "Compare weak and strong gravity by jumping on worlds.",
  },
  "gravity-goldfish": {
    kind: "physics",
    summary: "Pulse soft, moon, and big gravity to curve a fish path.",
  },
  "comet-curling": {
    kind: "physics",
    summary: "Change push and surface drag to slide a comet.",
  },
  "solar-sail-skimmer": {
    kind: "physics",
    summary: "Catch light on a shiny sail and glide to sparkle gates.",
  },
  "physics-play-lab": {
    kind: "physics",
    summary: "Tap through push, gravity, glide, orbit, and bounce.",
  },
  "galileo-eyepiece": {
    kind: "history",
    summary: "Slide through what humans knew.",
  },
  "venus-rain": {
    kind: "weather",
    summary: "See Venus's acid cloud rain.",
  },
  "alien-language": {
    kind: "patterns",
    summary: "Copy a friendly alien's color dance.",
  },
  "baby-rockets": {
    kind: "care",
    summary: "Help little rockets launch.",
  },
  "moon-aliens": {
    kind: "rescue",
    summary: "Guide visitors on the Moon.",
  },
  "moon-garden": {
    kind: "rescue",
    summary: "Grow a garden in space.",
  },
  "meteor-shield": {
    kind: "defense",
    summary: "Protect the space base.",
  },
  "shooting-star-ride": {
    kind: "reflex",
    summary: "Ride a star trail and collect sparkle snacks.",
  },
  "planet-pet-feeder": {
    kind: "care",
    summary: "Feed planets the right treats.",
  },
  "pack-mars-suitcase": {
    kind: "sorting",
    summary: "Pack useful Mars trip items.",
  },
  "space-memory": {
    kind: "matching",
    summary: "Flip cards and match space pairs.",
  },
  "trappist-system": {
    kind: "systems",
    summary: "Visit seven Earth-sized worlds.",
  },
  "kepler-system": {
    kind: "systems",
    summary: "Visit a system with eight planets.",
  },
  "story-books": {
    kind: "reading",
    summary: "Open gentle space stories.",
  },
  "space-reading": {
    kind: "reading",
    summary: "Read simple space words.",
  },
  "black-hole": {
    kind: "physics",
    summary: "Explore gravity and light.",
  },
  "helmet-lab": {
    kind: "dress-up",
    summary: "Decorate an astronaut helmet.",
  },
  "mars-story": {
    kind: "cinematic",
    summary: "Watch a Mars adventure.",
  },
  "space-flight": {
    kind: "cinematic",
    summary: "Watch a space flight.",
  },
};

const MISSION_MAP_CATEGORIES = [
  {
    id: "flight",
    label: "1 Study",
    summary: "Study a world from your ship",
    learningCue: "Watch where you move. Tell your grown-up what changed.",
    kinds: ["navigation", "driving", "controls", "reflex", "cinematic"],
  },
  {
    id: "science",
    label: "2 Play",
    summary: "Play a mission and try one idea",
    learningCue: "Try one idea. Say what helped the mission work.",
    kinds: [
      "operations",
      "sorting",
      "matching",
      "cause-effect",
      "problem-solving",
      "physics",
      "history",
      "weather",
      "systems",
      "patterns",
      "defense",
    ],
  },
  {
    id: "create",
    label: "3 Make",
    summary: "Make, read, or care for one memory",
    learningCue: "Make one thing. Point to the part you chose.",
    kinds: [
      "search",
      "builder",
      "creativity",
      "care",
      "rescue",
      "reading",
      "dress-up",
    ],
  },
];
const MISSION_MAP_ALL_CATEGORY_ID = "all";
const MISSION_MAP_DEFAULT_CATEGORY_ID = "flight";
const MISSION_MAP_CATEGORY_STORAGE_KEY = "planets_mission_map_category";
const MISSION_MAP_LAUNCH_TYPES = ["run", "panel", "custom"];
const MISSION_MAP_FEATURED_ACTIVITY_IDS = [
  "lego-rover-garage",
  "shooting-star-ride",
  "baby-rockets",
  "engine-match",
  "spaceship-wire-fix",
  "mashup-maker",
];

function missionMapKindToken(kind) {
  return String(kind || "")
    .trim()
    .toLowerCase()
    .replace(/[^a-z0-9_-]+/g, "-")
    .replace(/^-+|-+$/g, "");
}

function createMissionMapKindCategoryMap() {
  return MISSION_MAP_CATEGORIES.reduce((map, category) => {
    missionMapSafeArray(category.kinds).forEach((kind) => {
      const kindToken = missionMapKindToken(kind);
      if (kindToken && !map[kindToken]) map[kindToken] = category;
    });
    return map;
  }, {});
}

const MISSION_MAP_KIND_CATEGORY = createMissionMapKindCategoryMap();

function missionMapCategoryIds(includeAll = false) {
  const ids = MISSION_MAP_CATEGORIES.map((category) => category.id);
  return includeAll ? [MISSION_MAP_ALL_CATEGORY_ID, ...ids] : ids;
}

function normalizeMissionMapCategoryId(categoryId) {
  return missionMapCategoryIds(true).includes(categoryId)
    ? categoryId
    : MISSION_MAP_DEFAULT_CATEGORY_ID;
}

function preferredMissionMapCategoryId(options, requestedId) {
  const normalizedId = normalizeMissionMapCategoryId(requestedId);
  if (options.some((category) => category.id === normalizedId)) return normalizedId;
  if (options.some((category) => category.id === MISSION_MAP_DEFAULT_CATEGORY_ID)) {
    return MISSION_MAP_DEFAULT_CATEGORY_ID;
  }
  return options[0] ? options[0].id : MISSION_MAP_ALL_CATEGORY_ID;
}

function missionMapCategoryFor(activity) {
  const kind = missionMapKindToken(activity && activity.kind);
  return (
    MISSION_MAP_KIND_CATEGORY[kind] ||
    MISSION_MAP_CATEGORIES[MISSION_MAP_CATEGORIES.length - 1]
  );
}

function groupMissionMapActivities(activities) {
  return MISSION_MAP_CATEGORIES.map((category) => ({
    ...category,
    activities: activities.filter(
      (activity) => activity.categoryId === category.id,
    ),
  })).filter((category) => category.activities.length);
}

function missionMapCategoryOptions(groups) {
  const totalActivities = groups.reduce(
    (total, category) => total + category.activities.length,
    0,
  );
  return [
    {
      id: MISSION_MAP_ALL_CATEGORY_ID,
      label: "All games",
      summary: "Every game on the Mission Map",
      learningCue: "Pick one game. Tell your grown-up one thing you noticed.",
      count: totalActivities,
    },
    ...groups.map((category) => ({
      id: category.id,
      label: missionMapCopy(category.label, "Mission row"),
      summary: missionMapCopy(category.summary, "Space activities"),
      learningCue: missionMapCopy(
        category.learningCue,
        "Try one mission. Tell your grown-up one thing you noticed.",
      ),
      count: category.activities.length,
    })),
  ];
}

function createMissionMapFeaturedGroup(groups) {
  const activitiesById = groups
    .flatMap((group) => group.activities)
    .reduce((map, activity) => {
      map[activity.id] = activity;
      return map;
    }, {});
  const activities = MISSION_MAP_FEATURED_ACTIVITY_IDS.map(
    (activityId) => activitiesById[activityId],
  ).filter(Boolean);
  return activities.length
    ? {
        id: "featured",
        label: "Recently added",
        summary: "Rocket, maker, and engine games",
        activities,
      }
    : null;
}

function missionMapGameCountText(count) {
  const safeCount = Number.isFinite(count) ? count : 0;
  return `${safeCount} ${safeCount === 1 ? "game" : "games"}`;
}

function missionMapText(value, fallback = "") {
  const normalized = String(value || "")
    .replace(/\s+/g, " ")
    .trim();
  return normalized || fallback;
}

function missionMapCopy(value, fallback) {
  return missionMapText(value, fallback);
}

function missionMapStatusText(category) {
  const summary = missionMapCopy(category && category.summary, "Space activities");
  return category
    ? `${missionMapGameCountText(category.count)} · ${summary}`
    : "No Mission Map activities ready yet.";
}

function missionMapLearningCue(category) {
  return missionMapCopy(
    category && category.learningCue,
    "Try one mission. Tell your grown-up one thing you noticed.",
  );
}

function missionMapEmptyText(category) {
  const label = missionMapCopy(category && category.label, "this row");
  return category && category.id !== MISSION_MAP_ALL_CATEGORY_ID
    ? `No ${label} games are ready yet. Try another row.`
    : "No games are ready yet. Try More tools or pick a planet.";
}

function missionMapCategoryButtonLabel(category) {
  if (!category) return "Mission Map category";
  const label = missionMapCopy(category.label, "Mission Map category");
  const summary = missionMapCopy(category.summary, "Space activities");
  return `${label}: ${missionMapGameCountText(category.count)}. ${summary}`;
}

function missionMapDomId(value, fallback = "unknown") {
  const normalized = String(value || fallback)
    .trim()
    .toLowerCase()
    .replace(/[^a-z0-9_-]+/g, "-")
    .replace(/^-+|-+$/g, "");
  return normalized || fallback;
}

function nextMissionMapCategoryId(options, selectedId, key) {
  if (!options.length) return null;
  const currentIndex = Math.max(
    0,
    options.findIndex((category) => category.id === selectedId),
  );
  if (key === "Home") return options[0].id;
  if (key === "End") return options[options.length - 1].id;
  if (key === "ArrowRight" || key === "ArrowDown") {
    return options[(currentIndex + 1) % options.length].id;
  }
  if (key === "ArrowLeft" || key === "ArrowUp") {
    return options[(currentIndex - 1 + options.length) % options.length].id;
  }
  return null;
}

function createMissionMapView(groups, selectedCategoryId) {
  const categoryOptions = missionMapCategoryOptions(groups);
  const normalizedCategoryId = preferredMissionMapCategoryId(
    categoryOptions,
    selectedCategoryId,
  );
  const selectedCategory =
    categoryOptions.find((category) => category.id === normalizedCategoryId) ||
    categoryOptions[0];
  const visibleGroups =
    !selectedCategory || selectedCategory.id === MISSION_MAP_ALL_CATEGORY_ID
      ? groups
      : groups.filter((category) => category.id === selectedCategory.id);
  const featuredGroup =
    selectedCategory && selectedCategory.id === MISSION_MAP_ALL_CATEGORY_ID
      ? createMissionMapFeaturedGroup(groups)
      : null;

  return {
    categoryOptions,
    selectedCategory,
    selectedCategoryId: selectedCategory
      ? selectedCategory.id
      : MISSION_MAP_ALL_CATEGORY_ID,
    statusText: missionMapStatusText(selectedCategory),
    learningCue: missionMapLearningCue(selectedCategory),
    emptyText: missionMapEmptyText(selectedCategory),
    visibleGroups: featuredGroup
      ? [featuredGroup, ...visibleGroups]
      : visibleGroups,
  };
}

function missionMapTileLabel(activity) {
  const label = missionMapCopy(activity && activity.label, "");
  const summary = missionMapCopy(activity && activity.summary, "");
  return label
    ? `Launch ${label}${summary ? `. ${summary}` : ""}`
    : "Launch Mission Map activity";
}

function withMissionMapDetails(activity) {
  if (!activity || !activity.id) return activity;
  return {
    ...MISSION_MAP_ACTIVITY_DETAILS[activity.id],
    ...activity,
  };
}

function normalizeMissionMapNarrationOption(narration) {
  if (typeof narration === "function") {
    return () => missionMapNormalizeNarrationClip(narration());
  }
  return missionMapNormalizeNarrationClip(narration) || null;
}

function normalizeMissionMapSoundOption(sound) {
  if (sound === undefined || sound === null) return sound;
  const safeSound = String(sound).trim();
  return safeSound || null;
}

function normalizeMissionMapLaunchOptions(options) {
  const source = options && typeof options === "object" ? options : {};
  return {
    ...source,
    narration: normalizeMissionMapNarrationOption(source.narration),
    sound: normalizeMissionMapSoundOption(source.sound),
  };
}

function resolveMissionMapLaunchOptions(activity, target, optionResolvers) {
  const baseOptions = {
    narration: activity.narration,
    sound: activity.sound,
  };
  const resolver = optionResolvers[target];
  if (typeof resolver !== "function") {
    return normalizeMissionMapLaunchOptions(baseOptions);
  }
  try {
    const resolvedOptions = resolver(activity);
    return normalizeMissionMapLaunchOptions({
      ...baseOptions,
      ...(resolvedOptions && typeof resolvedOptions === "object"
        ? resolvedOptions
        : {}),
    });
  } catch (error) {
    return normalizeMissionMapLaunchOptions({
      ...baseOptions,
      resolverError: error && error.message ? error.message : "resolver failed",
    });
  }
}

function bindMissionMapActivity(activity, launchers) {
  activity = withMissionMapDetails(activity);
  if (!activity) {
    return {
      id: "missing-activity",
      icon: "!",
      label: "Unavailable",
      summary: "Ask a grown-up to reconnect this game.",
      ariaLabel: "Mission Map activity unavailable",
      kind: "activity",
      categoryId: MISSION_MAP_DEFAULT_CATEGORY_ID,
      unavailable: true,
      unavailableReason: "Activity data is missing.",
    };
  }
  const target = activity.target || activity.id;
  const runTargets = launchers.runTargets || {};
  const panelTargets = launchers.panelTargets || {};
  const customTargets = launchers.customTargets || {};
  const optionResolvers = launchers.optionResolvers || {};
  const options = resolveMissionMapLaunchOptions(
    activity,
    target,
    optionResolvers,
  );
  const launchOptions = { ...options, skipSceneTransition: true };
  let launch = null;

  if (options.resolverError) {
    launch = null;
  } else if (activity.launchType === "run") {
    const runTarget = runTargets[target];
    if (runTarget) {
      launch = () =>
        launchers.openRunActivity(
          runTarget.setRun,
          runTarget.setOpen,
          launchOptions,
        );
    }
  } else if (activity.launchType === "panel") {
    const panelTarget = panelTargets[target];
    if (panelTarget) {
      launch = () => launchers.openPanelActivity(panelTarget, launchOptions);
    }
  } else if (activity.launchType === "custom") {
    const customTarget = customTargets[target];
    if (customTarget) {
      launch = () =>
        launchers.openActivity({
          ...launchOptions,
          activate: customTarget,
        });
    }
  }
  const unavailableReason = options.resolverError
    ? "Activity options could not be loaded."
    : typeof launch === "function"
      ? ""
      : "Activity launcher is not connected.";
  const ariaLabel =
    typeof launch === "function"
      ? missionMapTileLabel(activity)
      : `${missionMapCopy(activity.label, "Mission Map activity")} unavailable. ${unavailableReason}`;

  return {
    ...activity,
    launch,
    ariaLabel,
    kind: missionMapKindToken(activity.kind) || "activity",
    label: missionMapCopy(activity.label, "Mission Map activity"),
    summary: missionMapCopy(activity.summary, "Open a space activity."),
    categoryId: missionMapCategoryFor(activity).id,
    unavailable: typeof launch !== "function",
    unavailableReason,
  };
}

function findMissionMapConfigIssues(metaActivities, boundActivities) {
  const issues = [];
  const activities = missionMapSafeArray(metaActivities);
  const bound = missionMapSafeArray(boundActivities);
  const ids = new Set();
  const targetsByLaunchType = new Set();
  const allowedLaunchTypes = new Set(MISSION_MAP_LAUNCH_TYPES);
  const categoryIds = new Set();
  const coveredKinds = new Set();

  MISSION_MAP_CATEGORIES.forEach((category, index) => {
    if (!category.id) {
      issues.push(`Category ${index + 1} is missing an id.`);
      return;
    }
    if (category.id === MISSION_MAP_ALL_CATEGORY_ID) {
      issues.push(`Category ${category.id} is reserved for the all filter.`);
    }
    if (categoryIds.has(category.id)) {
      issues.push(`Category ${category.id} is duplicated.`);
    }
    categoryIds.add(category.id);
    if (!missionMapText(category.label) || !missionMapText(category.summary)) {
      issues.push(`Category ${category.id} is missing display copy.`);
    }
    const categoryKinds = missionMapSafeArray(category.kinds);
    if (!categoryKinds.length) {
      issues.push(`Category ${category.id} has no activity kinds.`);
    }
    categoryKinds.forEach((kind) => {
      const kindToken = missionMapKindToken(kind);
      if (!kindToken) {
        issues.push(`Category ${category.id} has a blank activity kind.`);
        return;
      }
      if (coveredKinds.has(kindToken)) {
        issues.push(
          `Activity kind ${kindToken} is assigned to multiple categories.`,
        );
      }
      coveredKinds.add(kindToken);
    });
  });

  activities.forEach((activity, index) => {
    if (!activity || !activity.id) {
      issues.push(`Activity ${index + 1} is missing an id.`);
      return;
    }
    if (ids.has(activity.id)) {
      issues.push(`Activity ${activity.id} is duplicated.`);
    }
    ids.add(activity.id);
    if (!missionMapText(activity.label) || !missionMapText(activity.icon)) {
      issues.push(`Activity ${activity.id} is missing tile copy.`);
    }
    if (!allowedLaunchTypes.has(activity.launchType)) {
      issues.push(
        `Activity ${activity.id} has unknown launch type ${activity.launchType}.`,
      );
    }
    if (!activity.target) {
      issues.push(`Activity ${activity.id} is missing a target.`);
    }
    const targetKey = `${activity.launchType}:${activity.target || activity.id}`;
    if (targetsByLaunchType.has(targetKey)) {
      issues.push(
        `Activity ${activity.id} reuses launcher target ${activity.target}.`,
      );
    }
    targetsByLaunchType.add(targetKey);
    if (!MISSION_MAP_ACTIVITY_DETAILS[activity.id]) {
      issues.push(`Activity ${activity.id} is missing detail metadata.`);
    }
    const detail = MISSION_MAP_ACTIVITY_DETAILS[activity.id];
    const detailKind = missionMapKindToken(detail && detail.kind);
    if (detail && (!detailKind || !missionMapText(detail.summary))) {
      issues.push(`Activity ${activity.id} detail metadata is incomplete.`);
    }
    if (detail && detailKind && !coveredKinds.has(detailKind)) {
      issues.push(
        `Activity ${activity.id} uses uncategorized kind ${detailKind}.`,
      );
    }
  });

  bound.forEach((activity) => {
    if (activity.unavailable) {
      issues.push(
        `Activity ${activity.id} is not bound to a ${activity.launchType} launcher for target ${activity.target}.`,
      );
    }
  });

  Object.keys(MISSION_MAP_ACTIVITY_DETAILS).forEach((activityId) => {
    if (!ids.has(activityId)) {
      issues.push(`Activity detail ${activityId} has no matching tile.`);
    }
  });

  return issues;
}

function MissionMapCategoryJumps({ options, selectedId, onSelect }) {
  const routeOptions = options.filter(
    (category) => category.id !== MISSION_MAP_ALL_CATEGORY_ID,
  );
  const focusCategoryButton = (categoryId) => {
    const safeCategoryId = missionMapDomId(categoryId);
    window.requestAnimationFrame(() => {
      document
        .querySelector(`[data-mission-map-category="${safeCategoryId}"]`)
        ?.focus();
    });
  };
  const selectCategory = (categoryId, shouldFocus = false) => {
    onSelect(categoryId);
    if (shouldFocus) focusCategoryButton(categoryId);
  };
  const handleCategoryKey = (event) => {
    const nextId = nextMissionMapCategoryId(options, selectedId, event.key);
    if (!nextId) return;
    event.preventDefault();
    selectCategory(nextId, true);
  };

  return (
    <div
      className="game-map-jumps"
      aria-label="Mission Map categories"
      aria-describedby="mission-map-category-help"
      onKeyDown={handleCategoryKey}
    >
      <span id="mission-map-category-help" className="sr-only">
        Use arrow keys, Home, and End to move between Mission Map categories.
      </span>
      {routeOptions.length ? (
        <div className="mission-route-ribbon" aria-label="Mission route">
          {routeOptions.map((category, index) => {
            const isActive = selectedId === category.id;
            return (
              <button
                key={`route-${missionMapDomId(category.id)}`}
                type="button"
                className={isActive ? "on" : ""}
                onClick={() => selectCategory(category.id)}
                aria-pressed={isActive ? "true" : "false"}
                aria-label={`Route stop ${index + 1}: ${missionMapCategoryButtonLabel(category)}`}
              >
                <span className="mission-route-number">{index + 1}</span>
                <span className="mission-route-copy">
                  <strong>{missionMapCopy(category.label, "Mission")}</strong>
                  <small>{missionMapCopy(category.summary, "Space activities")}</small>
                </span>
              </button>
            );
          })}
        </div>
      ) : null}
      {options.map((category) => (
        <button
          key={missionMapDomId(category.id)}
          type="button"
          className={selectedId === category.id ? "on" : ""}
          onClick={() => selectCategory(category.id)}
          aria-pressed={selectedId === category.id ? "true" : "false"}
          aria-label={missionMapCategoryButtonLabel(category)}
          data-mission-map-category={missionMapDomId(category.id)}
        >
          {category.label}
          <span aria-label={missionMapGameCountText(category.count)}>
            {category.count}
          </span>
        </button>
      ))}
    </div>
  );
}

function MissionMapHeader({ statusText, learningCue }) {
  return (
    <div className="menu-map-heading">
      <span>Mission Map</span>
      <strong>Choose today's mission</strong>
      <em>1. Study a world. 2. Play a mission. 3. Remember one thing.</em>
      <small aria-live="polite">{statusText}</small>
      <p>{learningCue}</p>
    </div>
  );
}

function MissionMapActivityTile({ activity, onLaunch, isFirstReady }) {
  return (
    <button
      className="menu-item game-map-tile"
      onClick={() => onLaunch(activity)}
      aria-label={activity.ariaLabel}
      data-kind={missionMapKindToken(activity.kind) || "activity"}
      disabled={activity.unavailable}
      title={activity.unavailable ? activity.unavailableReason : undefined}
    >
      <span>{activity.icon}</span>
      <div className="game-map-tile-copy">
        <strong>{activity.label}</strong>
        <small>{activity.summary}</small>
        {isFirstReady ? (
          <small className="game-map-tile-cue">Try first</small>
        ) : null}
        {activity.unavailable ? (
          <small className="game-map-tile-unavailable">
            {activity.unavailableReason}
          </small>
        ) : null}
      </div>
      <b
        className={
          activity.unavailable
            ? "game-map-tile-start is-unavailable"
            : "game-map-tile-start"
        }
        aria-hidden="true"
      >
        {activity.unavailable ? "Ask grown-up" : "Start"}
      </b>
    </button>
  );
}

function MissionMapActivityGroups({ groups, onLaunch, emptyText }) {
  if (!groups.length) {
    return <div className="game-map-empty">{emptyText}</div>;
  }

  let foundFirstReady = false;

  return (
    <>
      {groups.map((category) => (
        <section
          key={missionMapDomId(category.id)}
          className="game-map-group"
          aria-label={`${category.label} activities`}
        >
          <div className="game-map-group-heading">
            <strong>{category.label}</strong>
            <small>{category.summary}</small>
          </div>
          <div className="game-map-group-grid">
            {category.activities.map((activity) => (
              (() => {
                const isFirstReady = !foundFirstReady && !activity.unavailable;
                if (isFirstReady) foundFirstReady = true;
                return (
                  <MissionMapActivityTile
                    key={missionMapDomId(activity.id)}
                    activity={activity}
                    onLaunch={onLaunch}
                    isFirstReady={isFirstReady}
                  />
                );
              })()
            ))}
          </div>
        </section>
      ))}
    </>
  );
}
