import { world, system, ItemStack } from "@minecraft/server";

// ─── Constants ──────────────────────────────────────────────
const TENT_ITEM = "silverlabs:tent";
const HAMMOCK_ITEM = "silverlabs:hammock";
const TENT_BLOCK = "silverlabs:tent_canvas"; // legacy cube — kept so old worlds load cleanly
const TENT_PANEL_L = "silverlabs:tent_panel_l";
const TENT_PANEL_R = "silverlabs:tent_panel_r";
const TENT_BLOCK_IDS = [TENT_BLOCK, TENT_PANEL_L, TENT_PANEL_R];
const HAMMOCK_BLOCK = "silverlabs:hammock_cloth";
const STATE_PROP = "camping_state_v1";
const HAMMOCK_TAG = "camping_hammock";
const TENT_REST_PROP = "camping_tent_rest"; // per-player: "{x,y,z,dim,startTick}"
const SLEEP_TICK_INTERVAL = 20; // run sleep loop every 1s
const NIGHT_START = 12500;
const NIGHT_END = 23500;

// ─── State ──────────────────────────────────────────────────
// tents:    key = "ox,oy,oz,dim" -> { ownerId, ownerName, cells: [[x,y,z]...] }
// hammocks: key = "ax,ay,az->bx,by,bz,dim" -> { ownerId, ownerName, anchorA, anchorB, cells }
let state = { tents: {}, hammocks: {} };

function loadState() {
  try {
    const raw = world.getDynamicProperty(STATE_PROP);
    if (raw && typeof raw === "string") {
      const parsed = JSON.parse(raw);
      state = {
        tents: parsed.tents || {},
        hammocks: parsed.hammocks || {},
      };
    }
  } catch (e) {
    world.sendMessage(`§c[Camping] state load failed: ${e.message}`);
  }
}

function saveState() {
  try {
    world.setDynamicProperty(STATE_PROP, JSON.stringify(state));
  } catch (e) {
    world.sendMessage(`§c[Camping] state save failed: ${e.message}`);
  }
}

function keyOf(x, y, z, dimId) {
  return `${x},${y},${z},${dimId}`;
}

// ─── Orientation helpers ────────────────────────────────────
function cardinalFacing(yaw) {
  let y = yaw;
  while (y > 180) y -= 360;
  while (y < -180) y += 360;
  if (y >= -45 && y < 45) return "south";
  if (y >= 45 && y < 135) return "west";
  if (y >= -135 && y < -45) return "east";
  return "north";
}

// Map our placement facing → block-state cardinal_direction the panel rotates to.
// The geometry's "default" (cardinal_direction = "north") has the slope's outer
// edge on -X and inner apex on +X with the depth running along Z. When the
// player faces north, that aligns with the world. When they face elsewhere,
// the placement_direction trait + permutations rotate the model to match.
function blockFacingFor(playerFacing) {
  return playerFacing; // 1:1 — placement_direction handles the rotation
}

function vecsForFacing(facing) {
  switch (facing) {
    case "north": return { fx: 0, fz: -1, rx: 1, rz: 0 };
    case "south": return { fx: 0, fz: 1, rx: -1, rz: 0 };
    case "east":  return { fx: 1, fz: 0, rx: 0, rz: -1 };
    case "west":  return { fx: -1, fz: 0, rx: 0, rz: 1 };
  }
  return { fx: 0, fz: 1, rx: -1, rz: 0 };
}

// ─── Inventory helpers ──────────────────────────────────────
function consumeOneOfType(player, typeId) {
  const inv = player.getComponent("inventory")?.container;
  if (!inv) return false;
  const slot = player.selectedSlotIndex;
  const item = inv.getItem(slot);
  if (item && item.typeId === typeId) {
    if (item.amount > 1) {
      item.amount -= 1;
      inv.setItem(slot, item);
    } else {
      inv.setItem(slot, undefined);
    }
    return true;
  }
  for (let i = 0; i < inv.size; i++) {
    const it = inv.getItem(i);
    if (it && it.typeId === typeId) {
      if (it.amount > 1) {
        it.amount -= 1;
        inv.setItem(i, it);
      } else {
        inv.setItem(i, undefined);
      }
      return true;
    }
  }
  return false;
}

