import { world, system, ItemStack } from "@minecraft/server";
import { transferPlayer, variables } from "@minecraft/server-admin";
import { ActionFormData, ModalFormData, MessageFormData } from "@minecraft/server-ui";

const LOBBY_HOST = "10.0.0.247";
const LOBBY_PORT = 19132;
const COMPASS_ID = "minecraft:recovery_compass";
const MARKER_BLOCK = "minecraft:lodestone";
const PORTAL_PROP = "hub_portal_location";
const WAYPOINTS_PROP = "waypoints_v1";
const PORTAL_RADIUS = 3;
const TRANSFER_COOLDOWN = 5000;
const SPAWN_PROTECTION = 10000;

const MAX_MARKERS_PER_PLAYER = 10;
const ARRIVAL_RADIUS = 3;
const HUD_TICK_INTERVAL = 5;
const ARROWS = ["\u2191", "\u2197", "\u2192", "\u2198", "\u2193", "\u2199", "\u2190", "\u2196"];

// Navigation HUD lives in the title/subtitle slot so it doesn't fight the
// vanilla mount UI for the action bar. The directional content goes in the
// TITLE position (large, central) with the waypoint label in the subtitle as
// a secondary line. Empty-string title gets suppressed by some Bedrock client
// versions, so we always pass a non-empty title — that's what causes the
// arrival message to render but a setActionBar-replacement subtitle not to.
function showNav(player, mainLine, subLine) {
  try {
    player.onScreenDisplay.setTitle(mainLine, {
      subtitle: subLine ?? "",
      fadeInDuration: 0,
      stayDuration: HUD_TICK_INTERVAL * 4,
      fadeOutDuration: 0,
    });
  } catch (_) {}
}
function clearNav(player) {
  try {
    player.onScreenDisplay.setTitle(" ", {
      subtitle: "",
      fadeInDuration: 0,
      stayDuration: 1,
      fadeOutDuration: 0,
    });
  } catch (_) {}
}

const recentTransfers = new Map();
const spawnTimes = new Map();

// ─── Waypoint State ─────────────────────────────────────────────

// Persisted: markers per player { [ownerId]: [ {x,y,z,dim,label,key}, ... ] }
let markers = {};
// Per-player active waypoint index, persisted via player dynamic property so
// it survives container restarts and reconnects.
const ACTIVE_PROP = "hub_active_waypoint_v1";
function getActiveIndex(player) {
  try {
    const v = player.getDynamicProperty(ACTIVE_PROP);
    return typeof v === "number" ? v : null;
  } catch (_) { return null; }
}
function setActiveIndex(player, idx) {
  try {
    if (idx == null) player.setDynamicProperty(ACTIVE_PROP, undefined);
    else player.setDynamicProperty(ACTIVE_PROP, idx);
  } catch (_) {}
}
function clearActiveIndex(player) { setActiveIndex(player, null); }

function keyOf(loc, dimensionId) {
  return `${Math.floor(loc.x)},${Math.floor(loc.y)},${Math.floor(loc.z)},${dimensionId}`;
}

function loadWaypoints() {
  try {
    const raw = world.getDynamicProperty(WAYPOINTS_PROP);
    if (raw && typeof raw === "string") markers = JSON.parse(raw);
  } catch (e) {
    world.sendMessage(`§c[Waypoints] Failed to load: ${e.message}`);
    markers = {};
  }
}

function saveWaypoints() {
  try {
    world.setDynamicProperty(WAYPOINTS_PROP, JSON.stringify(markers));
  } catch (e) {
    world.sendMessage(`§c[Waypoints] Failed to save: ${e.message}`);
  }
}

function getMarkers(ownerId) {
  if (!markers[ownerId]) markers[ownerId] = [];
  return markers[ownerId];
}

function findMarkerByKey(key) {
  for (const ownerId of Object.keys(markers)) {
    const list = markers[ownerId];
    for (let i = 0; i < list.length; i++) {
      if (list[i].key === key) return { ownerId, index: i, marker: list[i] };
    }
  }
  return null;
}

