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

// ─── Constants ──────────────────────────────────────────────
const MAILBOX_BLOCK = "silverlabs:mailbox";
const POST_OFFICE_BLOCK = "silverlabs:post_office";
const VANILLA_CHEST = "minecraft:chest";
const PROP_KEY_V1 = "postal_state_v1";
const PROP_KEY_V2 = "postal_state_v2";
const MAX_MAILBOXES_PER_PLAYER = 5;
const LABEL_MAX_LEN = 24;

// ─── State ──────────────────────────────────────────────────
// mailboxes: { "x,y,z,dim": { ownerId, ownerName, mailboxId, label } }
// registry:  { [ownerId]: { name, mailboxes: [{ mailboxId, label, x, y, z, dim }] } }
// pending:   { [ownerId]: [{ from, itemSummary, mailboxLabel, ts }] }
let state = { mailboxes: {}, registry: {}, pending: {} };

function uuid() {
  return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
    const r = (Math.random() * 16) | 0;
    const v = c === "x" ? r : (r & 0x3) | 0x8;
    return v.toString(16);
  });
}

function loadState() {
  // Try v2 first
  try {
    const rawV2 = world.getDynamicProperty(PROP_KEY_V2);
    if (rawV2 && typeof rawV2 === "string") {
      const parsed = JSON.parse(rawV2);
      state = {
        mailboxes: parsed.mailboxes || {},
        registry: parsed.registry || {},
        pending: parsed.pending || {},
      };
      return;
    }
  } catch (e) {
    world.sendMessage(`§c[Postal] Failed to load v2 state: ${e.message}`);
  }

  // One-shot v1 → v2 migration
  try {
    const rawV1 = world.getDynamicProperty(PROP_KEY_V1);
    if (rawV1 && typeof rawV1 === "string") {
      const v1 = JSON.parse(rawV1);
      const next = { mailboxes: {}, registry: {}, pending: v1.pending || {} };

      // Each v1 mailbox entry → v2 entry with synthetic mailboxId + default label
      for (const [k, entry] of Object.entries(v1.mailboxes || {})) {
        next.mailboxes[k] = {
          ownerId: entry.ownerId,
          ownerName: entry.ownerName,
          mailboxId: uuid(),
          label: "Mailbox",
        };
      }

      // v1 registry shape: { [ownerId]: { name, x, y, z, dim } }
      for (const [ownerId, reg] of Object.entries(v1.registry || {})) {
        const k = `${reg.x},${reg.y},${reg.z},${reg.dim}`;
        const mb = next.mailboxes[k];
        if (!mb) continue; // orphaned
        next.registry[ownerId] = {
          name: reg.name,
          mailboxes: [
            {
              mailboxId: mb.mailboxId,
              label: "Mailbox",
              x: reg.x,
              y: reg.y,
              z: reg.z,
              dim: reg.dim,
            },
          ],
        };
      }

      state = next;
      saveState();
      try {
        world.setDynamicProperty(PROP_KEY_V1, undefined);
      } catch (_) {}
      world.sendMessage(`§6[Postal] §7Migrated mail data to v2 (multi-mailbox).`);
      return;
    }
  } catch (e) {
    world.sendMessage(`§c[Postal] Migration failed: ${e.message}`);
  }

  state = { mailboxes: {}, registry: {}, pending: {} };
}

function saveState() {
  try {
    world.setDynamicProperty(PROP_KEY_V2, JSON.stringify(state));
  } catch (e) {
    world.sendMessage(`§c[Postal] Failed to save state: ${e.message}`);
  }
}

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

function getMailboxOwner(loc, dimensionId) {
  return state.mailboxes[keyOf(loc, dimensionId)] || null;
}

function claimMailbox(loc, dimensionId, player, label) {
  const k = keyOf(loc, dimensionId);
  const mailboxId = uuid();
  state.mailboxes[k] = {
    ownerId: player.id,
    ownerName: player.name,
    mailboxId,
    label,
  };
  if (!state.registry[player.id]) {
    state.registry[player.id] = { name: player.name, mailboxes: [] };
  }
  state.registry[player.id].name = player.name;
  state.registry[player.id].mailboxes.push({
    mailboxId,
    label,
    x: Math.floor(loc.x),
    y: Math.floor(loc.y),
    z: Math.floor(loc.z),
    dim: dimensionId,
  });
  saveState();
  return mailboxId;
}

