package com.tiedup.remake.v2.furniture;

import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.items.ItemMasterKey;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.CollarHelper;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import org.jetbrains.annotations.Nullable;

/**
 * Shared authorization predicate for furniture packets and in-world interactions.
 *
 * <p>The 2026-04-17 audit found three divergent code paths enforcing different
 * subsets of the intended security model:</p>
 * <ul>
 *   <li>{@code EntityFurniture.interact()} force-mount path — all checks present</li>
 *   <li>{@code PacketFurnitureForcemount.handleOnServer} — missing
 *       {@code isTiedUp} and {@code !isPassenger} checks</li>
 *   <li>{@code EntityFurniture.interact()} lock path and
 *       {@code PacketFurnitureLock.handleOnServer} — missing collar-ownership
 *       enforcement</li>
 * </ul>
 *
 * <p>This class unifies the model. The security rule is:</p>
 * <ol>
 *   <li><b>Lock/unlock</b>: sender holds a master key, seat exists and is
 *       lockable, seat is occupied, and the occupant wears a collar owned by
 *       the sender (or the sender has OP permissions). A seat whose occupant
 *       has no collar cannot be locked by anyone other than an OP.</li>
 *   <li><b>Force-mount</b>: captive is alive, within range, wears a collar
 *       owned by the sender (or sender is OP), is currently tied-up (leashed),
 *       and is not already a passenger of some other entity.</li>
 * </ol>
 *
 * <p>The boolean core methods are package-private to allow pure-logic unit
 * testing without a Minecraft runtime.</p>
 */
public final class FurnitureAuthPredicate {

    /** Max interaction range from sender to furniture/captive (blocks). */
    public static final double INTERACTION_RANGE = 5.0;

    private FurnitureAuthPredicate() {}

    // ============================================================
    // Pure-logic core (unit-testable with booleans)
    // ============================================================

    /**
     * Pure-logic authorization for a lock/unlock action. All parameters are
     * pre-extracted from Minecraft entities by {@link #canLockUnlock}.
     */
    static boolean isAuthorizedForLock(
        boolean senderHasKey,
        boolean seatExistsAndLockable,
        boolean seatOccupied,
        boolean occupantHasOwnedCollar
    ) {
        return (
            senderHasKey &&
            seatExistsAndLockable &&
            seatOccupied &&
            occupantHasOwnedCollar
        );
    }

    /**
     * Pure-logic authorization for a force-mount action. All parameters are
     * pre-extracted from Minecraft entities by {@link #canForceMount}.
     */
    static boolean isAuthorizedForForceMount(
        boolean captiveAlive,
        boolean captiveWithinRange,
        boolean captiveHasOwnedCollar,
        boolean captiveIsTiedUp,
        boolean captiveIsNotPassenger
    ) {
        return (
            captiveAlive &&
            captiveWithinRange &&
            captiveHasOwnedCollar &&
            captiveIsTiedUp &&
            captiveIsNotPassenger
        );
    }

    // ============================================================
    // MC-aware wrappers (call sites use these)
    // ============================================================

    /**
     * Check whether {@code sender} may toggle the lock state of the given seat.
     * Also runs proximity / liveness gates on the furniture entity itself.
     * Logs a DEBUG reason on denial.
     */
    public static boolean canLockUnlock(
        ServerPlayer sender,
        Entity furniture,
        String seatId
    ) {
        if (sender == null || furniture == null || seatId == null) {
            return false;
        }
        if (!furniture.isAlive() || furniture.isRemoved()) {
            debug(
                "denied: furniture removed/dead (id={})",
                furniture.getId()
            );
            return false;
        }
        if (sender.distanceTo(furniture) > INTERACTION_RANGE) {
            debug(
                "denied: out of range ({} blocks)",
                sender.distanceTo(furniture)
            );
            return false;
        }
        if (!(furniture instanceof ISeatProvider provider)) {
            debug("denied: entity is not a seat provider");
            return false;
        }

        boolean senderHasKey = holdsMasterKey(sender);
        boolean seatExistsAndLockable = seatIsLockable(provider, seatId);
        LivingEntity occupant = findOccupant(furniture, seatId);
        boolean seatOccupied = occupant != null;
        boolean occupantHasOwnedCollar = occupantHasOwnedCollarFor(
            occupant,
            sender
        );

        boolean authorized = isAuthorizedForLock(
            senderHasKey,
            seatExistsAndLockable,
            seatOccupied,
            occupantHasOwnedCollar
        );

        if (!authorized) {
            debug(
                "lock denied: sender={}, key={}, lockable={}, occupied={}, ownedCollar={}",
                sender.getName().getString(),
                senderHasKey,
                seatExistsAndLockable,
                seatOccupied,
                occupantHasOwnedCollar
            );
        }
        return authorized;
    }