function removeMarker(ownerId, index) {
  const list = getMarkers(ownerId);
  list.splice(index, 1);
  // Active index is stored on the owner's player dynamic property; update only
  // if they're online (offline owner will revalidate their own index next tick).
  for (const p of world.getAllPlayers()) {
    if (p.id !== ownerId) continue;
    const activeIdx = getActiveIndex(p);
    if (activeIdx != null) {
      if (activeIdx === index) clearActiveIndex(p);
      else if (activeIdx > index) setActiveIndex(p, activeIdx - 1);
    }
    break;
  }
  saveWaypoints();
}

// ─── Hub Transfer ───────────────────────────────────────────────

function doTransfer(player) {
  const now = Date.now();
  if (recentTransfers.has(player.name) && now - recentTransfers.get(player.name) < TRANSFER_COOLDOWN) {
    player.sendMessage("§c[Hub] §fPlease wait a moment before transferring again.");
    return;
  }

  recentTransfers.set(player.name, now);
  world.sendMessage(`§a${player.name} is returning to the Hub...`);

  const portalJson = world.getDynamicProperty(PORTAL_PROP);
  if (portalJson) {
    try {
      const portal = JSON.parse(portalJson);
      const pos = player.location;
      const dx = Math.abs(pos.x - portal.x);
      const dz = Math.abs(pos.z - portal.z);
      if (dx <= PORTAL_RADIUS + 1 && dz <= PORTAL_RADIUS + 1) {
        const safePos = { x: pos.x, y: pos.y, z: pos.z + 4 };
        player.teleport(safePos);
      }
    } catch (e) {
      // Non-fatal
    }
  }

  system.runTimeout(() => {
    try {
      transferPlayer(player, { hostname: LOBBY_HOST, port: LOBBY_PORT });
    } catch (e) {
      player.sendMessage(`§cTransfer failed: ${e.message}`);
    }
  }, 5);
}

// ─── Compass Distribution ───────────────────────────────────────

function giveCompassIfMissing(player) {
  const inventory = player.getComponent("minecraft:inventory");
  if (!inventory) return;

  const container = inventory.container;
  for (let i = 0; i < container.size; i++) {
    const item = container.getItem(i);
    if (item && item.typeId === COMPASS_ID) return;
  }

  container.addItem(new ItemStack(COMPASS_ID, 1));
  player.sendMessage("§b[Hub] §fYou received a §dHub Compass§f — right-click to open the menu.");
}

let WORLD_NAME = "this world";
try {
  WORLD_NAME = variables.get("world_name") || "this world";
} catch (e) {
  // variables.json may not exist
}

world.afterEvents.playerSpawn.subscribe((event) => {
  const player = event.player;
  spawnTimes.set(player.name, Date.now());
  system.runTimeout(() => {
    try {
      player.runCommand(`titleraw @s title {"rawtext":[{"text":"§6Welcome!"}]}`);
      player.runCommand(`titleraw @s subtitle {"rawtext":[{"text":"§7You are now in §e${WORLD_NAME}"}]}`);
      giveCompassIfMissing(player);
    } catch (e) {
      // Player may have disconnected
    }
  }, 20);
});

// Active waypoint is persisted as a player dynamic property, so playerLeave
// does not need to clear any in-memory session state.

// ─── Compass Menu ───────────────────────────────────────────────

world.afterEvents.itemUse.subscribe((event) => {
  const player = event.source;
  if (event.itemStack.typeId !== COMPASS_ID) return;
  system.run(() => openCompassMenu(player));
});