function renameMailbox(loc, dimensionId, label) {
  const k = keyOf(loc, dimensionId);
  const entry = state.mailboxes[k];
  if (!entry) return;
  entry.label = label;
  const reg = state.registry[entry.ownerId];
  if (reg) {
    const m = reg.mailboxes.find((m) => m.mailboxId === entry.mailboxId);
    if (m) m.label = label;
  }
  saveState();
}

function releaseMailbox(loc, dimensionId) {
  const k = keyOf(loc, dimensionId);
  const entry = state.mailboxes[k];
  delete state.mailboxes[k];
  if (entry && state.registry[entry.ownerId]) {
    const reg = state.registry[entry.ownerId];
    reg.mailboxes = reg.mailboxes.filter((m) => m.mailboxId !== entry.mailboxId);
    if (reg.mailboxes.length === 0) {
      delete state.registry[entry.ownerId];
    }
  }
  saveState();
}

function countMailboxes(playerId) {
  return state.registry[playerId]?.mailboxes?.length || 0;
}

function getPlayerMailboxes(playerId) {
  return state.registry[playerId]?.mailboxes || [];
}

// ─── Chest facing from player rotation ──────────────────────
function chestFacing(yaw) {
  let y = yaw;
  while (y > 180) y -= 360;
  while (y < -180) y += 360;
  if (y >= -45 && y < 45) return "north";
  if (y >= 45 && y < 135) return "east";
  if (y >= -135 && y < -45) return "west";
  return "south";
}