// ─── Tent placement (2×3 footprint, ridge-tunnel shape) ─────
function tryPlaceTent(player) {
  const dim = player.dimension;
  const facing = cardinalFacing(player.getRotation().y);
  const { fx, fz, rx, rz } = vecsForFacing(facing);
  // Use precise player position; floor X/Z but scan Y downward to find the actual
  // standing surface. player.location.y may be fractionally above the block you're
  // on (e.g. 87.01), so floor() alone is reliable, but if the player is in the
  // air (jumping / on a slab / flying) we want to project them down to solid ground
  // so the tent doesn't try to sit on empty space.
  const feetX = Math.floor(player.location.x);
  const feetZ = Math.floor(player.location.z);
  let feetY = Math.floor(player.location.y);
  // If the block at feet level is solid (player inside a block, e.g. standing in
  // tall grass that rounded up), step up one.
  const feetBlock = dim.getBlock({ x: feetX, y: feetY, z: feetZ });
  if (feetBlock && !feetBlock.isAir && !feetBlock.isLiquid) feetY += 1;
  // If the block below is air (mid-jump / airborne), project down to ground.
  for (let probe = 0; probe < 4; probe++) {
    const below = dim.getBlock({ x: feetX, y: feetY - 1, z: feetZ });
    if (below && !below.isAir && !below.isLiquid) break;
    feetY -= 1;
  }
  const ox = feetX + fx;
  const oy = feetY;
  const oz = feetZ + fz;

  const groundCells = [];
  const clearCells = [];
  for (let l = 0; l < 3; l++) {
    for (let w = 0; w < 2; w++) {
      const cx = ox + l * fx + w * rx;
      const cz = oz + l * fz + w * rz;
      groundCells.push({ x: cx, y: oy - 1, z: cz });
      for (let h = 0; h <= 1; h++) clearCells.push({ x: cx, y: oy + h, z: cz });
    }
  }

  for (const g of groundCells) {
    const b = dim.getBlock(g);
    if (!b || b.isAir || b.isLiquid) {
      const seen = b ? b.typeId : "unloaded";
      player.sendMessage(`§c[Camping] §7Ground at §f${g.x},${g.y},${g.z}§7 is §f${seen}§7 — need solid ground there.`);
      return false;
    }
  }
  for (const c of clearCells) {
    const b = dim.getBlock(c);
    if (!b) {
      player.sendMessage(`§c[Camping] §7Can't reach §f${c.x},${c.y},${c.z}§7 (chunk unloaded).`);
      return false;
    }
    if (!b.isAir && !b.isLiquid) {
      player.sendMessage(`§c[Camping] §7Space at §f${c.x},${c.y},${c.z}§7 is blocked by §f${b.typeId}§7.`);
      return false;
    }
  }

  // A-frame layout: 3 long × 2 wide, single block tall. Each cross-section is a
  // pair of slope panels meeting at the apex on the seam between the two columns.
  // Bedrock renders the panel geometry mirrored across the block's local X axis
  // relative to a literal read of the .geo.json, so panel_r goes in the player's
  // column and panel_l goes one step right to land /\ instead of \/.
  const blockFacing = blockFacingFor(facing);
  const canvasCells = [];
  for (let l = 0; l < 3; l++) {
    canvasCells.push({
      x: ox + l * fx,
      y: oy,
      z: oz + l * fz,
      block: TENT_PANEL_R,
    });
    canvasCells.push({
      x: ox + l * fx + rx,
      y: oy,
      z: oz + l * fz + rz,
      block: TENT_PANEL_L,
    });
  }

  for (const c of canvasCells) {
    try {
      dim.runCommand(
        `setblock ${c.x} ${c.y} ${c.z} ${c.block} ["minecraft:cardinal_direction"="${blockFacing}"]`
      );
    } catch (_) {}
  }

  const key = keyOf(ox, oy, oz, dim.id);
  state.tents[key] = {
    ownerId: player.id,
    ownerName: player.name,
    facing,
    cells: canvasCells.map((c) => [c.x, c.y, c.z]),
  };
  saveState();
  return true;
}