async function openCompassMenu(player) {
  const list = getMarkers(player.id);
  const dimId = player.dimension.id;
  const visible = list
    .map((m, i) => ({ m, i, dist: distanceXZ(player.location, m) }))
    .filter(({ m }) => m.dim === dimId);

  const form = new ActionFormData()
    .title("Hub Compass")
    .body(`§7Waypoints: §f${list.length}§7 / ${MAX_MARKERS_PER_PLAYER}`);

  const actions = [];
  form.button("\uD83C\uDFE0 Return to Hub");
  actions.push({ kind: "hub" });

  for (const { m, i, dist } of visible) {
    form.button(`§f${m.label}\n§7${Math.round(dist)}m away`);
    actions.push({ kind: "select", index: i });
  }

  const activeIdx = getActiveIndex(player);
  if (activeIdx != null && list[activeIdx]) {
    form.button(`§c\u274C Clear active waypoint`);
    actions.push({ kind: "clear" });
  }

  if (list.length > 0) {
    form.button("\uD83D\uDDD1 Delete a waypoint…");
    actions.push({ kind: "delete_menu" });

    form.button("\uD83D\uDCE4 Share with another player…");
    actions.push({ kind: "share" });
  }

  form.button("\uD83E\uDDED Get a marker block");
  actions.push({ kind: "give_marker" });

  form.button("§7Cancel");
  actions.push({ kind: "cancel" });

  let res;
  try { res = await form.show(player); } catch (_) { return; }
  if (res.canceled || res.selection === undefined) return;

  const a = actions[res.selection];
  if (!a || a.kind === "cancel") return;

  if (a.kind === "hub") {
    doTransfer(player);
    return;
  }
  if (a.kind === "select") {
    setActiveIndex(player, a.index);
    const m = list[a.index];
    player.sendMessage(`§b[Waypoints] §fGuiding you to §d${m.label}§f. §8(arrow appears at the top of your screen)`);
    return;
  }
  if (a.kind === "clear") {
    clearActiveIndex(player);
    clearNav(player);
    player.sendMessage("§b[Waypoints] §fActive waypoint cleared.");
    return;
  }
  if (a.kind === "delete_menu") {
    await openDeleteMenu(player);
    return;
  }
  if (a.kind === "share") {
    await openShareMenu(player);
    return;
  }
  if (a.kind === "give_marker") {
    giveMarkerBlock(player);
    return;
  }
}

function giveMarkerBlock(player) {
  try {
    const inv = player.getComponent("minecraft:inventory")?.container;
    if (!inv) {
      player.sendMessage("§c[Waypoints] §fCouldn't access your inventory.");
      return;
    }
    const leftover = inv.addItem(new ItemStack(MARKER_BLOCK, 1));
    if (leftover) {
      const pos = player.location;
      player.dimension.spawnItem(new ItemStack(MARKER_BLOCK, 1), { x: pos.x, y: pos.y + 1, z: pos.z });
      player.sendMessage("§b[Waypoints] §fInventory full — dropped a §dlodestone§f at your feet.");
    } else {
      player.sendMessage("§b[Waypoints] §fHere's a §dlodestone§f — place it to set a waypoint.");
    }
  } catch (e) {
    player.sendMessage(`§c[Waypoints] §fCouldn't give lodestone: ${e.message}`);
  }
}

async function openDeleteMenu(player) {
  const list = getMarkers(player.id);
  if (list.length === 0) return;

  const form = new ActionFormData()
    .title("Delete a Waypoint")
    .body("§7Pick a waypoint to remove. The lodestone block stays — break it to clean up.");

  for (const m of list) {
    form.button(`§f${m.label}\n§7${m.x}, ${m.y}, ${m.z}`);
  }
  form.button("§7Cancel");

  let res;
  try { res = await form.show(player); } catch (_) { return; }
  if (res.canceled || res.selection === undefined) return;
  if (res.selection >= list.length) return;

  const chosen = list[res.selection];
  const confirm = new MessageFormData()
    .title("Confirm Delete")
    .body(`Remove waypoint §f${chosen.label}§r?`)
    .button1("Delete")
    .button2("Cancel");

  let cf;
  try { cf = await confirm.show(player); } catch (_) { return; }
  if (cf.canceled || cf.selection !== 0) return;

  removeMarker(player.id, res.selection);
  player.sendMessage(`§b[Waypoints] §fDeleted §d${chosen.label}§f.`);
}

// ─── Share Waypoint ─────────────────────────────────────────────
//     Pick one of your waypoints, pick an online player in this world, send
//     them a confirm prompt. On accept, the waypoint is copied into the
//     recipient's list with a `(from <sender>)` suffix so it's clearly
//     attributed. Cross-world shares are blocked here — a player on the lyla
//     server can't be reached from jamie. Same dimension is preferred but
//     cross-dim is allowed; the HUD already labels "different dimension".