// ─── Mailbox placement ──────────────────────────────────────
world.afterEvents.playerPlaceBlock.subscribe((event) => {
  const block = event.block;
  if (block.typeId !== MAILBOX_BLOCK) return;

  const player = event.player;
  const loc = block.location;
  const dim = block.dimension;

  const currentCount = countMailboxes(player.id);
  if (currentCount >= MAX_MAILBOXES_PER_PLAYER) {
    try {
      dim.runCommand(`setblock ${loc.x} ${loc.y} ${loc.z} air`);
      dim.spawnItem(new ItemStack(MAILBOX_BLOCK, 1), {
        x: loc.x + 0.5,
        y: loc.y + 0.5,
        z: loc.z + 0.5,
      });
    } catch (_) {}
    player.sendMessage(
      `§c[Postal] §7Max of ${MAX_MAILBOXES_PER_PLAYER} mailboxes per player — break one first.`
    );
    return;
  }

  const facing = chestFacing(player.getRotation().y);
  try {
    dim.runCommand(
      `setblock ${loc.x} ${loc.y} ${loc.z} chest ["minecraft:cardinal_direction":"${facing}"]`
    );
  } catch (_) {
    try {
      dim.runCommand(`setblock ${loc.x} ${loc.y} ${loc.z} chest`);
    } catch (_) {}
  }

  const defaultLabel = `Mailbox ${currentCount + 1}`;
  claimMailbox(loc, dim.id, player, defaultLabel);

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

async function promptForLabel(player, loc, dimId, defaultLabel) {
  const form = new ModalFormData()
    .title("Name this Mailbox")
    .textField("Label", "e.g. Home, Mine, Base", defaultLabel);

  let res;
  try {
    res = await form.show(player);
  } catch (_) {
    return;
  }
  if (res.canceled) {
    player.sendMessage(`§6[Postal] §7Mailbox locked to you as §d${defaultLabel}§7.`);
    return;
  }

  const raw = (res.formValues && res.formValues[0]) || "";
  const label = String(raw).trim().slice(0, LABEL_MAX_LEN) || defaultLabel;
  renameMailbox(loc, dimId, label);
  player.sendMessage(`§6[Postal] §7Mailbox locked to you as §d${label}§7.`);
}

// ─── Interact: gate mailbox opening for non-owners ──────────
world.beforeEvents.playerInteractWithBlock.subscribe((event) => {
  const block = event.block;
  if (!block) return;

  // Post-office block: open menu (cancel default interact)
  if (block.typeId === POST_OFFICE_BLOCK) {
    event.cancel = true;
    const player = event.player;
    system.run(() => openPostOfficeMenu(player));
    return;
  }

  // Vanilla chest: gate if it's a claimed mailbox
  if (block.typeId !== VANILLA_CHEST) return;
  const owner = getMailboxOwner(block.location, block.dimension.id);
  if (!owner) return;
  if (owner.ownerId === event.player.id) return;
  event.cancel = true;
  const playerRef = event.player;
  system.run(() =>
    playerRef.sendMessage(
      `§c[Postal] §7This mailbox belongs to §f${owner.ownerName}§7.`
    )
  );
});

// ─── Break: protect mailboxes; owner break returns custom item ──
world.beforeEvents.playerBreakBlock.subscribe((event) => {
  const block = event.block;
  if (block.typeId !== VANILLA_CHEST) return;
  const owner = getMailboxOwner(block.location, block.dimension.id);
  if (!owner) return;

  const player = event.player;

  if (owner.ownerId !== player.id) {
    event.cancel = true;
    system.run(() =>
      player.sendMessage(
        `§c[Postal] §7This mailbox belongs to §f${owner.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 = owner.label;

  system.run(() => {
    try {
      const inv = dim.getBlock(loc)?.getComponent("inventory");
      const container = inv?.container;
      const dropPos = { x: loc.x + 0.5, y: loc.y + 0.5, z: loc.z + 0.5 };
      if (container) {
        for (let i = 0; i < container.size; i++) {
          const item = container.getItem(i);
          if (item) {
            dim.spawnItem(item, dropPos);
            container.setItem(i, undefined);
          }
        }
      }
      dim.spawnItem(new ItemStack(MAILBOX_BLOCK, 1), dropPos);
      dim.runCommand(`setblock ${loc.x} ${loc.y} ${loc.z} air`);
    } catch (e) {
      player.sendMessage(`§c[Postal] Error during break: ${e.message}`);
    }
    releaseMailbox(loc, dim.id);
    player.sendMessage(`§6[Postal] §7Removed §d${label}§7.`);
  });
});

// ─── Post Office: root menu (router) ────────────────────────
async function openPostOfficeMenu(player) {
  const myCount = countMailboxes(player.id);

  // No mailboxes of their own → no redirect option, jump straight to send
  if (myCount === 0) {
    return openSendForm(player);
  }

  const form = new ActionFormData()
    .title("Post Office")
    .body("What would you like to do?");
  form.button("§fSend Mail\n§7Deliver an item to another player");
  form.button("§fRedirect Mailbox\n§7Move one of your mailboxes' contents into another");
  form.button("§cClose");

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

// ─── Held item helpers ──────────────────────────────────────
function getHeldItem(player) {
  try {
    const inv = player.getComponent("inventory")?.container;
    if (!inv) return { item: undefined, slot: -1 };
    const slot = player.selectedSlotIndex;
    const item = inv.getItem(slot);
    return { item, slot, inv };
  } catch (_) {
    return { item: undefined, slot: -1 };
  }
}

function itemSummary(item) {
  const niceName = item.typeId.replace(/^minecraft:/, "").replace(/_/g, " ");
  return `${item.amount} × ${niceName}`;
}

// ─── Send form: Stage A (recipient) ─────────────────────────
async function openSendForm(player) {
  const { item, slot, inv } = getHeldItem(player);
  if (!item) {
    player.sendMessage(
      `§c[Postal] §7Hold the item you want to send in your hotbar first.`
    );
    return;
  }

  const candidates = [];
  for (const [ownerId, reg] of Object.entries(state.registry)) {
    if (ownerId === player.id) continue;
    if (!reg.mailboxes || reg.mailboxes.length === 0) continue;
    candidates.push({ ownerId, reg });
  }

  if (candidates.length === 0) {
    player.sendMessage(`§c[Postal] §7No other players have claimed a mailbox yet.`);
    return;
  }

  const form = new ActionFormData()
    .title("Post Office — Recipient")
    .body(`Sending §f${itemSummary(item)}§r\n\nChoose a recipient:`);
  for (const c of candidates) {
    const n = c.reg.mailboxes.length;
    form.button(`§f${c.reg.name}\n§7${n} mailbox${n === 1 ? "" : "es"}`);
  }
  form.button("§cCancel");

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

  const chosen = candidates[res.selection];

  // Single mailbox → skip Stage B
  if (chosen.reg.mailboxes.length === 1) {
    return confirmAndSend(player, chosen, chosen.reg.mailboxes[0], item, slot, inv);
  }

  return openMailboxPicker(player, chosen, item, slot, inv);
}

// ─── Send form: Stage B (mailbox) ───────────────────────────
async function openMailboxPicker(player, chosen, item, slot, inv) {
  const form = new ActionFormData()
    .title(`Send to ${chosen.reg.name}`)
    .body(`Sending §f${itemSummary(item)}§r\n\nChoose which mailbox:`);
  for (const m of chosen.reg.mailboxes) {
    form.button(`§f${m.label}\n§7${m.x}, ${m.y}, ${m.z}`);
  }
  form.button("§cBack");

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

  const mailbox = chosen.reg.mailboxes[res.selection];
  return confirmAndSend(player, chosen, mailbox, item, slot, inv);
}

// ─── Send form: Stage C (confirm) ───────────────────────────
async function confirmAndSend(player, chosen, mailbox, item, slot, inv) {
  const confirm = new MessageFormData()
    .title("Confirm Send")
    .body(
      `Send §f${itemSummary(item)}§r\nto §f${chosen.reg.name}§r's §d${mailbox.label}§r?`
    )
    .button1("Send")
    .button2("Cancel");

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

  // Re-fetch held item in case it changed while form was open
  const fresh = getHeldItem(player);
  if (
    !fresh.item ||
    fresh.item.typeId !== item.typeId ||
    fresh.item.amount !== item.amount
  ) {
    player.sendMessage(`§c[Postal] §7Held item changed — send cancelled.`);
    return;
  }

  deliver(
    player,
    chosen.ownerId,
    chosen.reg.name,
    mailbox,
    fresh.item,
    fresh.slot,
    fresh.inv
  );
}

// ─── Delivery ───────────────────────────────────────────────
function deliver(
  senderPlayer,
  recipientId,
  recipientName,
  mailbox,
  itemStack,
  slot,
  inv,
  retry = 0
) {
  const dim = world.getDimension(mailbox.dim);
  const loc = { x: mailbox.x, y: mailbox.y, z: mailbox.z };
  let block;
  try {
    block = dim.getBlock(loc);
  } catch (_) {
    block = undefined;
  }

  if (!block) {
    if (retry >= 1) {
      senderPlayer.sendMessage(
        `§c[Postal] §7Couldn't reach §f${recipientName}§7's §d${mailbox.label}§7 (chunk not loaded). Try again later.`
      );
      return;
    }
    const taName = `postal_d_${(mailbox.mailboxId || "").slice(0, 8)}`.replace(
      /[^a-zA-Z0-9_]/g,
      "_"
    );
    try {
      dim.runCommand(
        `tickingarea add ${loc.x} ${loc.y} ${loc.z} ${loc.x} ${loc.y} ${loc.z} ${taName}`
      );
    } catch (_) {}
    system.runTimeout(() => {
      deliver(
        senderPlayer,
        recipientId,
        recipientName,
        mailbox,
        itemStack,
        slot,
        inv,
        retry + 1
      );
      try {
        dim.runCommand(`tickingarea remove ${taName}`);
      } catch (_) {}
    }, 20);
    return;
  }

  if (block.typeId !== VANILLA_CHEST) {
    releaseMailbox(loc, mailbox.dim);
    senderPlayer.sendMessage(
      `§c[Postal] §f${recipientName}§7's §d${mailbox.label}§7 is missing. Send cancelled.`
    );
    return;
  }

  const owner = getMailboxOwner(loc, mailbox.dim);
  if (!owner || owner.ownerId !== recipientId) {
    releaseMailbox(loc, mailbox.dim);
    senderPlayer.sendMessage(
      `§c[Postal] §f${recipientName}§7's §d${mailbox.label}§7 is no longer claimed. Send cancelled.`
    );
    return;
  }

  const container = block.getComponent("inventory")?.container;
  if (!container) {
    senderPlayer.sendMessage(`§c[Postal] §7Couldn't access the mailbox contents.`);
    return;
  }

  const leftover = container.addItem(itemStack);

  try {
    inv.setItem(slot, undefined);
  } catch (_) {}

  if (leftover) {
    try {
      senderPlayer.dimension.spawnItem(leftover, senderPlayer.location);
    } catch (_) {}
    senderPlayer.sendMessage(
      `§c[Postal] §f${recipientName}§7's §d${mailbox.label}§7 is full — partial delivery. Leftover dropped at your feet.`
    );
  }

  const summary = itemSummary(itemStack);
  senderPlayer.sendMessage(
    `§6[Postal] §7Sent §f${summary}§7 to §f${recipientName}§7's §d${mailbox.label}§7.`
  );

  const onlineRecipient = world.getAllPlayers().find((p) => p.id === recipientId);
  if (onlineRecipient) {
    onlineRecipient.sendMessage(
      `§6[Postal] §7You've got mail from §f${senderPlayer.name}§7 in §d${mailbox.label}§7! (${summary})`
    );
  } else {
    if (!state.pending[recipientId]) state.pending[recipientId] = [];
    state.pending[recipientId].push({
      from: senderPlayer.name,
      itemSummary: summary,
      mailboxLabel: mailbox.label,
      ts: Date.now(),
    });
    saveState();
  }
}

// ─── Redirect: Stage 1 (source) ─────────────────────────────
async function openRedirectForm(player) {
  const myMailboxes = getPlayerMailboxes(player.id);
  if (myMailboxes.length < 2) {
    player.sendMessage(`§c[Postal] §7You need at least two mailboxes to redirect.`);
    return;
  }

  const form = new ActionFormData()
    .title("Redirect — Source")
    .body("Choose the mailbox to empty and remove:");
  for (const m of myMailboxes) {
    form.button(`§f${m.label}\n§7${m.x}, ${m.y}, ${m.z}`);
  }
  form.button("§cCancel");

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

  const source = myMailboxes[res.selection];
  return redirectStep2(player, source);
}

// ─── Redirect: Stage 2 (destination) ────────────────────────
async function redirectStep2(player, source) {
  const dests = getPlayerMailboxes(player.id).filter(
    (m) => m.mailboxId !== source.mailboxId
  );
  if (dests.length === 0) {
    player.sendMessage(`§c[Postal] §7No other mailboxes to redirect into.`);
    return;
  }

  const form = new ActionFormData()
    .title("Redirect — Destination")
    .body(`Source: §d${source.label}§r\n\nChoose where to move its contents:`);
  for (const m of dests) {
    form.button(`§f${m.label}\n§7${m.x}, ${m.y}, ${m.z}`);
  }
  form.button("§cBack");

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

  const dest = dests[res.selection];
  return redirectConfirm(player, source, dest);
}

// ─── Redirect: Stage 3 (confirm) ────────────────────────────
async function redirectConfirm(player, source, dest) {
  const confirm = new MessageFormData()
    .title("Confirm Redirect")
    .body(
      `Redirect §d${source.label}§r → §d${dest.label}§r?\n\n§7The source mailbox will be destroyed and all its contents moved to the destination.`
    )
    .button1("Redirect")
    .button2("Cancel");

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

  executeRedirect(player, source, dest);
}

// ─── Redirect: execute ──────────────────────────────────────
function executeRedirect(player, source, dest, retry = 0) {
  const sDim = world.getDimension(source.dim);
  const dDim = world.getDimension(dest.dim);
  const sLoc = { x: source.x, y: source.y, z: source.z };
  const dLoc = { x: dest.x, y: dest.y, z: dest.z };

  let sBlock, dBlock;
  try {
    sBlock = sDim.getBlock(sLoc);
  } catch (_) {}
  try {
    dBlock = dDim.getBlock(dLoc);
  } catch (_) {}

  if (!sBlock || !dBlock) {
    if (retry >= 1) {
      player.sendMessage(
        `§c[Postal] §7Couldn't reach mailbox chunks. Try again later.`
      );
      return;
    }
    const taS = `postal_rs_${(source.mailboxId || "").slice(0, 6)}`.replace(
      /[^a-zA-Z0-9_]/g,
      "_"
    );
    const taD = `postal_rd_${(dest.mailboxId || "").slice(0, 6)}`.replace(
      /[^a-zA-Z0-9_]/g,
      "_"
    );
    try {
      sDim.runCommand(
        `tickingarea add ${sLoc.x} ${sLoc.y} ${sLoc.z} ${sLoc.x} ${sLoc.y} ${sLoc.z} ${taS}`
      );
    } catch (_) {}
    try {
      dDim.runCommand(
        `tickingarea add ${dLoc.x} ${dLoc.y} ${dLoc.z} ${dLoc.x} ${dLoc.y} ${dLoc.z} ${taD}`
      );
    } catch (_) {}
    system.runTimeout(() => {
      executeRedirect(player, source, dest, retry + 1);
      try {
        sDim.runCommand(`tickingarea remove ${taS}`);
      } catch (_) {}
      try {
        dDim.runCommand(`tickingarea remove ${taD}`);
      } catch (_) {}
    }, 20);
    return;
  }

  if (sBlock.typeId !== VANILLA_CHEST) {
    releaseMailbox(sLoc, source.dim);
    player.sendMessage(
      `§c[Postal] §7Source mailbox §d${source.label}§7 is missing. Aborted.`
    );
    return;
  }
  if (dBlock.typeId !== VANILLA_CHEST) {
    releaseMailbox(dLoc, dest.dim);
    player.sendMessage(
      `§c[Postal] §7Destination mailbox §d${dest.label}§7 is missing. Aborted.`
    );
    return;
  }

  const sOwner = getMailboxOwner(sLoc, source.dim);
  const dOwner = getMailboxOwner(dLoc, dest.dim);
  if (!sOwner || sOwner.ownerId !== player.id) {
    player.sendMessage(`§c[Postal] §7You no longer own the source mailbox. Aborted.`);
    return;
  }
  if (!dOwner || dOwner.ownerId !== player.id) {
    player.sendMessage(
      `§c[Postal] §7You no longer own the destination mailbox. Aborted.`
    );
    return;
  }

  const sContainer = sBlock.getComponent("inventory")?.container;
  const dContainer = dBlock.getComponent("inventory")?.container;
  if (!sContainer || !dContainer) {
    player.sendMessage(`§c[Postal] §7Couldn't access mailbox contents.`);
    return;
  }

  const leftovers = [];
  for (let i = 0; i < sContainer.size; i++) {
    const it = sContainer.getItem(i);
    if (!it) continue;
    const lo = dContainer.addItem(it);
    if (lo) leftovers.push(lo);
    sContainer.setItem(i, undefined);
  }

  // Destroy source block (no item drop — pure consolidation)
  try {
    sDim.runCommand(`setblock ${sLoc.x} ${sLoc.y} ${sLoc.z} air`);
  } catch (_) {}
  releaseMailbox(sLoc, source.dim);

  if (leftovers.length) {
    for (const lo of leftovers) {
      try {
        player.dimension.spawnItem(lo, player.location);
      } catch (_) {}
    }
    player.sendMessage(
      `§e[Postal] §7Destination was full — leftovers dropped at your feet.`
    );
  }

  player.sendMessage(
    `§6[Postal] §7Redirected §d${source.label}§7 → §d${dest.label}§7.`
  );
}

// ─── Flush pending notifications on login ───────────────────
world.afterEvents.playerSpawn.subscribe((event) => {
  if (!event.initialSpawn) return;
  const player = event.player;
  const queue = state.pending[player.id];
  if (!queue || queue.length === 0) return;

  system.runTimeout(() => {
    player.sendMessage(
      `§6[Postal] §7You have §f${queue.length}§7 new mail notification(s):`
    );
    for (const n of queue) {
      const when = new Date(n.ts).toISOString().replace("T", " ").slice(0, 16);
      const where = n.mailboxLabel ? ` in §d${n.mailboxLabel}§7` : "";
      player.sendMessage(
        `§7[${when}] §7from §f${n.from}§7${where} — ${n.itemSummary}`
      );
    }
    delete state.pending[player.id];
    saveState();
  }, 40);
});

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