// ─── Hammock placement ──────────────────────────────────────
function isPostBlock(block) {
  if (!block) return false;
  const id = block.typeId;
  return (
    id.endsWith("_fence") ||
    id.endsWith("_log") ||
    id.endsWith("_wood") ||
    id.includes("stripped_") ||
    id.endsWith("_wall")
  );
}

function findPartnerPost(dim, anchor) {
  const candidates = [];
  for (let dx = -6; dx <= 6; dx++) {
    for (let dz = -6; dz <= 6; dz++) {
      if (dx === 0 && dz === 0) continue;
      const aligned = dx === 0 || dz === 0 || Math.abs(dx) === Math.abs(dz);
      if (!aligned) continue;
      const dist = Math.max(Math.abs(dx), Math.abs(dz));
      if (dist < 3 || dist > 6) continue;
      for (let dy = -1; dy <= 1; dy++) {
        const pos = { x: anchor.x + dx, y: anchor.y + dy, z: anchor.z + dz };
        let blk;
        try { blk = dim.getBlock(pos); } catch (_) { continue; }
        if (!blk || !isPostBlock(blk)) continue;
        candidates.push({ pos, dist, dy });
      }
    }
  }
  if (candidates.length === 0) return null;
  candidates.sort((a, b) => (Math.abs(a.dy) - Math.abs(b.dy)) || (a.dist - b.dist));
  return candidates[0].pos;
}

function computeHammockCells(a, b) {
  const dx = b.x - a.x;
  const dy = b.y - a.y;
  const dz = b.z - a.z;
  const steps = Math.max(Math.abs(dx), Math.abs(dz));
  const cells = [];
  for (let t = 1; t < steps; t++) {
    const cx = a.x + Math.round((dx * t) / steps);
    const cz = a.z + Math.round((dz * t) / steps);
    let cy = a.y + Math.round((dy * t) / steps);
    const rel = t / steps;
    if (steps >= 4 && rel > 0.25 && rel < 0.75) cy -= 1;
    cells.push({ x: cx, y: cy, z: cz });
  }
  return cells;
}

function tryPlaceHammock(player, anchorBlock) {
  const dim = player.dimension;
  const a = {
    x: anchorBlock.location.x,
    y: anchorBlock.location.y,
    z: anchorBlock.location.z,
  };
  const b = findPartnerPost(dim, a);
  if (!b) {
    player.sendMessage("§c[Camping] §7Need a second post 3–6 blocks away (straight line or diagonal, ±1 block in height).");
    return false;
  }
  const cells = computeHammockCells(a, b);
  for (const c of cells) {
    let blk;
    try { blk = dim.getBlock(c); } catch (_) { return false; }
    if (!blk || (!blk.isAir && !blk.isLiquid)) {
      player.sendMessage("§c[Camping] §7The space between the posts isn't clear.");
      return false;
    }
  }
  for (const c of cells) {
    try { dim.runCommand(`setblock ${c.x} ${c.y} ${c.z} ${HAMMOCK_BLOCK}`); } catch (_) {}
  }
  const key = `${a.x},${a.y},${a.z}->${b.x},${b.y},${b.z},${dim.id}`;
  state.hammocks[key] = {
    ownerId: player.id,
    ownerName: player.name,
    anchorA: [a.x, a.y, a.z],
    anchorB: [b.x, b.y, b.z],
    cells: cells.map((c) => [c.x, c.y, c.z]),
  };
  saveState();
  return true;
}