async function openShareMenu(sender) {
  const list = getMarkers(sender.id);
  if (list.length === 0) {
    sender.sendMessage("§7[Share] You have no waypoints to share.");
    return;
  }
  const others = world.getAllPlayers().filter((p) => p.id !== sender.id);
  if (others.length === 0) {
    sender.sendMessage("§7[Share] No other players are online in this world.");
    return;
  }

  const wpForm = new ActionFormData()
    .title("Share a Waypoint")
    .body("§7Pick which waypoint to share:");
  for (const m of list) {
    wpForm.button(`§f${m.label}\n§7${m.x}, ${m.y}, ${m.z}`);
  }
  wpForm.button("§7Cancel");

  let wpRes;
  try { wpRes = await wpForm.show(sender); } catch (_) { return; }
  if (wpRes.canceled || wpRes.selection === undefined) return;
  if (wpRes.selection >= list.length) return;
  const marker = list[wpRes.selection];

  const recipForm = new ActionFormData()
    .title(`Share "${marker.label}"`)
    .body("§7Who should receive this waypoint?");
  for (const p of others) recipForm.button(`§f${p.name}`);
  recipForm.button("§7Cancel");

  let rRes;
  try { rRes = await recipForm.show(sender); } catch (_) { return; }
  if (rRes.canceled || rRes.selection === undefined) return;
  if (rRes.selection >= others.length) return;
  const recipient = others[rRes.selection];

  // Receiver capacity check before bothering them with a prompt
  const recList = getMarkers(recipient.id);
  if (recList.length >= MAX_MARKERS_PER_PLAYER) {
    sender.sendMessage(`§c[Share] §f${recipient.name}'s waypoint slots are full.`);
    return;
  }

  sender.sendMessage(`§b[Share] §fSent §d${marker.label}§f to §e${recipient.name}§f — waiting for them to accept…`);

  const offer = new MessageFormData()
    .title("Waypoint Shared")
    .body(`§e${sender.name}§r wants to share a waypoint with you:\n\n§f${marker.label}\n§7${marker.x}, ${marker.y}, ${marker.z}\n\nAdd to your compass?`)
    .button1("§aAccept")
    .button2("§cDecline");

  let oRes;
  try { oRes = await offer.show(recipient); } catch (_) {
    sender.sendMessage(`§c[Share] §f${recipient.name} couldn't be reached.`);
    return;
  }
  if (oRes.canceled || oRes.selection !== 0) {
    sender.sendMessage(`§7[Share] §f${recipient.name} declined.`);
    recipient.sendMessage(`§7[Share] §fDeclined waypoint from §e${sender.name}§f.`);
    return;
  }

  // Re-check capacity after the await — markers may have changed
  if (recList.length >= MAX_MARKERS_PER_PLAYER) {
    sender.sendMessage(`§c[Share] §f${recipient.name}'s waypoint slots are full.`);
    recipient.sendMessage(`§c[Share] §fYour waypoint slots are full — couldn't accept §d${marker.label}§f.`);
    return;
  }

  const sharedLabel = `${marker.label} (from ${sender.name})`.slice(0, 40);
  // Use a key that won't collide with the receiver's own lodestones
  const sharedKey = `shared:${sender.id}:${marker.key}`;
  recList.push({
    x: marker.x,
    y: marker.y,
    z: marker.z,
    dim: marker.dim,
    label: sharedLabel,
    key: sharedKey,
  });
  saveWaypoints();

  sender.sendMessage(`§a[Share] §f${recipient.name} accepted §d${marker.label}§f.`);
  recipient.sendMessage(`§a[Share] §fAdded §d${sharedLabel}§f. Type §e!nav§f to find it.`);
}

// ─── Placement → Label Prompt ───────────────────────────────────

world.afterEvents.playerPlaceBlock.subscribe((event) => {
  if (event.block.typeId !== MARKER_BLOCK) return;
  const player = event.player;
  const block = event.block;
  const dim = block.dimension;
  const loc = { x: block.location.x, y: block.location.y, z: block.location.z };

  const list = getMarkers(player.id);
  if (list.length >= MAX_MARKERS_PER_PLAYER) {
    system.run(() => {
      try {
        dim.runCommand(`setblock ${loc.x} ${loc.y} ${loc.z} air`);
        dim.spawnItem(new ItemStack(MARKER_BLOCK, 1), { x: loc.x + 0.5, y: loc.y + 0.5, z: loc.z + 0.5 });
      } catch (_) {}
      player.sendMessage(`§c[Waypoints] §fMax of ${MAX_MARKERS_PER_PLAYER} waypoints reached — delete one first.`);
    });
    return;
  }

  system.run(() => promptForLabel(player, loc, dim.id, list.length));
});