    /**
     * Check whether {@code sender} may force-mount {@code captive} onto
     * {@code furniture}. Logs a DEBUG reason on denial.
     */
    public static boolean canForceMount(
        ServerPlayer sender,
        Entity furniture,
        LivingEntity captive
    ) {
        if (sender == null || furniture == null || captive == null) {
            return false;
        }
        if (!furniture.isAlive() || furniture.isRemoved()) return false;
        // Entity.distanceTo ignores dimension — require same level explicitly.
        if (sender.level() != furniture.level()) return false;
        if (sender.level() != captive.level()) return false;
        if (sender.distanceTo(furniture) > INTERACTION_RANGE) return false;

        // A full furniture must reject force-mount: assignSeat would find no
        // free seat and the captive would become a passenger with no seat id.
        if (furniture instanceof com.tiedup.remake.v2.furniture.EntityFurniture ef) {
            FurnitureDefinition def = ef.getDefinition();
            int seatCount = def != null ? def.seats().size() : 0;
            if (ef.getPassengers().size() >= seatCount) return false;
        }

        boolean captiveAlive = captive.isAlive() && !captive.isRemoved();
        boolean captiveWithinRange =
            sender.distanceTo(captive) <= INTERACTION_RANGE &&
            captive.distanceTo(furniture) <= INTERACTION_RANGE;
        boolean captiveHasOwnedCollar = captiveHasCollarOwnedBy(
            captive,
            sender
        );
        boolean captiveIsTiedUp = captiveIsLeashed(captive);
        boolean captiveIsNotPassenger =
            !captive.isPassenger() || captive.getVehicle() == furniture;

        boolean authorized = isAuthorizedForForceMount(
            captiveAlive,
            captiveWithinRange,
            captiveHasOwnedCollar,
            captiveIsTiedUp,
            captiveIsNotPassenger
        );

        if (!authorized) {
            debug(
                "force-mount denied: sender={}, captive={}, alive={}, range={}, ownedCollar={}, tiedUp={}, notPassenger={}",
                sender.getName().getString(),
                captive.getName().getString(),
                captiveAlive,
                captiveWithinRange,
                captiveHasOwnedCollar,
                captiveIsTiedUp,
                captiveIsNotPassenger
            );
        }
        return authorized;
    }

    // ============================================================
    // Extraction helpers (isolate MC-API touch points)
    // ============================================================

    private static boolean holdsMasterKey(Player sender) {
        return (
            sender.getMainHandItem().getItem() instanceof ItemMasterKey ||
            sender.getOffhandItem().getItem() instanceof ItemMasterKey
        );
    }

    private static boolean seatIsLockable(ISeatProvider provider, String seatId) {
        for (SeatDefinition seat : provider.getSeats()) {
            if (seat.id().equals(seatId)) {
                return seat.lockable();
            }
        }
        return false;
    }

    /**
     * Find the passenger currently assigned to the given seat. Returns null if
     * the seat is unoccupied or the occupant is not a LivingEntity.
     */
    @Nullable
    private static LivingEntity findOccupant(Entity furniture, String seatId) {
        if (!(furniture instanceof ISeatProvider provider)) return null;
        Entity passenger = provider.findPassengerInSeat(seatId);
        return passenger instanceof LivingEntity living ? living : null;
    }

    /**
     * True when {@code occupant} wears a collar and either the sender owns it
     * or the sender has OP permissions.
     */
    private static boolean occupantHasOwnedCollarFor(
        @Nullable LivingEntity occupant,
        ServerPlayer sender
    ) {
        if (occupant == null) return sender.hasPermissions(2);
        return captiveHasCollarOwnedBy(occupant, sender);
    }

    /**
     * True when {@code captive} has a V2 collar in the NECK slot AND either
     * the sender owns it or the sender has OP permissions.
     */
    private static boolean captiveHasCollarOwnedBy(
        LivingEntity captive,
        ServerPlayer sender
    ) {
        IBondageState state = resolveBondageState(captive);
        if (state == null || !state.hasCollar()) {
            return sender.hasPermissions(2);
        }
        ItemStack collar = state.getEquipment(BodyRegionV2.NECK);
        if (collar.isEmpty() || !CollarHelper.isCollar(collar)) {
            return sender.hasPermissions(2);
        }
        return (
            CollarHelper.isOwner(collar, sender) || sender.hasPermissions(2)
        );
    }

    /**
     * Resolve an {@link IBondageState} for any entity that can wear bondage —
     * player, MCA villager, damsel, etc. Delegates to {@link KidnappedHelper}
     * which handles the multi-type dispatch.
     */
    @Nullable
    private static IBondageState resolveBondageState(LivingEntity entity) {
        if (entity instanceof Player player) {
            return com.tiedup.remake.state.PlayerBindState.getInstance(player);
        }
        return KidnappedHelper.getKidnappedState(entity);
    }

    /**
     * True when the entity is currently leashed/tied-up (not just wearing
     * restraints — actively held by a captor).
     */
    private static boolean captiveIsLeashed(LivingEntity captive) {
        IBondageState state = resolveBondageState(captive);
        return state != null && state.isTiedUp();
    }

    private static void debug(String format, Object... args) {
        TiedUpMod.LOGGER.debug("[FurnitureAuth] " + format, args);
    }
}
