--- a/net/minecraft/world/inventory/ContainerEnchantTable.java
+++ b/net/minecraft/world/inventory/ContainerEnchantTable.java
@@ -27,6 +27,21 @@
 import net.minecraft.world.level.block.BlockEnchantmentTable;
 import net.minecraft.world.level.block.Blocks;
 
+// CraftBukkit start
+import java.util.Map;
+import net.minecraft.world.item.enchantment.Enchantment;
+import org.bukkit.Location;
+import org.bukkit.NamespacedKey;
+import org.bukkit.craftbukkit.inventory.CraftInventoryEnchanting;
+import org.bukkit.craftbukkit.inventory.CraftInventoryView;
+import org.bukkit.craftbukkit.inventory.CraftItemStack;
+import org.bukkit.craftbukkit.util.CraftNamespacedKey;
+import org.bukkit.enchantments.EnchantmentOffer;
+import org.bukkit.event.enchantment.EnchantItemEvent;
+import org.bukkit.event.enchantment.PrepareItemEnchantEvent;
+import org.bukkit.entity.Player;
+// CraftBukkit end
+
 public class ContainerEnchantTable extends Container {
 
     static final MinecraftKey EMPTY_SLOT_LAPIS_LAZULI = new MinecraftKey("item/empty_slot_lapis_lazuli");
@@ -37,6 +52,10 @@
     public final int[] costs;
     public final int[] enchantClue;
     public final int[] levelClue;
+    // CraftBukkit start
+    private CraftInventoryView bukkitEntity = null;
+    private Player player;
+    // CraftBukkit end
 
     public ContainerEnchantTable(int i, PlayerInventory playerinventory) {
         this(i, playerinventory, ContainerAccess.NULL);
@@ -50,6 +69,13 @@
                 super.setChanged();
                 ContainerEnchantTable.this.slotsChanged(this);
             }
+
+            // CraftBukkit start
+            @Override
+            public Location getLocation() {
+                return containeraccess.getLocation();
+            }
+            // CraftBukkit end
         };
         this.random = RandomSource.create();
         this.enchantmentSeed = ContainerProperty.standalone();
@@ -97,6 +123,9 @@
         this.addDataSlot(ContainerProperty.shared(this.levelClue, 0));
         this.addDataSlot(ContainerProperty.shared(this.levelClue, 1));
         this.addDataSlot(ContainerProperty.shared(this.levelClue, 2));
+        // CraftBukkit start
+        player = (Player) playerinventory.player.getBukkitEntity();
+        // CraftBukkit end
     }
 
     @Override