async function promptForLabel(player, loc, dimId, nextIndex) {
  const form = new ModalFormData()
    .title("Name this Waypoint")
    .textField("Label", "e.g. Mine, Base, Farm", `Waypoint ${nextIndex + 1}`);

  let res;
  try { res = await form.show(player); } catch (_) { return; }
  if (res.canceled) {
    // Still register with default label so the physical lodestone isn't orphaned
    saveMarker(player, loc, dimId, `Waypoint ${nextIndex + 1}`);
    return;
  }

  const raw = (res.formValues && res.formValues[0]) || "";
  const label = String(raw).trim().slice(0, 24) || `Waypoint ${nextIndex + 1}`;
  saveMarker(player, loc, dimId, label);
}

function saveMarker(player, loc, dimId, label) {
  const list = getMarkers(player.id);
  const key = keyOf(loc, dimId);
  if (list.some((m) => m.key === key)) return; // already registered (e.g. re-placed)
  list.push({
    x: Math.floor(loc.x),
    y: Math.floor(loc.y),
    z: Math.floor(loc.z),
    dim: dimId,
    label,
    key,
  });
  saveWaypoints();
  player.sendMessage(`§b[Waypoints] §fSaved §d${label}§f — select it from the compass to navigate.`);
}

// ─── Break → Unregister ─────────────────────────────────────────

try {
  world.beforeEvents.playerBreakBlock.subscribe((event) => {
    const block = event.block;
    if (block.typeId !== MARKER_BLOCK) return;

    const key = keyOf(block.location, block.dimension.id);
    const hit = findMarkerByKey(key);
    if (!hit) return; // untracked lodestone — normal vanilla break

    const player = event.player;
    if (hit.ownerId !== player.id) {
      event.cancel = true;
      const ownerName = findOwnerName(hit.ownerId);
      system.run(() =>
        player.sendMessage(
          `§c[Waypoints] §7This waypoint belongs to §f${ownerName}§7. You can't break it.`
        )
      );
      return;
    }

    event.cancel = true;
    const loc = { x: block.location.x, y: block.location.y, z: block.location.z };
    const dim = block.dimension;
    const label = hit.marker.label;
    system.run(() => {
      try {
        const dropPos = { x: loc.x + 0.5, y: loc.y + 0.5, z: loc.z + 0.5 };
        dim.spawnItem(new ItemStack(MARKER_BLOCK, 1), dropPos);
        dim.runCommand(`setblock ${loc.x} ${loc.y} ${loc.z} air`);
      } catch (e) {
        player.sendMessage(`§c[Waypoints] Error during break: ${e.message}`);
      }
      removeMarker(hit.ownerId, hit.index);
      player.sendMessage(`§b[Waypoints] §fRemoved §d${label}§f.`);
    });
  });
} catch (e) {
  console.warn(`[Waypoints] beforeEvents.playerBreakBlock unavailable: ${e}`);
}

function findOwnerName(ownerId) {
  for (const p of world.getAllPlayers()) if (p.id === ownerId) return p.name;
  return "another player";
}

// ─── Guidance HUD ───────────────────────────────────────────────

function distanceXZ(a, b) {
  const dx = (b.x + 0.5) - a.x;
  const dz = (b.z + 0.5) - a.z;
  return Math.sqrt(dx * dx + dz * dz);
}

system.runInterval(() => {
  for (const player of world.getAllPlayers()) {
    const idx = getActiveIndex(player);
    if (idx == null) continue;

    const list = markers[player.id];
    const m = list && list[idx];
    if (!m) {
      clearActiveIndex(player);
      continue;
    }
    if (m.dim !== player.dimension.id) {
      showNav(player, `§c⚠ Different Dimension`, `§7${m.label}`);
      continue;
    }

    const pos = player.location;
    const dx = (m.x + 0.5) - pos.x;
    const dz = (m.z + 0.5) - pos.z;
    const distXZ = Math.sqrt(dx * dx + dz * dz);

    if (distXZ <= ARRIVAL_RADIUS) {
      clearActiveIndex(player);
      try {
        player.onScreenDisplay.setTitle(`§aArrived`, { subtitle: `§f${m.label}`, fadeInDuration: 5, stayDuration: 30, fadeOutDuration: 10 });
      } catch (_) {}
      continue;
    }

    // Bedrock yaw: 0 faces +Z (south), +yaw turns clockwise (toward -X, i.e. west).
    // Bearing to target in the same yaw convention:
    const bearingDeg = (Math.atan2(-dx, dz) * 180 / Math.PI + 360) % 360;
    let yaw;
    try { yaw = player.getRotation().y; } catch (_) { continue; }
    const relative = ((bearingDeg - yaw + 540) % 360) - 180; // -180..180
    const bucket = ((Math.round(relative / 45) % 8) + 8) % 8;
    const arrow = ARROWS[bucket];

    showNav(player, `§a${arrow} §f${Math.round(distXZ)}m`, `§7${m.label}`);
  }
}, HUD_TICK_INTERVAL);