// ─── Item use handler ───────────────────────────────────────
world.afterEvents.itemUse.subscribe((event) => {
  const player = event.source;
  const stack = event.itemStack;
  if (!stack || !player) return;
  if (stack.typeId === TENT_ITEM) {
    system.run(() => {
      if (tryPlaceTent(player)) {
        consumeOneOfType(player, TENT_ITEM);
        player.sendMessage("§a[Camping] §7Tent pitched. Right-click the canvas to rest until dawn.");
      }
    });
  } else if (stack.typeId === HAMMOCK_ITEM) {
    system.run(() => {
      const looking = player.getBlockFromViewDirection({ maxDistance: 6 });
      const block = looking?.block;
      if (!block || !isPostBlock(block)) {
        player.sendMessage("§c[Camping] §7Aim at a fence, log, or wooden post to anchor the hammock.");
        return;
      }
      if (tryPlaceHammock(player, block)) {
        consumeOneOfType(player, HAMMOCK_ITEM);
        player.sendMessage("§a[Camping] §7Hammock strung. Right-click the cloth to climb in.");
      }
    });
  }
});

// ─── Interact: tent rest + hammock toggle ───────────────────
try {
  world.beforeEvents.playerInteractWithBlock.subscribe((event) => {
    const block = event.block;
    if (!block) return;
    if (TENT_BLOCK_IDS.includes(block.typeId)) {
      event.cancel = true;
      const player = event.player;
      const loc = { x: block.location.x, y: block.location.y, z: block.location.z };
      const dimId = block.dimension.id;
      system.run(() => enterTentRest(player, loc, dimId));
    } else if (block.typeId === HAMMOCK_BLOCK) {
      event.cancel = true;
      const player = event.player;
      const loc = block.location;
      system.run(() => toggleHammock(player, loc));
    }
  });
} catch (e) {
  console.warn(`[Camping] playerInteractWithBlock unavailable: ${e}`);
}

// ─── Tent rest: vote-skip with mixed bed + tent sleepers ────────
// Tracks which players are currently "resting" in a tent. A player counts as a
// tent sleeper as long as they stay near the panel they interacted with and
// don't sneak/move/disconnect/take damage. Vanilla bed sleepers are detected
// via player.isSleeping (true while a player is in a real bed). We compare the
// combined count against the world's playersSleepingPercentage gamerule and
// skip the night when the threshold is crossed.

const tentRest = new Map(); // playerId → { x, y, z, dimId, startTick }

function isNight(tod) {
  return tod >= NIGHT_START && tod <= NIGHT_END;
}

function enterTentRest(player, loc, dimId) {
  const tod = world.getTimeOfDay();
  if (!isNight(tod)) {
    player.sendMessage("§e[Camping] §7It's still daylight — nothing to sleep off.");
    return;
  }
  if (tentRest.has(player.id)) {
    leaveTentRest(player, "§7[Camping] You stop resting.");
    return;
  }
  tentRest.set(player.id, {
    x: loc.x,
    y: loc.y,
    z: loc.z,
    dimId,
    startTick: system.currentTick,
  });
  // Cinematic fade so it reads like sleep instead of a status message.
  try {
    player.runCommand("camera @s fade time 0.4 1.5 0.6 color 0 0 0");
  } catch (_) {}
  try {
    player.onScreenDisplay.setTitle("§7Resting…", {
      fadeInDuration: 8,
      stayDuration: 60,
      fadeOutDuration: 12,
      subtitle: "§8Move, sneak, or take damage to wake.",
    });
  } catch (_) {}
  reportSleepProgress(player, /*onEnter*/ true);
}

function leaveTentRest(player, msg) {
  if (!tentRest.delete(player.id)) return;
  if (msg && player) {
    try { player.sendMessage(msg); } catch (_) {}
  }
}

function countSleepers() {
  let bed = 0;
  let tent = 0;
  let online = 0;
  for (const p of world.getAllPlayers()) {
    online++;
    // Vanilla bed sleep: Player.isSleeping is true while they're in a bed.
    // Available since @minecraft/server 1.10+; guarded for safety.
    let sleeping = false;
    try { sleeping = !!p.isSleeping; } catch (_) {}
    if (sleeping) bed++;
    else if (tentRest.has(p.id)) tent++;
  }
  return { bed, tent, online, resting: bed + tent };
}