@@ -104,7 +133,7 @@
         if (iinventory == this.enchantSlots) {
             ItemStack itemstack = iinventory.getItem(0);
 
-            if (!itemstack.isEmpty() && itemstack.isEnchantable()) {
+            if (!itemstack.isEmpty()) { // CraftBukkit - relax condition
                 this.access.execute((world, blockposition) -> {
                     int i = 0;
                     Iterator iterator = BlockEnchantmentTable.BOOKSHELF_OFFSETS.iterator();
@@ -143,6 +172,41 @@
                         }
                     }
 
+                    // CraftBukkit start
+                    CraftItemStack item = CraftItemStack.asCraftMirror(itemstack);
+                    org.bukkit.enchantments.EnchantmentOffer[] offers = new EnchantmentOffer[3];
+                    for (j = 0; j < 3; ++j) {
+                        org.bukkit.enchantments.Enchantment enchantment = (this.enchantClue[j] >= 0) ? org.bukkit.enchantments.Enchantment.getByKey(CraftNamespacedKey.fromMinecraft(BuiltInRegistries.ENCHANTMENT.getKey(BuiltInRegistries.ENCHANTMENT.byId(this.enchantClue[j])))) : null;
+                        offers[j] = (enchantment != null) ? new EnchantmentOffer(enchantment, this.levelClue[j], this.costs[j]) : null;
+                    }
+
+                    PrepareItemEnchantEvent event = new PrepareItemEnchantEvent(player, this.getBukkitView(), access.getLocation().getBlock(), item, offers, i);
+                    event.setCancelled(!itemstack.isEnchantable());
+                    world.getCraftServer().getPluginManager().callEvent(event);
+
+                    if (event.isCancelled()) {
+                        for (j = 0; j < 3; ++j) {
+                            this.costs[j] = 0;
+                            this.enchantClue[j] = -1;
+                            this.levelClue[j] = -1;
+                        }
+                        return;
+                    }
+
+                    for (j = 0; j < 3; j++) {
+                        EnchantmentOffer offer = event.getOffers()[j];
+                        if (offer != null) {
+                            this.costs[j] = offer.getCost();
+                            this.enchantClue[j] = BuiltInRegistries.ENCHANTMENT.getId(BuiltInRegistries.ENCHANTMENT.get(CraftNamespacedKey.toMinecraft(offer.getEnchantment().getKey())));
+                            this.levelClue[j] = offer.getEnchantmentLevel();
+                        } else {
+                            this.costs[j] = 0;
+                            this.enchantClue[j] = -1;
+                            this.levelClue[j] = -1;
+                        }
+                    }
+                    // CraftBukkit end
+
                     this.broadcastChanges();
                 });
             } else {
@@ -170,8 +234,25 @@
                     ItemStack itemstack2 = itemstack;
                     List<WeightedRandomEnchant> list = this.getEnchantmentList(itemstack, i, this.costs[i]);
 
-                    if (!list.isEmpty()) {
-                        entityhuman.onEnchantmentPerformed(itemstack, j);
+                    // CraftBukkit start
+                    if (true || !list.isEmpty()) {
+                        // entityhuman.onEnchantmentPerformed(itemstack, j); // Moved down
+                        Map<org.bukkit.enchantments.Enchantment, Integer> enchants = new java.util.HashMap<org.bukkit.enchantments.Enchantment, Integer>();
+                        for (WeightedRandomEnchant instance : list) {
+                            enchants.put(org.bukkit.enchantments.Enchantment.getByKey(CraftNamespacedKey.fromMinecraft(BuiltInRegistries.ENCHANTMENT.getKey(instance.enchantment))), instance.level);
+                        }
+                        CraftItemStack item = CraftItemStack.asCraftMirror(itemstack2);
+
+                        org.bukkit.enchantments.Enchantment hintedEnchantment = org.bukkit.enchantments.Enchantment.getByKey(CraftNamespacedKey.fromMinecraft(BuiltInRegistries.ENCHANTMENT.getKey(Enchantment.byId(enchantClue[i]))));
+                        int hintedEnchantmentLevel = levelClue[i];
+                        EnchantItemEvent event = new EnchantItemEvent((Player) entityhuman.getBukkitEntity(), this.getBukkitView(), access.getLocation().getBlock(), item, this.costs[i], enchants, hintedEnchantment, hintedEnchantmentLevel, i);
+                        world.getCraftServer().getPluginManager().callEvent(event);
+
+                        int level = event.getExpLevelCost();
+                        if (event.isCancelled() || (level > entityhuman.experienceLevel && !entityhuman.getAbilities().instabuild) || event.getEnchantsToAdd().isEmpty()) {
+                            return;
+                        }
+                        // CraftBukkit end
                         boolean flag = itemstack.is(Items.BOOK);
 
                         if (flag) {
@@ -185,18 +266,30 @@
                             this.enchantSlots.setItem(0, itemstack2);
                         }
 
-                        Iterator iterator = list.iterator();
-
-                        while (iterator.hasNext()) {
-                            WeightedRandomEnchant weightedrandomenchant = (WeightedRandomEnchant) iterator.next();
+                        // CraftBukkit start
+                        for (Map.Entry<org.bukkit.enchantments.Enchantment, Integer> entry : event.getEnchantsToAdd().entrySet()) {
+                            try {
+                                if (flag) {
+                                    NamespacedKey enchantId = entry.getKey().getKey();
+                                    Enchantment nms = BuiltInRegistries.ENCHANTMENT.get(CraftNamespacedKey.toMinecraft(enchantId));
+                                    if (nms == null) {
+                                        continue;
+                                    }
 
-                            if (flag) {
-                                ItemEnchantedBook.addEnchantment(itemstack2, weightedrandomenchant);
-                            } else {
-                                itemstack2.enchant(weightedrandomenchant.enchantment, weightedrandomenchant.level);
+                                    WeightedRandomEnchant weightedrandomenchant = new WeightedRandomEnchant(nms, entry.getValue());
+                                    ItemEnchantedBook.addEnchantment(itemstack2, weightedrandomenchant);
+                                } else {
+                                    item.addUnsafeEnchantment(entry.getKey(), entry.getValue());
+                                }
+                            } catch (IllegalArgumentException e) {
+                                /* Just swallow invalid enchantments */
                             }
                         }
 
+                        entityhuman.onEnchantmentPerformed(itemstack, j);
+                        // CraftBukkit end
+
+                        // CraftBukkit - TODO: let plugins change this
                         if (!entityhuman.getAbilities().instabuild) {
                             itemstack1.shrink(j);
                             if (itemstack1.isEmpty()) {
@@ -259,6 +352,7 @@
 
     @Override
     public boolean stillValid(EntityHuman entityhuman) {
+        if (!this.checkReachable) return true; // CraftBukkit
         return stillValid(this.access, entityhuman, Blocks.ENCHANTING_TABLE);
     }
 
@@ -309,4 +403,17 @@
 
         return itemstack;
     }
+
+    // CraftBukkit start
+    @Override
+    public CraftInventoryView getBukkitView() {
+        if (bukkitEntity != null) {
+            return bukkitEntity;
+        }
+
+        CraftInventoryEnchanting inventory = new CraftInventoryEnchanting(this.enchantSlots);
+        bukkitEntity = new CraftInventoryView(this.player, inventory, this);
+        return bukkitEntity;
+    }
+    // CraftBukkit end
 }