// ─── Portal Zone Detection ──────────────────────────────────────

system.runInterval(() => {
  const portalJson = world.getDynamicProperty(PORTAL_PROP);
  if (!portalJson) return;

  let portal;
  try {
    portal = JSON.parse(portalJson);
  } catch {
    return;
  }

  const now = Date.now();
  const players = world.getAllPlayers();

  for (const player of players) {
    if (recentTransfers.has(player.name) && now - recentTransfers.get(player.name) < TRANSFER_COOLDOWN) continue;
    if (spawnTimes.has(player.name) && now - spawnTimes.get(player.name) < SPAWN_PROTECTION) continue;

    const pos = player.location;
    const dx = Math.abs(pos.x - portal.x);
    const dy = Math.abs(pos.y - portal.y);
    const dz = Math.abs(pos.z - portal.z);

    if (dx <= PORTAL_RADIUS && dy <= 3 && dz <= PORTAL_RADIUS) {
      doTransfer(player);
    }
  }
}, 20);

// ─── Chat Commands ──────────────────────────────────────────────

function handleChatCommand(player, msg) {
  if (msg === "!compass") {
    try {
      giveCompassIfMissing(player);
      player.sendMessage("§b[Hub] §fCompass check complete.");
    } catch (e) {
      player.sendMessage(`§c[Hub] §fCouldn't give compass: ${e.message}`);
    }
    return true;
  }

  if (msg === "!setportal") {
    const pos = player.location;
    const loc = { x: Math.floor(pos.x), y: Math.floor(pos.y), z: Math.floor(pos.z) };
    world.setDynamicProperty(PORTAL_PROP, JSON.stringify(loc));
    buildPortal(loc, player.dimension);
    player.sendMessage(`§b[Hub] §fReturn portal set at §d${loc.x}, ${loc.y}, ${loc.z}§f!`);
    return true;
  }

  if (msg === "!hub" || msg === "!lobby") {
    doTransfer(player);
    return true;
  }

  if (msg === "!waypoints") {
    system.run(() => openCompassMenu(player));
    return true;
  }

  if (msg === "!marker") {
    giveMarkerBlock(player);
    return true;
  }

  if (msg === "!share") {
    system.run(() => openShareMenu(player));
    return true;
  }

  if (msg === "!nav" || msg.startsWith("!nav ")) {
    const list = getMarkers(player.id);
    const arg = msg.slice(4).trim();
    if (arg === "off" || arg === "clear") {
      clearActiveIndex(player);
      clearNav(player);
      player.sendMessage("§b[Waypoints] §fNavigation cleared.");
      return true;
    }
    if (arg) {
      const n = Number.parseInt(arg, 10);
      if (Number.isFinite(n) && n >= 1 && n <= list.length) {
        setActiveIndex(player, n - 1);
        const m = list[n - 1];
        player.sendMessage(`§b[Waypoints] §fGuiding you to §d${m.label}§f. §8(arrow at top of screen)`);
        return true;
      }
      player.sendMessage(`§c[Waypoints] §fNo waypoint #${arg} — type §e!nav§f to list yours.`);
      return true;
    }
    if (list.length === 0) {
      player.sendMessage("§7[Waypoints] No waypoints yet — place a §dlodestone§7 to set one.");
      return true;
    }
    const idx = getActiveIndex(player);
    const cur = idx != null ? list[idx] : null;
    if (cur) {
      const dist = Math.round(distanceXZ(player.location, cur));
      player.sendMessage(`§b[Waypoints] §fActive: §d${cur.label}§f (${dist}m). Type §e!nav off§f to clear, or §e!nav <number>§f to switch.`);
    } else {
      player.sendMessage("§b[Waypoints] §fNo active waypoint. Pick one:");
    }
    for (let i = 0; i < list.length; i++) {
      const m = list[i];
      const here = m.dim === player.dimension.id;
      const tag = here ? `§7${Math.round(distanceXZ(player.location, m))}m` : "§8(other dim)";
      player.sendMessage(`  §e${i + 1}§7. §f${m.label} ${tag}`);
    }
    return true;
  }

  if (msg === "!clearwaypoints") {
    system.run(async () => {
      const confirm = new MessageFormData()
        .title("Clear all waypoints?")
        .body("§7This deletes every waypoint you've saved. Lodestone blocks stay in the world — break them to reclaim.")
        .button1("§cDelete all")
        .button2("Cancel");
      let res;
      try { res = await confirm.show(player); } catch (_) { return; }
      if (res.canceled || res.selection !== 0) return;
      markers[player.id] = [];
      clearActiveIndex(player);
      saveWaypoints();
      player.sendMessage("§b[Waypoints] §fAll your waypoints were cleared.");
    });
    return true;
  }

  return false;
}