function getSleepThreshold() {
  // playersSleepingPercentage is a percentage 0-100. 0 means any one player
  // can skip night (vanilla quirk); 100 means everyone must sleep.
  let pct = 100;
  try {
    const v = world.gameRules?.playersSleepingPercentage;
    if (typeof v === "number") pct = v;
  } catch (_) {}
  // 0 in vanilla means "one is enough" — preserve that intent.
  if (pct <= 0) return 1;
  return pct;
}

function requiredSleepers(online, pct) {
  // Standard vanilla rounding: ceil(online * pct / 100), min 1.
  return Math.max(1, Math.ceil((online * pct) / 100));
}

function reportSleepProgress(targetPlayer, onEnter = false) {
  const { bed, tent, online, resting } = countSleepers();
  const pct = getSleepThreshold();
  const need = requiredSleepers(online, pct);
  const remaining = Math.max(0, need - resting);
  const msg = onEnter
    ? `§a[Camping] §7You settle in. §f${resting}§7/§f${need}§7 resting (§f${tent}§7 tent + §f${bed}§7 bed)${remaining ? `. Need §f${remaining}§7 more.` : `.`}`
    : `§7[Sleep] §f${resting}§7/§f${need}§7 resting (§f${tent}§7 tent + §f${bed}§7 bed)`;
  if (onEnter && targetPlayer) {
    try { targetPlayer.sendMessage(msg); } catch (_) {}
  } else {
    // Broadcast a subtle update to everyone currently resting.
    for (const p of world.getAllPlayers()) {
      if (tentRest.has(p.id) || (() => { try { return !!p.isSleeping; } catch (_) { return false; } })()) {
        try { p.sendMessage(msg); } catch (_) {}
      }
    }
  }
}

function awardRestEffects(player) {
  try {
    player.addEffect("regeneration", 200, { amplifier: 1, showParticles: false });
    player.addEffect("saturation", 40, { amplifier: 0, showParticles: false });
  } catch (_) {}
}

function executeNightSkip() {
  // Snapshot tent sleepers before clearing, so we can give them rest perks.
  const tentIds = [...tentRest.keys()];
  tentRest.clear();
  try { world.setTimeOfDay(0); } catch (_) {}
  for (const p of world.getAllPlayers()) {
    if (tentIds.includes(p.id) || p.isSleeping) {
      awardRestEffects(p);
    }
    try { p.runCommand("camera @s clear"); } catch (_) {}
  }
  world.sendMessage("§6[Sleep] §7The camp rests. Dawn breaks.");
}

// ─── Sleep loop: validate tent sleepers, check threshold ────────
system.runInterval(() => {
  if (tentRest.size === 0) return;

  // Validate each tent sleeper still meets the criteria.
  for (const [pid, rest] of [...tentRest.entries()]) {
    const player = world.getAllPlayers().find((p) => p.id === pid);
    if (!player) {
      tentRest.delete(pid);
      continue;
    }
    if (player.dimension.id !== rest.dimId) {
      leaveTentRest(player, "§7[Camping] You wandered out of camp.");
      continue;
    }
    const dx = player.location.x - (rest.x + 0.5);
    const dy = player.location.y - rest.y;
    const dz = player.location.z - (rest.z + 0.5);
    if (dx * dx + dz * dz > 4 || Math.abs(dy) > 2) {
      leaveTentRest(player, "§7[Camping] You wandered out of camp.");
      continue;
    }
    if (player.isSneaking) {
      leaveTentRest(player, "§7[Camping] You climb out of the tent.");
      continue;
    }
    // Sleepers don't get scared off by mobs but a hit cancels rest:
    // (handled implicitly — damage breaks the camera fade and the player is
    // expected to sneak out; we don't have a public hurt event hook here)
    awardRestEffects(player);
  }

  if (tentRest.size === 0) return;
  const tod = world.getTimeOfDay();
  if (!isNight(tod)) {
    // Sun came up some other way — clear resters quietly.
    tentRest.clear();
    return;
  }

  const { online, resting } = countSleepers();
  const pct = getSleepThreshold();
  const need = requiredSleepers(online, pct);
  if (resting >= need) {
    executeNightSkip();
  }
}, SLEEP_TICK_INTERVAL);