try {
  world.beforeEvents.chatSend.subscribe((event) => {
    const msg = event.message.trim().toLowerCase();
    if (msg.startsWith("!")) {
      event.cancel = true;
      const player = event.sender;
      system.run(() => handleChatCommand(player, msg));
    }
  });
} catch (e) {
  world.sendMessage("§e[Hub] §fChat commands (!hub, !lobby) unavailable — use compass or portal instead.");
}

system.afterEvents.scriptEventReceive.subscribe((event) => {
  if (event.id !== "hub:cmd") return;
  const player = event.sourceEntity;
  if (!player) return;
  const msg = (event.message || "").trim().toLowerCase();
  handleChatCommand(player, `!${msg}`);
});

// ─── Portal Auto-Build ──────────────────────────────────────────

function buildPortal(loc, dimension) {
  const x = loc.x;
  const y = loc.y;
  const z = loc.z;

  const commands = [];
  commands.push(`fill ${x - 1} ${y} ${z} ${x + 1} ${y + 4} ${z} air`);
  commands.push(`setblock ${x - 1} ${y} ${z} obsidian`);
  commands.push(`setblock ${x} ${y} ${z} obsidian`);
  commands.push(`setblock ${x + 1} ${y} ${z} obsidian`);
  commands.push(`setblock ${x - 1} ${y + 1} ${z} crying_obsidian`);
  commands.push(`setblock ${x - 1} ${y + 2} ${z} crying_obsidian`);
  commands.push(`setblock ${x - 1} ${y + 3} ${z} crying_obsidian`);
  commands.push(`setblock ${x + 1} ${y + 1} ${z} crying_obsidian`);
  commands.push(`setblock ${x + 1} ${y + 2} ${z} crying_obsidian`);
  commands.push(`setblock ${x + 1} ${y + 3} ${z} crying_obsidian`);
  commands.push(`setblock ${x} ${y + 1} ${z} stained_glass ["color":"purple"]`);
  commands.push(`setblock ${x} ${y + 2} ${z} stained_glass ["color":"purple"]`);
  commands.push(`setblock ${x} ${y + 3} ${z} stained_glass ["color":"purple"]`);
  commands.push(`setblock ${x} ${y + 4} ${z} obsidian`);

  for (const cmd of commands) {
    try {
      dimension.runCommand(cmd);
    } catch (e) {
      // Non-fatal
    }
  }
}

system.runTimeout(() => {
  const existing = world.getDynamicProperty(PORTAL_PROP);
  if (existing) return;

  const spawn = world.getDefaultSpawnLocation();
  const loc = { x: spawn.x + 5, y: spawn.y, z: spawn.z };

  world.setDynamicProperty(PORTAL_PROP, JSON.stringify(loc));

  const overworld = world.getDimension("overworld");
  buildPortal(loc, overworld);

  world.sendMessage("§b[Hub] §fReturn portal auto-built near spawn! Use §d!setportal§f to relocate it.");
}, 100);

system.run(() => {
  loadWaypoints();
  world.sendMessage("§b[World] §fHub return system loaded! Place a §dlodestone§f to set a waypoint, then right-click your compass and pick one — the §anavigation arrow§f shows at the §atop§f of your screen. (Type §e!nav§f for chat fallback.)");
});