const HAMMOCK_ANCHOR_PROP = "camping_hammock_anchor";

function toggleHammock(player, loc) {
  if (player.hasTag(HAMMOCK_TAG)) {
    exitHammock(player);
    return;
  }
  player.addTag(HAMMOCK_TAG);
  const anchor = { x: loc.x + 0.5, y: loc.y + 0.1, z: loc.z + 0.5 };
  try {
    player.setDynamicProperty(HAMMOCK_ANCHOR_PROP, JSON.stringify(anchor));
  } catch (_) {}
  try {
    player.teleport(anchor, { dimension: player.dimension });
  } catch (_) {}
  // Slowness 255 + weakness + mining_fatigue make the player effectively immobile while
  // still conscious; saturation + regen are the "rest" payoff.
  try {
    player.addEffect("slowness",       100000, { amplifier: 255, showParticles: false });
    player.addEffect("weakness",       100000, { amplifier: 255, showParticles: false });
    player.addEffect("mining_fatigue", 100000, { amplifier: 255, showParticles: false });
    player.addEffect("saturation",         40, { amplifier: 0,   showParticles: false });
    player.addEffect("regeneration",      200, { amplifier: 1,   showParticles: false });
  } catch (_) {}
  // Pull the camera back into a cinematic third-person view so the player can see
  // themselves lying in the hammock. Ease in smoothly.
  try {
    player.runCommand("camera @s set minecraft:third_person ease 0.8 out_sine");
  } catch (_) {}
  player.sendMessage("§a[Camping] §7You settle into the hammock. Wild creatures don't notice you. §8(Sneak to climb out.)");
}

function exitHammock(player) {
  player.removeTag(HAMMOCK_TAG);
  try {
    player.removeEffect("slowness");
    player.removeEffect("weakness");
    player.removeEffect("mining_fatigue");
  } catch (_) {}
  // Nudge player one block off the hammock so the next tick doesn't re-teleport them
  // back into the cradle.
  try {
    const raw = player.getDynamicProperty(HAMMOCK_ANCHOR_PROP);
    if (raw && typeof raw === "string") {
      const a = JSON.parse(raw);
      const yaw = player.getRotation().y;
      const rad = (yaw * Math.PI) / 180;
      const dx = -Math.sin(rad);
      const dz = Math.cos(rad);
      player.teleport(
        { x: a.x + dx * 1.2, y: a.y + 0.3, z: a.z + dz * 1.2 },
        { dimension: player.dimension }
      );
    }
  } catch (_) {}
  try { player.setDynamicProperty(HAMMOCK_ANCHOR_PROP, undefined); } catch (_) {}
  try { player.runCommand("camera @s clear"); } catch (_) {}
  player.sendMessage("§7[Camping] You climb out of the hammock.");
}

// ─── Hammock upkeep loop: position lock + mob repulsion + sneak-exit ────────
system.runInterval(() => {
  for (const player of world.getAllPlayers()) {
    if (!player.hasTag(HAMMOCK_TAG)) continue;
    if (player.isSneaking) {
      exitHammock(player);
      continue;
    }
    // Pin the player to the hammock anchor so they can't drift off even with slowness
    try {
      const raw = player.getDynamicProperty(HAMMOCK_ANCHOR_PROP);
      if (raw && typeof raw === "string") {
        const a = JSON.parse(raw);
        const dx = player.location.x - a.x;
        const dy = player.location.y - a.y;
        const dz = player.location.z - a.z;
        if (dx * dx + dy * dy + dz * dz > 0.25) {
          player.teleport(a, { dimension: player.dimension });
        }
      }
    } catch (_) {}
    let hostiles = [];
    try {
      hostiles = player.dimension.getEntities({
        families: ["monster"],
        location: player.location,
        maxDistance: 14,
      });
    } catch (_) {}
    for (const m of hostiles) {
      const dx = m.location.x - player.location.x;
      const dy = m.location.y - player.location.y;
      const dz = m.location.z - player.location.z;
      const d = Math.sqrt(dx * dx + dy * dy + dz * dz);
      if (d > 0.01 && d < 6) {
        const scale = 9 / d;
        const target = {
          x: player.location.x + dx * scale,
          y: m.location.y,
          z: player.location.z + dz * scale,
        };
        try { m.tryTeleport(target, { checkForBlocks: true }); } catch (_) {}
      }
    }
  }
}, 10);

// ─── Break cleanup: break one = pack up the whole structure ─
try {
  world.beforeEvents.playerBreakBlock.subscribe((event) => {
    const block = event.block;
    if (!block) return;
    const id = block.typeId;
    const isTent = TENT_BLOCK_IDS.includes(id);
    if (!isTent && id !== HAMMOCK_BLOCK) return;
    event.cancel = true;
    const loc = { x: block.location.x, y: block.location.y, z: block.location.z };
    const dimId = block.dimension.id;
    const player = event.player;
    if (isTent) {
      system.run(() => dismantleTentAt(loc, dimId, player));
    } else {
      system.run(() => dismantleHammockAt(loc, dimId, player));
    }
  });
} catch (e) {
  console.warn(`[Camping] playerBreakBlock unavailable: ${e}`);
}

function dismantleTentAt(loc, dimId, player) {
  const dim = world.getDimension(dimId);
  let matchedKey = null;
  for (const [k, tent] of Object.entries(state.tents)) {
    const parts = k.split(",");
    if (parts[parts.length - 1] !== dimId) continue;
    if (tent.cells.some(([x, y, z]) => x === loc.x && y === loc.y && z === loc.z)) {
      matchedKey = k;
      break;
    }
  }
  if (!matchedKey) {
    try { dim.runCommand(`setblock ${loc.x} ${loc.y} ${loc.z} air`); } catch (_) {}
    return;
  }
  const tent = state.tents[matchedKey];
  for (const [x, y, z] of tent.cells) {
    try { dim.runCommand(`setblock ${x} ${y} ${z} air`); } catch (_) {}
  }
  delete state.tents[matchedKey];
  saveState();
  try {
    dim.spawnItem(new ItemStack(TENT_ITEM, 1), { x: loc.x + 0.5, y: loc.y + 0.5, z: loc.z + 0.5 });
  } catch (_) {}
  if (player) player.sendMessage("§7[Camping] Tent packed up.");
}

function dismantleHammockAt(loc, dimId, player) {
  const dim = world.getDimension(dimId);
  let matchedKey = null;
  for (const [k, h] of Object.entries(state.hammocks)) {
    if (!k.endsWith("," + dimId)) continue;
    if (h.cells.some(([x, y, z]) => x === loc.x && y === loc.y && z === loc.z)) {
      matchedKey = k;
      break;
    }
  }
  if (!matchedKey) {
    try { dim.runCommand(`setblock ${loc.x} ${loc.y} ${loc.z} air`); } catch (_) {}
    return;
  }
  const h = state.hammocks[matchedKey];
  for (const [x, y, z] of h.cells) {
    try { dim.runCommand(`setblock ${x} ${y} ${z} air`); } catch (_) {}
  }
  delete state.hammocks[matchedKey];
  saveState();
  try {
    dim.spawnItem(new ItemStack(HAMMOCK_ITEM, 1), { x: loc.x + 0.5, y: loc.y + 0.5, z: loc.z + 0.5 });
  } catch (_) {}
  if (player) player.sendMessage("§7[Camping] Hammock taken down.");
}

// ─── Ore Detector ───────────────────────────────────────────
// Three tiered items share one scan handler. Tier → max scan range.
// Faraday: any ore within 4 blocks of a silverlabs:private_chest is hidden
// (mirrors the source DetectOre faraday-cage mechanic, but built into private chests).
const DETECTOR_RANGES = {
  "silverlabs:ore_detector_basic": 8,
  "silverlabs:ore_detector_improved": 16,
  "silverlabs:ore_detector_advanced": 32,
};
const PRIVATE_CHEST_ID = "silverlabs:private_chest";
const FARADAY_RADIUS = 4; // 9³ cube around each ore candidate

const ORE_IDS = new Set([
  "minecraft:coal_ore", "minecraft:deepslate_coal_ore",
  "minecraft:iron_ore", "minecraft:deepslate_iron_ore",
  "minecraft:copper_ore", "minecraft:deepslate_copper_ore",
  "minecraft:gold_ore", "minecraft:deepslate_gold_ore", "minecraft:nether_gold_ore",
  "minecraft:redstone_ore", "minecraft:deepslate_redstone_ore",
  "minecraft:lit_redstone_ore", "minecraft:lit_deepslate_redstone_ore",
  "minecraft:lapis_ore", "minecraft:deepslate_lapis_ore",
  "minecraft:emerald_ore", "minecraft:deepslate_emerald_ore",
  "minecraft:diamond_ore", "minecraft:deepslate_diamond_ore",
  "minecraft:nether_quartz_ore",
  "minecraft:ancient_debris",
]);

function prettyOreName(typeId) {
  const bare = typeId.replace(/^minecraft:/, "")
    .replace(/^lit_/, "")
    .replace(/^deepslate_/, "Deepslate ")
    .replace(/^nether_/, "Nether ");
  return bare.split("_").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
}

function isPrivateChestNearby(dim, cx, cy, cz) {
  for (let dx = -FARADAY_RADIUS; dx <= FARADAY_RADIUS; dx++) {
    for (let dy = -FARADAY_RADIUS; dy <= FARADAY_RADIUS; dy++) {
      for (let dz = -FARADAY_RADIUS; dz <= FARADAY_RADIUS; dz++) {
        const b = dim.getBlock({ x: cx + dx, y: cy + dy, z: cz + dz });
        if (b && b.typeId === PRIVATE_CHEST_ID) return true;
      }
    }
  }
  return false;
}

function runOreScan(player, range) {
  const dim = player.dimension;
  const head = player.getHeadLocation();
  const dir = player.getViewDirection();

  // Walk integer-block steps along the view ray. Resolution 0.5 blocks
  // catches ores the cardinal-aligned steps would skip on diagonals.
  const stepCount = Math.floor(range * 2);
  const seen = new Set();
  let foundOre = null;
  let foundDist = 0;

  for (let i = 1; i <= stepCount; i++) {
    const t = i * 0.5;
    const px = head.x + dir.x * t;
    const py = head.y + dir.y * t;
    const pz = head.z + dir.z * t;
    const bx = Math.floor(px);
    const by = Math.floor(py);
    const bz = Math.floor(pz);
    const key = `${bx},${by},${bz}`;
    if (seen.has(key)) continue;
    seen.add(key);

    let block;
    try { block = dim.getBlock({ x: bx, y: by, z: bz }); } catch (_) { continue; }
    if (!block) continue;

    if (ORE_IDS.has(block.typeId)) {
      if (isPrivateChestNearby(dim, bx, by, bz)) continue; // faraday
      foundOre = block.typeId;
      foundDist = t;
      break;
    }
  }

  if (foundOre) {
    const distRounded = Math.round(foundDist * 10) / 10;
    const pitch = Math.max(0.8, Math.min(2.0, 2.0 - (foundDist / range)));
    try {
      player.playSound("random.orb", { pitch, volume: 0.7 });
    } catch (_) {}
    try {
      player.onScreenDisplay.setActionBar(`§a● §f${prettyOreName(foundOre)} §7at §b${distRounded}m`);
    } catch (_) {}
  } else {
    try {
      player.playSound("note.bass", { pitch: 0.7, volume: 0.5 });
    } catch (_) {}
    try {
      player.onScreenDisplay.setActionBar(`§7○ No ores within §f${range}m`);
    } catch (_) {}
  }
}

world.afterEvents.itemUse.subscribe((event) => {
  const player = event.source;
  const stack = event.itemStack;
  if (!stack || !player) return;
  const range = DETECTOR_RANGES[stack.typeId];
  if (!range) return;
  system.run(() => runOreScan(player, range));
});

// ─── Boot ───────────────────────────────────────────────────
system.run(() => {
  loadState();
  world.sendMessage("§6[Camping] §7Camping Supplies loaded.");
});
