import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;

/*
================================================================================
WHOLE-CABIN HYBRID BOARDING SIMULATION
RESEARCH EXPERIMENT 12
PRECEDENCE GATEKEEPER VALIDATION
WINDOWS FILE-LOCK SAFE EDITION
================================================================================

EXPERIMENT 11 PURPOSE
---------------------
This experiment tests local event ordering.

Each scenario runs twice with the same seed. The baseline preserves normal
selection. The dependency-aware run follows a blocked candidate's occupied
yield tile to another waiting-row passenger and gives precedence only to a
downstream event that can start immediately.

No passenger queue is reordered and no airline policy is introduced.

Each scenario still preserves the hybrid execution model:

1. GLOBAL AISLE MOVEMENT
   Tick-driven and synchronous.

2. LOCAL SEAT ACCESS
   Event-driven, with event phases advancing at tick boundaries.

RELATIONSHIP TO AMIT'S ORIGINAL MOVEMENT BACKBONE
-------------------------------------------------
This whole-cabin experiment is a substantial domain expansion, so much of the
surrounding structure is necessarily new: passenger objects, seat assignments,
two aisle datasets, batch execution, file output and experiment statistics.

However, the original backbone still survives in several important places.

DIRECTLY PRESERVED OR CLOSELY PRESERVED PRINCIPLES
- A one-dimensional integer array represents occupancy.
- Empty and occupied positions are tested explicitly.
- Movement occurs only when the next position is available.
- Movement is scan-driven and deterministic.
- Directional index movement is still performed one position at a time.
- Boolean/state flags distinguish movement continuation, waiting and completion.
- The original state is evaluated repeatedly until a terminal condition is met.

EVOLVED FROM THE ORIGINAL BACKBONE
- Anonymous 1 values became unique passenger IDs.
- One aisle array became two mutually exclusive aisle arrays.
- The repeated scan became a synchronous whole-tick update.
- "Finished moving" evolved into WAITING_AT_ROW, IN_SEAT_EVENT and SEATED.
- Local adjacency/blocking logic evolved into seat-access interference events.
- The original comparison/measurement philosophy evolved into experiment metrics.

NEW FOR THIS DOMAIN
- Cabin configurations and seat maps.
- Unique passenger manifests and assigned seats.
- Middle-bank reservations.
- Blocker stand/yield/reseat events.
- Batch scenario generation and results files.

Inline comments below identify these three categories so the reader can see where
the original backbone remains, where it has evolved and where genuinely new
aviation-specific logic begins.

AUTOMATICALLY VARIED FACTORS
----------------------------
- Cabin configuration: 2-4-2, 3-3-3, 3-4-3.
- Passenger occupancy: selected from a controlled range.
- Passenger order: independently shuffled for each aisle.
- Middle-bank aisle assignment: randomly fixed per passenger.
- Entry headway range: selected per scenario.
- Random seed: unique per scenario.
- Optional verbose output: normally disabled for batch runs.

EXPERIMENT 11 DEPENDENCY-AWARE PAIRED STRATEGY
-------------------------------
The code retains all Experiment 04 diagnostics and adds the reverse graph needed
to measure blocker fan-out.

It identifies:

- which unresolved passenger blocks which other passenger;
- the critical blocker with the largest descendant set;
- direct fan-out and total descendants;
- dependency-tree depth;
- separate frozen internal components;
- unresolved passengers outside the critical tree;
- passengers still outside the aircraft.

OUTPUT FILES
------------
Human-readable append log:
    C:\Intel\results.txt

CSV append dataset:
    C:\Intel\results.csv

The code creates C:\Intel if it does not already exist.

IMPORTANT
---------
Tick-by-tick console output is deliberately suppressed during batch execution.
Only concise scenario summaries and the final experiment summary are printed.
This keeps large experiments manageable while preserving detailed statistics.
================================================================================
*/
public class WholeCabinHybridBoardingSimulation {

    // ======================== EXPERIMENT IDENTITY ========================
    static final int EXPERIMENT_NUMBER = 12;
    static final String EXPERIMENT_TITLE =
            "PRECEDENCE GATEKEEPER VALIDATION";

    // ======================== EXPERIMENT CONTROL ========================

    // Number of scenarios executed automatically in one program run.
    static final int NUMBER_OF_SCENARIOS = 30;

    // Reproducible experiment seed. Change this to generate a different experiment set.
    static final long EXPERIMENT_SEED = System.currentTimeMillis();

    // Selected per scenario.
    static final double[] OCCUPANCY_OPTIONS = {0.80, 0.85, 0.90, 0.95, 0.98};

    // Selected per scenario. Each pair is {minimumHeadway, maximumHeadway}.
    static final int[][] HEADWAY_OPTIONS = {
        {0, 0},   // tightly released stream
        {0, 1},
        {0, 2},
        {1, 2},
        {1, 3}    // more dispersed entry
    };

    static final int MAX_TICKS = 30000;

    // Diagnostic watchdog:
    // stop a scenario early when no passenger becomes seated for this many ticks.
    // This avoids wasting the remaining tick budget after a stable deadlock forms.
    static final int STALL_TICK_LIMIT = 1500;

    // Print detailed diagnostic information only when a scenario stalls.
    static final boolean VERBOSE_TICK_OUTPUT = false;
    static final boolean PRINT_STALL_DIAGNOSTICS = true;

    static final Path HUMAN_RESULTS_PATH =
            Paths.get("C:\\Intel\\results.txt");
    static final Path CSV_RESULTS_PATH =
            Paths.get("C:\\Intel\\results.csv");
    /*
     * IMPORTANT WINDOWS OUTPUT NOTE
     * -----------------------------
     * This program does NOT open C:\Intel\output.txt itself.
     *
     * NetBeans, a batch file, PowerShell or Command Prompt may already redirect
     * System.out to output.txt before main() begins. Opening the same file again
     * from Java would then fail with "file is being used by another process".
     *
     * Console output can still be redirected externally to:
     *     C:\Intel\output.txt
     *
     * The program itself writes only:
     *     C:\Intel\results.txt
     *     C:\Intel\results.csv
     */

    // ORIGINAL BACKBONE REMAINS:
    // The original code used integer arrays with explicit empty/occupied values.
    // Here -1 replaces 0 as the empty marker because non-negative values are now
    // reserved for unique passenger IDs.
    static final int EMPTY = -1;

    enum CabinConfiguration {
        TWO_FOUR_TWO(
                "2-4-2", 30,
                new char[]{'A','B','C','D','E','F','G','H'},
                1, 2, 5, 6,
                "A B | LEFT AISLE | C D E F | RIGHT AISLE | G H"
        ),
        THREE_THREE_THREE(
                "3-3-3", 32,
                new char[]{'A','B','C','D','E','F','G','H','I'},
                2, 3, 5, 6,
                "A B C | LEFT AISLE | D E F | RIGHT AISLE | G H I"
        ),
        THREE_FOUR_THREE(
                "3-4-3", 36,
                new char[]{'A','B','C','D','E','F','G','H','I','J'},
                2, 3, 6, 7,
                "A B C | LEFT AISLE | D E F G | RIGHT AISLE | H I J"
        );

        final String name;
        final int rows;
        final char[] seatLetters;
        final int leftOuterEnd;
        final int middleStart;
        final int middleEnd;
        final int rightOuterStart;
        final String layout;

        CabinConfiguration(
                String name,
                int rows,
                char[] seatLetters,
                int leftOuterEnd,
                int middleStart,
                int middleEnd,
                int rightOuterStart,
                String layout) {
            this.name = name;
            this.rows = rows;
            this.seatLetters = seatLetters;
            this.leftOuterEnd = leftOuterEnd;
            this.middleStart = middleStart;
            this.middleEnd = middleEnd;
            this.rightOuterStart = rightOuterStart;
            this.layout = layout;
        }

        int totalSeats() {
            return rows * seatLetters.length;
        }
    }

    enum AisleSide { LEFT, RIGHT }

    enum PassengerState {
        WAITING_TO_ENTER,
        MOVING_IN_AISLE,
        WAITING_AT_ROW,
        IN_SEAT_EVENT,
        SEATED
    }

    enum EventPhase {
        BLOCKERS_STAND,
        BLOCKERS_ENTER_AISLE,
        TARGET_SITS,
        BLOCKERS_RESEAT,
        COMPLETE
    }

    enum YieldDependencyType {
        OPEN_CHAIN_TO_ROW_EVENT,
        CLOSED_PASSENGER_CYCLE,
        REAR_BOUNDARY_LOCK,
        ACTIVE_EVENT_TRANSIENT,
        MIDDLE_BANK_RESERVATION_CHAIN,
        DISCONNECTED_OR_STALE_REFERENCE,
        MIXED_MULTIPLE_YIELD_OCCUPANTS,
        NO_YIELD_OCCUPANT,
        UNCLASSIFIED
    }

    enum FinalSnapshotDependencyType {
        LINKED_ROW_EVENT_CHAIN,
        YIELD_TILE_OCCUPIED_BY_MOVING_PASSENGER,
        REAR_BOUNDARY_LOCK,
        CURRENT_ACTIVE_EVENT,
        CURRENT_RESERVATION_CONFLICT,
        YIELD_SPACE_AVAILABLE,
        STALE_OR_DISCONNECTED_REFERENCE,
        MULTIPLE_MIXED_OCCUPANTS,
        UNCLASSIFIED
    }

    static final class ScenarioConfig {
        final int scenarioNumber;
        final long seed;
        final CabinConfiguration cabin;
        final double occupancyRate;
        final int minEntryHeadway;
        final int maxEntryHeadway;
        final boolean releaseImminentProtectionEnabled;

        ScenarioConfig(
                int scenarioNumber,
                long seed,
                CabinConfiguration cabin,
                double occupancyRate,
                int minEntryHeadway,
                int maxEntryHeadway,
                boolean releaseImminentProtectionEnabled) {
            this.scenarioNumber = scenarioNumber;
            this.seed = seed;
            this.cabin = cabin;
            this.occupancyRate = occupancyRate;
            this.minEntryHeadway = minEntryHeadway;
            this.maxEntryHeadway = maxEntryHeadway;
            this.releaseImminentProtectionEnabled =
                    releaseImminentProtectionEnabled;
        }
    }

    static final class ScenarioResult {
        int scenarioNumber;
        long seed;
        String cabin;
        int rows;
        int seatsPerRow;
        int totalSeats;
        int passengers;
        double occupancyRate;
        int minHeadway;
        int maxHeadway;
        int completedTicks;
        boolean completed;
        int passengersSeated;

        int leftMoves;
        int rightMoves;
        int leftMaxOccupied;
        int rightMaxOccupied;
        int leftSeatEvents;
        int rightSeatEvents;
        int leftSingleBlockers;
        int rightSingleBlockers;
        int leftMultipleBlockers;
        int rightMultipleBlockers;
        int leftTemporaryAisleReoccupations;
        int rightTemporaryAisleReoccupations;

        int middleBankReservationRequests;
        int middleBankReservationGranted;
        int middleBankReservationDenied;
        int simultaneousMiddleBankConflicts;

        double averageWaitingTicks;
        int longestWaitingTicks;
        String longestWaitingPassenger;

        // Stall diagnostics.
        boolean stalled;
        int ticksWithoutNewSeat;
        int remainingWaitingToEnter;
        int remainingMovingInAisle;
        int remainingWaitingAtRow;
        int remainingInSeatEvent;
        int remainingSeated;
        String dominantBlockingReason;
        int dominantBlockingReasonCount;
        int stalledRow;
        String stalledAisle;
        int stalledPassengerId;

        // Experiment 04 dependency evidence.
        int longestDependencyChainLength;
        String longestDependencyChain;
        int dependencyRootPassengerId;
        String dependencyTerminalReason;
        int passengersIndirectlyAffected;
        int passengersWithDirectBlocker;
        int totalBlockedTicks;
        int totalStationaryTicks;
        int totalSeatEventRequests;
        int totalFailedSeatEventStarts;

        // Experiment 05 reverse dependency / fan-out evidence.
        int criticalBlockerPassengerId;
        String criticalBlockerIdentity;
        int criticalBlockerDirectFanOut;
        int criticalBlockerTotalDescendants;
        int criticalBlockerTreeDepth;
        int criticalBlockerTreeSize;
        String criticalBlockerTree;
        int frozenComponentCount;
        int terminalRowEventNodeCount;
        int unresolvedInternalPassengers;
        int unresolvedOutsidePassengers;
        int unresolvedInternalNotInCriticalTree;

        // Experiment 06 seat-event prerequisite evidence.
        String dominantPrerequisiteFailure;
        int dominantPrerequisiteFailureCount;
        String criticalBlockerFinalPrerequisite;
        int criticalBlockerRequiredBlockers;
        String criticalBlockerRequiredYieldTiles;
        String criticalBlockerUnavailableYieldTiles;
        String criticalBlockerYieldTileOccupants;
        int rowPassengersZeroBlockers;
        int rowPassengersOneBlocker;
        int rowPassengersMultipleBlockers;
        int totalYieldTileOccupiedFailures;
        int totalRearBoundaryFailures;
        int totalReservationFailures;
        int totalActiveEventDeferrals;
        int totalArbitrationLosses;

        // Experiment 07 yield-occupant trace evidence.
        YieldDependencyType yieldDependencyType;
        String criticalYieldOccupantTrace;
        int criticalYieldOccupantTraceLength;
        boolean dependencyCycleDetected;
        String dependencyCycleMembers;
        String dependencyTerminalPassenger;
        String dependencyTerminalCondition;
        int distinctRowEventTerminals;
        int requiredYieldOccupantCount;
        boolean allYieldOccupantsSameFrozenComponent;
        String finalStablePrerequisite;
        String criticalYieldOccupantDetails;

        // Experiment 08 final-snapshot evidence.
        FinalSnapshotDependencyType finalSnapshotDependencyType;
        String finalSnapshotPrerequisite;
        int finalSnapshotRequiredBlockers;
        String finalSnapshotRequiredYieldTiles;
        String finalSnapshotUnavailableYieldTiles;
        String finalSnapshotYieldOccupants;
        String finalSnapshotRowEventChain;
        int finalSnapshotRowEventChainLength;
        int finalSnapshotDistinctRowEvents;
        String finalSnapshotTerminalCondition;
        boolean oneEmptyTileWouldStartEvent;
        boolean oneTileEarlierHoldWouldPreserveSpace;
        int simultaneousEmptyTilesRequired;
        int finalRowEventDependencyChainCount;
        boolean activeEventExistsAtStall;
        boolean reservationConflictExistsAtStall;

        // Experiment 10 paired validation evidence.
        String experimentMode;
        boolean releaseImminentProtectionEnabled;
        int releaseImminentEvaluations;
        int releaseImminentWindowsActivated;
        int releaseImminentTilesProtected;
        int releaseImminentPassengerHolds;
        int releaseImminentNextTickEventStarts;
        int releaseImminentWindowsWithoutEvent;
        int lastProtectedCandidateId;

        int gatekeeperUniqueEpisodes;
        int gatekeeperRepeatedSelections;
        int gatekeeperEventStarts;
        int gatekeeperConfirmedReleaseTiles;
        int gatekeeperUpstreamStartsWithin1;
        int gatekeeperUpstreamStartsWithin2;
        int gatekeeperUpstreamStartsWithin5;
        int gatekeeperUpstreamStartsWithin10;
        int gatekeeperEpisodesWithoutEffect;
        int gatekeeperHighestTenTickGain;
        int gatekeeperHighestValueEpisodeId;
        String gatekeeperEpisodeSummary;

        String status() {
            if (completed) {
                return "SUCCESS";
            }
            if (stalled) {
                return "STALL_DETECTED";
            }
            return "MAX_TICKS_REACHED";
        }
    }

    static final class Passenger {
        final int id;
        final int row;
        final char seatLetter;
        final int seatIndex;
        final AisleSide servingAisle;

        PassengerState state = PassengerState.WAITING_TO_ENTER;
        int aisleEntryTick = -1;
        int seatedTick = -1;
        int waitingTicks = 0;

        // Diagnostic history.
        int lastStateChangeTick = 0;
        int lastAislePosition = -1;
        int lastMovementTick = 0;
        String lastBlockingReason = "NONE";

        // Experiment 04 wait-history fields.
        int aisleMoveCount = 0;
        int stationaryTicks = 0;
        int blockedTicks = 0;
        int blockerChangeCount = 0;
        int directBlockingPassengerId = EMPTY;
        int previousDirectBlockingPassengerId = EMPTY;
        int seatEventRequests = 0;
        int failedSeatEventStarts = 0;
        int lastBlockedTick = -1;

        // Experiment 06 prerequisite history.
        int successfulSeatEventStarts = 0;
        int zeroBlockerAttempts = 0;
        int oneBlockerAttempts = 0;
        int multipleBlockerAttempts = 0;
        int yieldTileOccupiedFailures = 0;
        int rearBoundaryFailures = 0;
        int reservationFailures = 0;
        int activeEventDeferrals = 0;
        int arbitrationLosses = 0;
        int lastRequiredBlockerCount = 0;
        String lastPrerequisiteFailure = "NONE";
        String lastRequiredYieldTiles = "NONE";
        String lastUnavailableYieldTiles = "NONE";
        String lastYieldTileOccupants = "NONE";

        Passenger(int id, int row, char seatLetter, int seatIndex, AisleSide servingAisle) {
            this.id = id;
            this.row = row;
            this.seatLetter = seatLetter;
            this.seatIndex = seatIndex;
            this.servingAisle = servingAisle;
        }

        String seatName() {
            return (row + 1) + String.valueOf(seatLetter);
        }

        @Override
        public String toString() {
            return "P" + id + "->" + seatName() + "(" + servingAisle + ")";
        }
    }

    static final class DependencyTrace {
        final List<Integer> path = new ArrayList<>();
        final LinkedHashSet<Integer> cycleMembers = new LinkedHashSet<>();
        String terminalCondition = "UNCLASSIFIED";
        Integer terminalPassengerId = null;
        boolean cycleDetected = false;

        String format(Map<Integer, Passenger> passengers) {
            if (path.isEmpty()) {
                return "NONE";
            }

            StringBuilder sb = new StringBuilder();

            for (int i = 0; i < path.size(); i++) {
                if (i > 0) {
                    sb.append(" -> ");
                }

                int id = path.get(i);
                Passenger p = passengers.get(id);

                sb.append("P").append(id);

                if (p != null) {
                    sb.append("(")
                      .append(p.seatName())
                      .append("/")
                      .append(p.servingAisle)
                      .append("/")
                      .append(p.state)
                      .append("@tile")
                      .append(p.lastAislePosition + 1)
                      .append(")");
                }
            }

            sb.append(" -> [")
              .append(terminalCondition)
              .append("]");

            return sb.toString();
        }
    }

    static final class FinalSnapshotAnalysis {
        FinalSnapshotDependencyType type =
                FinalSnapshotDependencyType.UNCLASSIFIED;
        String prerequisite = "UNCLASSIFIED";
        int requiredBlockers = 0;
        final List<Integer> requiredTiles = new ArrayList<>();
        final List<Integer> unavailableTiles = new ArrayList<>();
        final List<Integer> occupantIds = new ArrayList<>();
        final List<Integer> rowEventChain = new ArrayList<>();
        String terminalCondition = "UNCLASSIFIED";
        boolean activeEventExists = false;
        boolean reservationConflictExists = false;
        boolean oneEmptyTileWouldStart = false;
        boolean oneTileEarlierHoldWouldPreserve = false;
        int simultaneousEmptyTilesRequired = 0;
    }

    static final class SeatEvent {
        final Passenger target;
        final int rowIndex;
        final List<Integer> blockerPassengerIds;
        final List<Integer> blockerSeatIndices;
        final Map<Integer, Integer> blockerYieldTileByPassenger = new LinkedHashMap<>();
        EventPhase phase = EventPhase.BLOCKERS_STAND;

        SeatEvent(
                Passenger target,
                int rowIndex,
                List<Integer> blockerPassengerIds,
                List<Integer> blockerSeatIndices) {
            this.target = target;
            this.rowIndex = rowIndex;
            this.blockerPassengerIds = blockerPassengerIds;
            this.blockerSeatIndices = blockerSeatIndices;
        }
    }

    static final class ReleaseImminentWindow {
        final int candidatePassengerId;
        final Set<Integer> protectedTiles;

        ReleaseImminentWindow(
                int candidatePassengerId,
                Set<Integer> protectedTiles) {
            this.candidatePassengerId = candidatePassengerId;
            this.protectedTiles = protectedTiles;
        }

        boolean protects(int tile) {
            return protectedTiles.contains(tile);
        }
    }

    static final class GatekeeperEpisode {
        final int id;
        final int upstreamId;
        final int gatekeeperId;
        final AisleSide aisle;
        final int expectedReleaseTile;
        final int selectionTick;
        final int seatedAtSelection;
        final String chain;
        boolean gatekeeperStarted;
        boolean releaseConfirmed;
        int upstreamStartedTick = -1;
        int gainAt10Ticks;
        boolean closed;

        GatekeeperEpisode(
                int id,
                int upstreamId,
                int gatekeeperId,
                AisleSide aisle,
                int expectedReleaseTile,
                int selectionTick,
                int seatedAtSelection,
                String chain) {
            this.id = id;
            this.upstreamId = upstreamId;
            this.gatekeeperId = gatekeeperId;
            this.aisle = aisle;
            this.expectedReleaseTile = expectedReleaseTile;
            this.selectionTick = selectionTick;
            this.seatedAtSelection = seatedAtSelection;
            this.chain = chain;
        }
    }

    static final class AisleContext {
        final AisleSide side;

        // ORIGINAL BACKBONE REMAINS:
        // This is still a one-dimensional integer occupancy array.
        // Volume 1 used anonymous 1s; this version stores passenger IDs so identity,
        // destination row and seat assignment can be recovered.
        final int[] aisle;
        final Deque<Integer> entryQueue = new ArrayDeque<>();

        SeatEvent activeEvent;
        int entryCooldown = 0;

        int maxOccupiedTiles = 0;
        int totalMoves = 0;
        int seatEvents = 0;
        int singleBlockerEvents = 0;
        int multipleBlockerEvents = 0;
        int temporaryAisleReoccupations = 0;

        AisleContext(AisleSide side, int rows) {
            this.side = side;
            this.aisle = new int[rows];
            Arrays.fill(this.aisle, EMPTY);
        }
    }

    // ======================== ONE SCENARIO ENGINE ========================

    final ScenarioConfig config;
    final Random random;
    final int rows;
    final char[] seatLetters;

    final Map<Integer, Passenger> passengers = new LinkedHashMap<>();
    final int[][] seats;

    final AisleContext left;
    final AisleContext right;

    final int[] middleBankReservedByPassenger;
    final AisleSide[] middleBankReservedByAisle;

    int currentTick = 0;

    // Progress watchdog state.
    int lastSeatedCount = 0;
    int lastSeatProgressTick = 0;
    boolean stallDetected = false;

    // Aggregated blocking-reason counters.
    final Map<String, Integer> blockingReasonCounts = new LinkedHashMap<>();

    // Experiment 06 prerequisite failure counters.
    final Map<String, Integer> prerequisiteFailureCounts =
            new LinkedHashMap<>();

    int totalYieldTileOccupiedFailures = 0;
    int totalRearBoundaryFailures = 0;
    int totalReservationFailures = 0;
    int totalActiveEventDeferrals = 0;
    int totalArbitrationLosses = 0;

    // Experiment 11 dependency-aware event scheduling.
    int precedenceCandidatesExamined = 0;
    int precedenceBlockedByRowEvent = 0;
    int precedenceChainsFollowed = 0;
    int precedenceMaximumChainDepth = 0;
    int precedenceSubstitutionsMade = 0;
    int precedenceSubstitutedEventsStarted = 0;
    int precedenceRejectedBoundary = 0;
    int precedenceRejectedOccupiedSpace = 0;
    int precedenceRejectedReservation = 0;
    String lastPrecedenceChain = "NONE";
    final List<GatekeeperEpisode> gatekeeperEpisodes = new ArrayList<>();
    int nextGatekeeperEpisodeId = 1;
    int gatekeeperRepeatedSelections = 0;
    int gatekeeperConfirmedReleaseTiles = 0;
    int gatekeeperUpstreamStartsWithin1 = 0;
    int gatekeeperUpstreamStartsWithin2 = 0;
    int gatekeeperUpstreamStartsWithin5 = 0;
    int gatekeeperUpstreamStartsWithin10 = 0;
    int gatekeeperEpisodesWithoutEffect = 0;
    int gatekeeperHighestTenTickGain = 0;
    int gatekeeperHighestValueEpisodeId = EMPTY;


    // Experiment 10 release-imminent protection counters.
    int releaseImminentEvaluations = 0;
    int releaseImminentWindowsActivated = 0;
    int releaseImminentTilesProtected = 0;
    int releaseImminentPassengerHolds = 0;
    int releaseImminentNextTickEventStarts = 0;
    int releaseImminentWindowsWithoutEvent = 0;
    int lastProtectedCandidateId = EMPTY;
    int lastProtectionTick = -1;
    int pendingProtectedCandidateId = EMPTY;
    final Set<Integer> releaseImminentAttemptedCandidates =
            new LinkedHashSet<>();

    int middleBankReservationRequests = 0;
    int middleBankReservationGranted = 0;
    int middleBankReservationDenied = 0;
    int simultaneousMiddleBankConflicts = 0;

    WholeCabinHybridBoardingSimulation(ScenarioConfig config) {
        this.config = config;
        this.random = new Random(config.seed);
        this.rows = config.cabin.rows;
        this.seatLetters = config.cabin.seatLetters;

        this.seats = new int[rows][seatLetters.length];
        for (int[] row : seats) {
            Arrays.fill(row, EMPTY);
        }

        this.left = new AisleContext(AisleSide.LEFT, rows);
        this.right = new AisleContext(AisleSide.RIGHT, rows);

        this.middleBankReservedByPassenger = new int[rows];
        Arrays.fill(this.middleBankReservedByPassenger, EMPTY);
        this.middleBankReservedByAisle = new AisleSide[rows];
    }

    public static void main(String[] args) {
        Random experimentRandom = new Random(EXPERIMENT_SEED);
        List<ScenarioResult> results = new ArrayList<>();

        ensureResultsDirectory();
        ensureCsvHeader();

        String startupBanner =
                "============================================================\n" +
                "WHOLE-CABIN HYBRID BOARDING SIMULATION\n" +
                "RESEARCH EXPERIMENT " +
                String.format(Locale.ROOT, "%02d", EXPERIMENT_NUMBER) + "\n" +
                EXPERIMENT_TITLE + "\n" +
                "============================================================\n" +
                "Experiment seed: " + EXPERIMENT_SEED + "\n" +
                "Scenario pairs scheduled: " + NUMBER_OF_SCENARIOS + "\n" +
                "Total scenario executions: " +
                (NUMBER_OF_SCENARIOS * 2) + "\n" +
                "Human-readable log: " + HUMAN_RESULTS_PATH + "\n" +
                "CSV dataset: " + CSV_RESULTS_PATH + "\n" +
                "Console output: external/IDE capture only\n" +
                "============================================================\n";

        System.out.println(startupBanner);

        System.out.println("Experiment seed: " + EXPERIMENT_SEED);


        for (int scenarioNumber = 1;
             scenarioNumber <= NUMBER_OF_SCENARIOS;
             scenarioNumber++) {

            ScenarioConfig sharedConfig =
                    randomScenarioConfig(
                            scenarioNumber,
                            experimentRandom
                    );

            ScenarioResult baseline =
                    new WholeCabinHybridBoardingSimulation(
                            pairedConfig(sharedConfig, false)
                    ).runScenario();

            ScenarioResult protectedResult =
                    new WholeCabinHybridBoardingSimulation(
                            pairedConfig(sharedConfig, true)
                    ).runScenario();

            results.add(baseline);
            results.add(protectedResult);

            appendHumanReadableResult(baseline);
            appendCsvResult(baseline);
            printScenarioSummary(baseline);

            appendHumanReadableResult(protectedResult);
            appendCsvResult(protectedResult);
            printScenarioSummary(protectedResult);

            printPairComparison(
                    baseline,
                    protectedResult
            );
            appendPairComparison(
                    baseline,
                    protectedResult
            );
        }

        printExperimentSummary(results);
        appendExperimentSummary(results);
    }

    static ScenarioConfig randomScenarioConfig(
            int scenarioNumber,
            Random experimentRandom) {

        // NEW EXPERIMENT HARNESS:
        // This is not part of the original movement backbone. It exists so the same
        // preserved engine can be tested across many domain configurations without
        // manual intervention.

        CabinConfiguration[] cabins = CabinConfiguration.values();
        CabinConfiguration cabin =
                cabins[experimentRandom.nextInt(cabins.length)];

        double occupancy =
                OCCUPANCY_OPTIONS[
                        experimentRandom.nextInt(OCCUPANCY_OPTIONS.length)
                ];

        int[] headway =
                HEADWAY_OPTIONS[
                        experimentRandom.nextInt(HEADWAY_OPTIONS.length)
                ];

        long scenarioSeed = experimentRandom.nextLong();

        return new ScenarioConfig(
                scenarioNumber,
                scenarioSeed,
                cabin,
                occupancy,
                headway[0],
                headway[1],
                false
        );
    }

    static ScenarioConfig pairedConfig(
            ScenarioConfig original,
            boolean protectionEnabled) {

        return new ScenarioConfig(
                original.scenarioNumber,
                original.seed,
                original.cabin,
                original.occupancyRate,
                original.minEntryHeadway,
                original.maxEntryHeadway,
                protectionEnabled
        );
    }

    ScenarioResult runScenario() {
        createManifest();
        buildRandomAisleQueues();

        while (!allPassengersSeated() && currentTick < MAX_TICKS) {
            currentTick++;

            advanceSeatEvent(left);
            advanceSeatEvent(right);

            detectAndStartSeatEventsWithMiddleBankArbitration();

            if (pendingProtectedCandidateId != EMPTY &&
                    currentTick > lastProtectionTick + 1) {
                releaseImminentWindowsWithoutEvent++;
                pendingProtectedCandidateId = EMPTY;
            }

            synchronousAisleMove(left);
            synchronousAisleMove(right);

            insertPassengerIfPossible(left);
            insertPassengerIfPossible(right);

            updateWaitingTicks();
            updateAisleMetrics(left);
            updateAisleMetrics(right);
            updateProgressWatchdog();
            updateGatekeeperEpisodes();

            if (VERBOSE_TICK_OUTPUT) {
                printTickSummary();
            }

            if (stallDetected) {
                if (PRINT_STALL_DIAGNOSTICS) {
                    printStallDiagnostics();
                }
                break;
            }
        }

        return buildScenarioResult();
    }

    void createManifest() {
        List<int[]> allSeats = new ArrayList<>();

        for (int row = 0; row < rows; row++) {
            for (int seatIndex = 0; seatIndex < seatLetters.length; seatIndex++) {
                allSeats.add(new int[]{row, seatIndex});
            }
        }

        Collections.shuffle(allSeats, random);

        int targetPassengerCount =
                (int) Math.round(
                        config.cabin.totalSeats() * config.occupancyRate
                );

        for (int id = 0; id < targetPassengerCount; id++) {
            int[] coordinate = allSeats.get(id);
            int row = coordinate[0];
            int seatIndex = coordinate[1];

            Passenger passenger = new Passenger(
                    id,
                    row,
                    seatLetters[seatIndex],
                    seatIndex,
                    chooseServingAisle(seatIndex)
            );

            passenger.lastStateChangeTick = 0;
            passengers.put(id, passenger);
        }
    }

    AisleSide chooseServingAisle(int seatIndex) {
        if (seatIndex <= config.cabin.leftOuterEnd) {
            return AisleSide.LEFT;
        }

        if (seatIndex >= config.cabin.rightOuterStart) {
            return AisleSide.RIGHT;
        }

        // Middle-bank aisle assignment is random but fixed for the passenger.
        return random.nextBoolean() ? AisleSide.LEFT : AisleSide.RIGHT;
    }

    void buildRandomAisleQueues() {
        // NEW DOMAIN LAYER:
        // The original backbone did not require passenger manifests or two queues.
        // This method prepares the domain data that will later enter the preserved
        // one-dimensional scan-and-move engine.
        List<Integer> leftIds = new ArrayList<>();
        List<Integer> rightIds = new ArrayList<>();

        for (Passenger passenger : passengers.values()) {
            if (passenger.servingAisle == AisleSide.LEFT) {
                leftIds.add(passenger.id);
            } else {
                rightIds.add(passenger.id);
            }
        }

        Collections.shuffle(leftIds, random);
        Collections.shuffle(rightIds, random);

        left.entryQueue.addAll(leftIds);
        right.entryQueue.addAll(rightIds);
    }

    void insertPassengerIfPossible(AisleContext context) {
        // EVOLVED BACKBONE:
        // In the original code, occupied values already existed inside the array.
        // Here passengers are admitted gradually, but insertion still obeys the same
        // local rule: the target position must be empty before occupancy can change.
        if (context.entryCooldown > 0) {
            context.entryCooldown--;
            return;
        }

        if (context.entryQueue.isEmpty() || context.aisle[0] != EMPTY) {
            return;
        }

        int passengerId = context.entryQueue.removeFirst();
        Passenger passenger = passengers.get(passengerId);

        context.aisle[0] = passengerId;
        passenger.state = PassengerState.MOVING_IN_AISLE;
        passenger.aisleEntryTick = currentTick;
        passenger.lastStateChangeTick = currentTick;
        passenger.lastAislePosition = 0;
        passenger.lastMovementTick = currentTick;
        passenger.lastBlockingReason = "NONE";

        int range =
                config.maxEntryHeadway - config.minEntryHeadway + 1;

        context.entryCooldown =
                config.minEntryHeadway + random.nextInt(range);
    }

    void setDirectBlocker(Passenger passenger, int blockerId, String reason) {
        passenger.directBlockingPassengerId = blockerId;
        passenger.lastBlockingReason = reason;
        passenger.blockedTicks++;
        passenger.stationaryTicks++;
        passenger.lastBlockedTick = currentTick;

        if (passenger.previousDirectBlockingPassengerId != blockerId) {
            passenger.blockerChangeCount++;
            passenger.previousDirectBlockingPassengerId = blockerId;
        }

        recordBlockingReason(reason);
    }

    void clearDirectBlocker(Passenger passenger) {
        passenger.directBlockingPassengerId = EMPTY;
        passenger.lastBlockingReason = "NONE";
    }

    Passenger nextEligibleWaitingRowPassenger(
            AisleContext context) {

        if (context.activeEvent != null) {
            return null;
        }

        for (int position = 0;
             position < context.aisle.length;
             position++) {

            int passengerId = context.aisle[position];

            if (passengerId == EMPTY) {
                continue;
            }

            Passenger passenger = passengers.get(passengerId);

            if (passenger != null &&
                    passenger.state == PassengerState.WAITING_AT_ROW &&
                    position == passenger.row) {
                return passenger;
            }
        }

        return null;
    }

    boolean occupantCanMoveForwardThisTick(
            int[] snapshot,
            int occupantTile) {

        int occupantId = snapshot[occupantTile];

        if (occupantId == EMPTY) {
            return true;
        }

        Passenger occupant = passengers.get(occupantId);

        if (occupant == null ||
                occupant.state != PassengerState.MOVING_IN_AISLE) {
            return false;
        }

        if (occupantTile >= occupant.row) {
            return false;
        }

        int nextTile = occupantTile + 1;

        return nextTile < snapshot.length &&
                snapshot[nextTile] == EMPTY;
    }

    ReleaseImminentWindow buildReleaseImminentWindow(
            AisleContext context,
            int[] snapshot) {

        // Experiment 11 disables Experiment 10 tile protection.
        // The paired boolean now activates dependency-aware event precedence.
        return null;
    }

    List<Integer> currentBlockerIds(Passenger passenger) {
        List<Integer> ids = new ArrayList<>();
        for (int seatIndex : findBlockingSeatIndices(passenger)) {
            int id = seats[passenger.row][seatIndex];
            if (id != EMPTY) {
                ids.add(id);
            }
        }
        return ids;
    }

    boolean precedenceEventCanStartNow(
            AisleContext context,
            Passenger passenger) {

        List<Integer> blockers = currentBlockerIds(passenger);

        for (int offset = 1; offset <= blockers.size(); offset++) {
            int tile = passenger.row + offset;

            if (tile >= context.aisle.length) {
                precedenceRejectedBoundary++;
                return false;
            }

            if (context.aisle[tile] != EMPTY) {
                precedenceRejectedOccupiedSpace++;
                return false;
            }
        }

        if (isMiddleBankSeat(passenger.seatIndex)) {
            int holder = middleBankReservedByPassenger[passenger.row];
            if (holder != EMPTY && holder != passenger.id) {
                precedenceRejectedReservation++;
                return false;
            }
        }

        return true;
    }

    Passenger dependencyAwareCandidate(
            AisleContext context,
            Passenger normalCandidate) {

        if (!config.releaseImminentProtectionEnabled ||
                normalCandidate == null) {
            return normalCandidate;
        }

        precedenceCandidatesExamined++;

        LinkedHashSet<Integer> visited = new LinkedHashSet<>();
        List<Integer> chain = new ArrayList<>();
        Passenger current = normalCandidate;

        while (current != null && visited.add(current.id)) {
            chain.add(current.id);
            List<Integer> blockers = currentBlockerIds(current);

            if (blockers.isEmpty()) {
                break;
            }

            Passenger successor = null;

            for (int offset = 1; offset <= blockers.size(); offset++) {
                int tile = current.row + offset;

                if (tile >= context.aisle.length) {
                    precedenceRejectedBoundary++;
                    break;
                }

                int occupantId = context.aisle[tile];
                if (occupantId == EMPTY) {
                    continue;
                }

                Passenger occupant = passengers.get(occupantId);
                if (occupant != null &&
                        occupant.state == PassengerState.WAITING_AT_ROW &&
                        occupant.lastAislePosition == occupant.row) {
                    successor = occupant;
                }
                break;
            }

            if (successor == null) {
                break;
            }

            precedenceBlockedByRowEvent++;
            current = successor;

            if (precedenceEventCanStartNow(context, current)) {
                chain.add(current.id);
                precedenceChainsFollowed++;
                precedenceMaximumChainDepth = Math.max(
                        precedenceMaximumChainDepth,
                        chain.size()
                );
                precedenceSubstitutionsMade++;
                lastPrecedenceChain = joinIntegers(chain);

                boolean repeated = false;
                for (GatekeeperEpisode e : gatekeeperEpisodes) {
                    if (!e.closed &&
                            e.upstreamId == normalCandidate.id &&
                            e.gatekeeperId == current.id) {
                        repeated = true;
                        break;
                    }
                }

                if (repeated) {
                    gatekeeperRepeatedSelections++;
                } else {
                    gatekeeperEpisodes.add(
                            new GatekeeperEpisode(
                                    nextGatekeeperEpisodeId++,
                                    normalCandidate.id,
                                    current.id,
                                    context.side,
                                    normalCandidate.row + 1,
                                    currentTick,
                                    currentSeatedCount(),
                                    lastPrecedenceChain
                            )
                    );
                }

                return current;
            }
        }

        if (chain.size() > 1) {
            precedenceChainsFollowed++;
            precedenceMaximumChainDepth = Math.max(
                    precedenceMaximumChainDepth,
                    chain.size()
            );
            lastPrecedenceChain = joinIntegers(chain);
        }

        return normalCandidate;
    }

    void synchronousAisleMove(AisleContext context) {
        // ================================================================
        // MAIN ORIGINAL-BACKBONE DESCENDANT
        // ================================================================
        // This is the clearest surviving descendant of Amit's original movement code.
        //
        // Original structural idea:
        //   scan a one-dimensional array;
        //   inspect the current position and the next position;
        //   move only when the next position is empty;
        //   repeat scans until the movement objective is complete.
        //
        // Main evolution:
        //   decisions are now calculated from a tick-start snapshot and then applied
        //   together. This prevents one scan from allowing a passenger to exploit a
        //   tile that another passenger vacated earlier in the same tick.
        int[] snapshot = Arrays.copyOf(context.aisle, context.aisle.length);
        int[] next = Arrays.copyOf(snapshot, snapshot.length);

        ReleaseImminentWindow protectionWindow =
                buildReleaseImminentWindow(
                        context,
                        snapshot);

        int blockedRow =
                context.activeEvent == null
                        ? -1
                        : context.activeEvent.rowIndex;

        for (int position = snapshot.length - 1;
             position >= 0;
             position--) {

            int passengerId = snapshot[position];

            if (passengerId == EMPTY) {
                continue;
            }

            Passenger passenger = passengers.get(passengerId);

            if (passenger == null ||
                    passenger.state == PassengerState.IN_SEAT_EVENT) {
                continue;
            }

            if (position == passenger.row) {
                // EVOLVED FROM ORIGINAL "FINISHED / BLOCKED" BOOLEAN LOGIC:
                // Arrival at the destination row becomes an intermediate event trigger.
                if (passenger.state != PassengerState.WAITING_AT_ROW) {
                    passenger.state = PassengerState.WAITING_AT_ROW;
                    passenger.lastStateChangeTick = currentTick;
                }
                passenger.lastAislePosition = position;
                setDirectBlocker(
                        passenger,
                        EMPTY,
                        "WAITING_FOR_SEAT_EVENT"
                );
                continue;
            }

            if (position > passenger.row) {
                passenger.state = PassengerState.WAITING_AT_ROW;
                passenger.lastStateChangeTick = currentTick;
                passenger.lastAislePosition = position;
                setDirectBlocker(
                        passenger,
                        EMPTY,
                        "PASSED_ASSIGNED_ROW_DEFENSIVE"
                );
                continue;
            }

            if (blockedRow >= 0 && position <= blockedRow) {
                passenger.lastAislePosition = position;
                int eventTargetId =
                        context.activeEvent == null
                                ? EMPTY
                                : context.activeEvent.target.id;
                setDirectBlocker(
                        passenger,
                        eventTargetId,
                        "ACTIVE_SEAT_EVENT_AHEAD"
                );
                continue;
            }

            int nextPosition = position + 1;

            if (nextPosition >= snapshot.length) {
                passenger.lastAislePosition = position;
                setDirectBlocker(
                        passenger,
                        EMPTY,
                        "AISLE_BOUNDARY"
                );
                continue;
            }

            if (snapshot[nextPosition] == EMPTY) {
                if (protectionWindow != null &&
                        protectionWindow.protects(nextPosition)) {

                    passenger.lastAislePosition = position;
                    setDirectBlocker(
                            passenger,
                            EMPTY,
                            "RELEASE_IMMINENT_WINDOW_HOLD"
                    );
                    releaseImminentPassengerHolds++;
                    continue;
                }

                // ORIGINAL MOVE PATTERN, EVOLVED FROM:
                //
                // people[i] = 0;
                // people[indexMove] = 1;
                // moves++;
                //
                // The same occupancy transfer remains:
                // current position becomes empty; next position becomes occupied.
                // Passenger ID replaces anonymous value 1.
                next[position] = EMPTY;
                next[nextPosition] = passengerId;
                context.totalMoves++;
                passenger.aisleMoveCount++;
                passenger.lastAislePosition = nextPosition;
                passenger.lastMovementTick = currentTick;
                clearDirectBlocker(passenger);

                if (nextPosition == passenger.row) {
                    passenger.state = PassengerState.WAITING_AT_ROW;
                    passenger.lastStateChangeTick = currentTick;
                }
            } else {
                passenger.lastAislePosition = position;
                setDirectBlocker(
                        passenger,
                        snapshot[nextPosition],
                        "NEXT_AISLE_TILE_OCCUPIED"
                );
            }
        }

        System.arraycopy(
                next,
                0,
                context.aisle,
                0,
                context.aisle.length
        );
    }


    void recordPrerequisiteFailure(
            Passenger passenger,
            String reason) {

        passenger.lastPrerequisiteFailure = reason;
        prerequisiteFailureCounts.merge(reason, 1, Integer::sum);
    }

    String dominantPrerequisiteFailure() {
        String selected = "NONE";
        int highest = 0;

        for (Map.Entry<String, Integer> entry :
                prerequisiteFailureCounts.entrySet()) {

            if (entry.getValue() > highest) {
                selected = entry.getKey();
                highest = entry.getValue();
            }
        }

        return selected;
    }

    int dominantPrerequisiteFailureCount() {
        int highest = 0;

        for (int count : prerequisiteFailureCounts.values()) {
            highest = Math.max(highest, count);
        }

        return highest;
    }

    String joinIntegers(List<Integer> values) {
        if (values.isEmpty()) {
            return "NONE";
        }

        StringBuilder sb = new StringBuilder();

        for (int i = 0; i < values.size(); i++) {
            if (i > 0) {
                sb.append("|");
            }
            sb.append(values.get(i));
        }

        return sb.toString();
    }

    void detectAndStartSeatEventsWithMiddleBankArbitration() {
        // NEW DOMAIN LAYER BUILT ON TOP OF THE BACKBONE:
        // The original scan engine brings passengers to the relevant row.
        // This method converts that reached-location state into an event request.
        // Reservation and arbitration are new because two mutually exclusive aisles
        // can now address the same middle seat bank.
        Passenger leftCandidate =
                dependencyAwareCandidate(
                        left,
                        findWaitingPassengerAtRow(left)
                );

        Passenger rightCandidate =
                dependencyAwareCandidate(
                        right,
                        findWaitingPassengerAtRow(right)
                );

        boolean simultaneousSameMiddleRow =
                leftCandidate != null &&
                rightCandidate != null &&
                isMiddleBankSeat(leftCandidate.seatIndex) &&
                isMiddleBankSeat(rightCandidate.seatIndex) &&
                leftCandidate.row == rightCandidate.row;

        if (simultaneousSameMiddleRow) {
            simultaneousMiddleBankConflicts++;

            Passenger winner =
                    leftCandidate.id < rightCandidate.id
                            ? leftCandidate
                            : rightCandidate;

            AisleContext winnerContext =
                    winner.servingAisle == AisleSide.LEFT
                            ? left
                            : right;

            Passenger loser =
                    winner.id == leftCandidate.id
                            ? rightCandidate
                            : leftCandidate;

            loser.arbitrationLosses++;
            totalArbitrationLosses++;
            recordPrerequisiteFailure(
                    loser,
                    "SIMULTANEOUS_MIDDLE_BANK_ARBITRATION_LOSS"
            );

            if (reserveMiddleBank(winner)) {
                if (!startSeatEventForPassenger(winnerContext, winner)) {
                    releaseMiddleBankReservation(
                            winner.row,
                            winner.id
                    );
                }
            }
            return;
        }

        tryStartCandidate(left, leftCandidate);
        tryStartCandidate(right, rightCandidate);
    }

    void tryStartCandidate(
            AisleContext context,
            Passenger candidate) {

        if (candidate == null) {
            return;
        }

        if (isMiddleBankSeat(candidate.seatIndex) &&
                !reserveMiddleBank(candidate)) {
            return;
        }

        boolean started =
                startSeatEventForPassenger(context, candidate);

        if (started &&
                config.releaseImminentProtectionEnabled &&
                !"NONE".equals(lastPrecedenceChain) &&
                lastPrecedenceChain.endsWith(
                        String.valueOf(candidate.id))) {
            precedenceSubstitutedEventsStarted++;

            for (GatekeeperEpisode e : gatekeeperEpisodes) {
                if (!e.closed &&
                        e.gatekeeperId == candidate.id &&
                        !e.gatekeeperStarted) {
                    e.gatekeeperStarted = true;
                    break;
                }
            }
        }

        if (!started &&
                isMiddleBankSeat(candidate.seatIndex)) {
            releaseMiddleBankReservation(
                    candidate.row,
                    candidate.id
            );
        }
    }

    Passenger findWaitingPassengerAtRow(AisleContext context) {
        if (context.activeEvent != null) {
            for (int position = 0;
                 position < context.aisle.length;
                 position++) {

                int passengerId = context.aisle[position];

                if (passengerId == EMPTY) {
                    continue;
                }

                Passenger passenger = passengers.get(passengerId);

                if (passenger != null &&
                        position == passenger.row &&
                        passenger.state == PassengerState.WAITING_AT_ROW) {

                    passenger.activeEventDeferrals++;
                    totalActiveEventDeferrals++;
                    recordPrerequisiteFailure(
                            passenger,
                            "ANOTHER_ACTIVE_SEAT_EVENT"
                    );
                }
            }

            return null;
        }

        for (int position = 0;
             position < context.aisle.length;
             position++) {

            int passengerId = context.aisle[position];

            if (passengerId == EMPTY) {
                continue;
            }

            Passenger passenger = passengers.get(passengerId);

            if (passenger != null &&
                    position == passenger.row &&
                    passenger.state == PassengerState.WAITING_AT_ROW) {
                return passenger;
            }
        }

        return null;
    }

    boolean isMiddleBankSeat(int seatIndex) {
        return seatIndex >= config.cabin.middleStart &&
               seatIndex <= config.cabin.middleEnd;
    }

    boolean reserveMiddleBank(Passenger passenger) {
        middleBankReservationRequests++;

        int holder =
                middleBankReservedByPassenger[passenger.row];

        if (holder == EMPTY || holder == passenger.id) {
            middleBankReservedByPassenger[passenger.row] =
                    passenger.id;

            middleBankReservedByAisle[passenger.row] =
                    passenger.servingAisle;

            middleBankReservationGranted++;
            return true;
        }

        middleBankReservationDenied++;
        passenger.reservationFailures++;
        totalReservationFailures++;
        passenger.lastBlockingReason = "MIDDLE_BANK_RESERVED";
        recordBlockingReason(passenger.lastBlockingReason);
        recordPrerequisiteFailure(
                passenger,
                "MIDDLE_BANK_RESERVATION_DENIED"
        );
        return false;
    }

    void releaseMiddleBankReservation(
            int row,
            int passengerId) {

        if (middleBankReservedByPassenger[row] == passengerId) {
            middleBankReservedByPassenger[row] = EMPTY;
            middleBankReservedByAisle[row] = null;
        }
    }

    boolean startSeatEventForPassenger(
            AisleContext context,
            Passenger passenger) {

        passenger.seatEventRequests++;

        List<Integer> blockerSeats =
                findBlockingSeatIndices(passenger);

        List<Integer> blockerIds = new ArrayList<>();

        for (int seatIndex : blockerSeats) {
            int blockerId =
                    seats[passenger.row][seatIndex];

            if (blockerId != EMPTY) {
                blockerIds.add(blockerId);
            }
        }

        int blockerCount = blockerIds.size();
        passenger.lastRequiredBlockerCount = blockerCount;

        if (blockerCount == 0) {
            passenger.zeroBlockerAttempts++;
        } else if (blockerCount == 1) {
            passenger.oneBlockerAttempts++;
        } else {
            passenger.multipleBlockerAttempts++;
        }

        List<Integer> requiredTiles = new ArrayList<>();
        List<Integer> unavailableTiles = new ArrayList<>();
        List<Integer> occupyingPassengerIds = new ArrayList<>();

        boolean rearBoundaryFailure = false;

        for (int offset = 1;
             offset <= blockerCount;
             offset++) {

            int tile = passenger.row + offset;
            requiredTiles.add(tile + 1);

            if (tile >= context.aisle.length) {
                rearBoundaryFailure = true;
                unavailableTiles.add(tile + 1);
                occupyingPassengerIds.add(EMPTY);
                continue;
            }

            if (context.aisle[tile] != EMPTY) {
                unavailableTiles.add(tile + 1);
                occupyingPassengerIds.add(context.aisle[tile]);
            }
        }

        passenger.lastRequiredYieldTiles =
                joinIntegers(requiredTiles);
        passenger.lastUnavailableYieldTiles =
                joinIntegers(unavailableTiles);
        passenger.lastYieldTileOccupants =
                joinIntegers(occupyingPassengerIds);

        if (rearBoundaryFailure) {
            passenger.failedSeatEventStarts++;
            passenger.rearBoundaryFailures++;
            totalRearBoundaryFailures++;

            recordBlockingReason(
                    "INSUFFICIENT_REAR_YIELD_SPACE"
            );
            recordPrerequisiteFailure(
                    passenger,
                    "INSUFFICIENT_REAR_YIELD_SPACE"
            );
            return false;
        }

        if (!unavailableTiles.isEmpty()) {
            passenger.failedSeatEventStarts++;
            passenger.yieldTileOccupiedFailures++;
            totalYieldTileOccupiedFailures++;

            recordBlockingReason("YIELD_TILE_OCCUPIED");
            recordPrerequisiteFailure(
                    passenger,
                    "YIELD_TILE_OCCUPIED"
            );
            return false;
        }

        context.activeEvent =
                new SeatEvent(
                        passenger,
                        passenger.row,
                        blockerIds,
                        blockerSeats
                );

        context.seatEvents++;

        if (blockerCount == 1) {
            context.singleBlockerEvents++;
        } else if (blockerCount > 1) {
            context.multipleBlockerEvents++;
        }

        passenger.successfulSeatEventStarts++;

        if (passenger.id == pendingProtectedCandidateId &&
                currentTick == lastProtectionTick + 1) {
            releaseImminentNextTickEventStarts++;
            pendingProtectedCandidateId = EMPTY;
        }

        passenger.state = PassengerState.IN_SEAT_EVENT;
        passenger.lastStateChangeTick = currentTick;
        passenger.lastBlockingReason = "NONE";
        passenger.lastPrerequisiteFailure = "NONE";
        return true;
    }

    boolean hasRequiredYieldSpace(
            AisleContext context,
            int row,
            int blockerCount) {

        for (int offset = 1;
             offset <= blockerCount;
             offset++) {

            int tile = row + offset;

            if (tile >= context.aisle.length) {
                recordBlockingReason("INSUFFICIENT_REAR_YIELD_SPACE");
                return false;
            }

            if (context.aisle[tile] != EMPTY) {
                recordBlockingReason("YIELD_TILE_OCCUPIED");
                return false;
            }
        }

        return true;
    }

    List<Integer> findBlockingSeatIndices(Passenger passenger) {
        // ORIGINAL PRINCIPLE, DIFFERENT DATASET:
        // This remains a directional scan over local occupancy.
        // The dataset is now a seat row rather than the aisle, and the result is not
        // clustering; it is identification of seats that interfere with access.
        List<Integer> blockers = new ArrayList<>();

        if (passenger.seatIndex <= config.cabin.leftOuterEnd) {
            for (int i = config.cabin.leftOuterEnd;
                 i > passenger.seatIndex;
                 i--) {

                if (seats[passenger.row][i] != EMPTY) {
                    blockers.add(i);
                }
            }

        } else if (passenger.seatIndex >= config.cabin.rightOuterStart) {
            for (int i = config.cabin.rightOuterStart;
                 i < passenger.seatIndex;
                 i++) {

                if (seats[passenger.row][i] != EMPTY) {
                    blockers.add(i);
                }
            }

        } else if (passenger.servingAisle == AisleSide.LEFT) {
            for (int i = config.cabin.middleStart;
                 i < passenger.seatIndex;
                 i++) {

                if (seats[passenger.row][i] != EMPTY) {
                    blockers.add(i);
                }
            }

        } else {
            for (int i = config.cabin.middleEnd;
                 i > passenger.seatIndex;
                 i--) {

                if (seats[passenger.row][i] != EMPTY) {
                    blockers.add(i);
                }
            }
        }

        return blockers;
    }

    void advanceSeatEvent(AisleContext context) {
        // NEW EVENT-DRIVEN EXTENSION:
        // Volume 1 used repeated scans and booleans to describe movement stages.
        // Here those implicit stages have evolved into an explicit finite-state
        // sequence: stand -> enter aisle -> target sits -> reseat -> complete.
        SeatEvent event = context.activeEvent;

        if (event == null) {
            return;
        }

        switch (event.phase) {
            case BLOCKERS_STAND:
                event.phase = EventPhase.BLOCKERS_ENTER_AISLE;
                break;

            case BLOCKERS_ENTER_AISLE:
                if (placeBlockersInAisle(context, event)) {
                    event.phase = EventPhase.TARGET_SITS;
                }
                break;

            case TARGET_SITS:
                seatTargetPassenger(context, event);
                event.phase = EventPhase.BLOCKERS_RESEAT;
                break;

            case BLOCKERS_RESEAT:
                reseatBlockers(context, event);
                event.phase = EventPhase.COMPLETE;
                break;

            case COMPLETE:
                if (isMiddleBankSeat(event.target.seatIndex)) {
                    releaseMiddleBankReservation(
                            event.rowIndex,
                            event.target.id
                    );
                }

                context.activeEvent = null;
                break;
        }
    }

    boolean placeBlockersInAisle(
            AisleContext context,
            SeatEvent event) {

        int blockerCount =
                event.blockerPassengerIds.size();

        if (blockerCount == 0) {
            return true;
        }

        List<Integer> requiredTiles = new ArrayList<>();

        for (int i = 0; i < blockerCount; i++) {
            int rearwardOffset = blockerCount - i;
            int tile = event.rowIndex + rearwardOffset;

            if (tile < 0 ||
                    tile >= context.aisle.length) {
                return false;
            }

            requiredTiles.add(tile);
        }

        for (int tile : requiredTiles) {
            if (context.aisle[tile] != EMPTY) {
                return false;
            }
        }

        for (int i = 0; i < blockerCount; i++) {
            int blockerId =
                    event.blockerPassengerIds.get(i);

            int blockerSeatIndex =
                    event.blockerSeatIndices.get(i);

            int tile =
                    requiredTiles.get(i);

            context.aisle[tile] = blockerId;

            event.blockerYieldTileByPassenger.put(
                    blockerId,
                    tile
            );

            seats[event.rowIndex][blockerSeatIndex] = EMPTY;

            Passenger blocker =
                    passengers.get(blockerId);

            blocker.state = PassengerState.IN_SEAT_EVENT;

            context.temporaryAisleReoccupations++;
        }

        return true;
    }

    void seatTargetPassenger(
            AisleContext context,
            SeatEvent event) {

        Passenger target = event.target;

        if (context.aisle[event.rowIndex] == target.id) {
            context.aisle[event.rowIndex] = EMPTY;
        }

        seats[target.row][target.seatIndex] = target.id;
        target.state = PassengerState.SEATED;
        target.seatedTick = currentTick;
        target.lastStateChangeTick = currentTick;
        target.lastBlockingReason = "NONE";
    }

    void reseatBlockers(
            AisleContext context,
            SeatEvent event) {

        for (int i = event.blockerPassengerIds.size() - 1;
             i >= 0;
             i--) {

            int blockerId =
                    event.blockerPassengerIds.get(i);

            int blockerSeatIndex =
                    event.blockerSeatIndices.get(i);

            Integer yieldTile =
                    event.blockerYieldTileByPassenger.get(blockerId);

            if (yieldTile != null &&
                    context.aisle[yieldTile] == blockerId) {
                context.aisle[yieldTile] = EMPTY;
            }

            seats[event.rowIndex][blockerSeatIndex] =
                    blockerId;

            Passenger blockerPassenger = passengers.get(blockerId);
            blockerPassenger.state = PassengerState.SEATED;
            blockerPassenger.lastStateChangeTick = currentTick;
            blockerPassenger.lastBlockingReason = "NONE";
        }
    }

    void updateWaitingTicks() {
        for (Passenger passenger : passengers.values()) {
            if (passenger.state == PassengerState.WAITING_TO_ENTER ||
                passenger.state == PassengerState.WAITING_AT_ROW ||
                passenger.state == PassengerState.IN_SEAT_EVENT) {

                passenger.waitingTicks++;

                if (passenger.state == PassengerState.WAITING_TO_ENTER) {
                    passenger.stationaryTicks++;
                    passenger.lastBlockingReason = "WAITING_OUTSIDE_FOR_ENTRY";
                }
            }
        }
    }

    void updateAisleMetrics(AisleContext context) {
        int occupied = 0;

        for (int value : context.aisle) {
            if (value != EMPTY) {
                occupied++;
            }
        }

        context.maxOccupiedTiles =
                Math.max(
                        context.maxOccupiedTiles,
                        occupied
                );
    }

    void printTickSummary() {
        System.out.println(
                "Tick " + currentTick +
                " | seated=" +
                countPassengersInState(PassengerState.SEATED) +
                "/" + passengers.size() +
                " | leftInAisle=" +
                occupiedCount(left.aisle) +
                " | rightInAisle=" +
                occupiedCount(right.aisle)
        );
    }

    int occupiedCount(int[] aisle) {
        int count = 0;

        for (int value : aisle) {
            if (value != EMPTY) {
                count++;
            }
        }

        return count;
    }

    int countPassengersInState(PassengerState state) {
        int count = 0;

        for (Passenger passenger : passengers.values()) {
            if (passenger.state == state) {
                count++;
            }
        }

        return count;
    }

    boolean allPassengersSeated() {
        // ORIGINAL TERMINAL-CONDITION PRINCIPLE:
        // The original code repeatedly scanned until all anonymous passengers reached
        // the required final zone. This version repeats ticks until every identified
        // passenger reaches the SEATED state.
        return countPassengersInState(PassengerState.SEATED) ==
               passengers.size();
    }

    void recordBlockingReason(String reason) {
        if (reason == null || "NONE".equals(reason)) {
            return;
        }
        blockingReasonCounts.merge(reason, 1, Integer::sum);
    }

    void updateProgressWatchdog() {
        int seatedNow = countPassengersInState(PassengerState.SEATED);

        if (seatedNow > lastSeatedCount) {
            lastSeatedCount = seatedNow;
            lastSeatProgressTick = currentTick;
            return;
        }

        if ((currentTick - lastSeatProgressTick) >= STALL_TICK_LIMIT) {
            stallDetected = true;
        }
    }

    void printStallDiagnostics() {
        System.out.println();
        System.out.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
        System.out.println("STALL DETECTED");
        System.out.println("Scenario: " + config.scenarioNumber);
        System.out.println("Tick: " + currentTick);
        System.out.println("Ticks without a newly seated passenger: "
                + (currentTick - lastSeatProgressTick));
        System.out.println("Passengers seated: "
                + countPassengersInState(PassengerState.SEATED)
                + "/" + passengers.size());

        Map<PassengerState, Integer> stateCounts = new EnumMap<>(PassengerState.class);
        for (PassengerState state : PassengerState.values()) {
            stateCounts.put(state, 0);
        }

        for (Passenger passenger : passengers.values()) {
            stateCounts.put(
                    passenger.state,
                    stateCounts.get(passenger.state) + 1
            );
        }

        System.out.println("Remaining-state distribution: " + stateCounts);

        List<Passenger> unresolved = new ArrayList<>();
        for (Passenger passenger : passengers.values()) {
            if (passenger.state != PassengerState.SEATED) {
                unresolved.add(passenger);
            }
        }

        unresolved.sort(
                Comparator.comparingInt(
                        (Passenger p) -> currentTick - p.lastStateChangeTick
                ).reversed()
        );

        System.out.println("Top unresolved passengers:");
        int limit = Math.min(12, unresolved.size());

        for (int i = 0; i < limit; i++) {
            Passenger p = unresolved.get(i);
            System.out.println("  " + p
                    + " state=" + p.state
                    + " aislePos=" + p.lastAislePosition
                    + " unchangedTicks=" + (currentTick - p.lastStateChangeTick)
                    + " lastMoveTick=" + p.lastMovementTick
                    + " reason=" + p.lastBlockingReason);
        }

        System.out.println("Blocking-reason totals: " + blockingReasonCounts);
        System.out.println("LEFT aisle snapshot : " + Arrays.toString(left.aisle));
        System.out.println("RIGHT aisle snapshot: " + Arrays.toString(right.aisle));
        System.out.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
        System.out.println();
    }

    String dominantBlockingReason() {
        String bestReason = "NONE";
        int bestCount = 0;

        for (Map.Entry<String, Integer> entry : blockingReasonCounts.entrySet()) {
            if (entry.getValue() > bestCount) {
                bestReason = entry.getKey();
                bestCount = entry.getValue();
            }
        }

        return bestReason;
    }

    int dominantBlockingReasonCount() {
        int bestCount = 0;

        for (int count : blockingReasonCounts.values()) {
            bestCount = Math.max(bestCount, count);
        }

        return bestCount;
    }

    Passenger longestUnresolvedPassenger() {
        Passenger selected = null;
        int longest = -1;

        for (Passenger passenger : passengers.values()) {
            if (passenger.state == PassengerState.SEATED) {
                continue;
            }

            int unchanged = currentTick - passenger.lastStateChangeTick;
            if (unchanged > longest) {
                longest = unchanged;
                selected = passenger;
            }
        }

        return selected;
    }


    Passenger passengerById(int passengerId) {
        return passengerId == EMPTY ? null : passengers.get(passengerId);
    }

    String terminalReasonFor(Passenger passenger) {
        if (passenger == null) {
            return "NO_PASSENGER";
        }

        if (passenger.directBlockingPassengerId != EMPTY) {
            return "BLOCKED_BY_PASSENGER";
        }

        return passenger.lastBlockingReason == null
                ? "UNKNOWN"
                : passenger.lastBlockingReason;
    }

    List<Integer> buildDependencyChainFrom(Passenger start) {
        List<Integer> chain = new ArrayList<>();
        Set<Integer> visited = new LinkedHashSet<>();

        Passenger current = start;

        while (current != null &&
                current.state != PassengerState.SEATED &&
                visited.add(current.id)) {

            chain.add(current.id);

            if (current.directBlockingPassengerId == EMPTY) {
                break;
            }

            current = passengerById(current.directBlockingPassengerId);
        }

        return chain;
    }

    String formatDependencyChain(List<Integer> chain) {
        if (chain.isEmpty()) {
            return "NONE";
        }

        StringBuilder sb = new StringBuilder();

        for (int i = 0; i < chain.size(); i++) {
            if (i > 0) {
                sb.append(" -> ");
            }

            Passenger passenger = passengers.get(chain.get(i));
            sb.append("P").append(chain.get(i));

            if (passenger != null) {
                sb.append("(")
                  .append(passenger.seatName())
                  .append("/")
                  .append(passenger.servingAisle)
                  .append(")");
            }
        }

        Passenger terminal = passengers.get(chain.get(chain.size() - 1));
        sb.append(" -> [")
          .append(terminalReasonFor(terminal))
          .append("]");

        return sb.toString();
    }

    Passenger longestDependencyRoot() {
        Passenger best = null;
        int bestLength = -1;

        for (Passenger passenger : passengers.values()) {
            if (passenger.state == PassengerState.SEATED) {
                continue;
            }

            int length = buildDependencyChainFrom(passenger).size();

            if (length > bestLength) {
                bestLength = length;
                best = passenger;
            }
        }

        return best;
    }

    int countPassengersWithDirectBlocker() {
        int count = 0;

        for (Passenger passenger : passengers.values()) {
            if (passenger.state != PassengerState.SEATED &&
                    passenger.directBlockingPassengerId != EMPTY) {
                count++;
            }
        }

        return count;
    }

    int countIndirectlyAffectedBy(int rootPassengerId) {
        if (rootPassengerId == EMPTY) {
            return 0;
        }

        int count = 0;

        for (Passenger passenger : passengers.values()) {
            if (passenger.state == PassengerState.SEATED) {
                continue;
            }

            List<Integer> chain = buildDependencyChainFrom(passenger);

            if (chain.contains(rootPassengerId) &&
                    passenger.id != rootPassengerId) {
                count++;
            }
        }

        return count;
    }


    Map<Integer, List<Integer>> buildReverseDependencyGraph() {
        Map<Integer, List<Integer>> reverse = new LinkedHashMap<>();

        for (Passenger passenger : passengers.values()) {
            if (passenger.state == PassengerState.SEATED) {
                continue;
            }

            int blockerId = passenger.directBlockingPassengerId;

            if (blockerId != EMPTY &&
                    passengers.containsKey(blockerId)) {
                reverse.computeIfAbsent(
                        blockerId,
                        ignored -> new ArrayList<>()
                ).add(passenger.id);
            }
        }

        for (List<Integer> dependants : reverse.values()) {
            Collections.sort(dependants);
        }

        return reverse;
    }

    Set<Integer> collectDescendants(
            int blockerId,
            Map<Integer, List<Integer>> reverse) {

        Set<Integer> descendants = new LinkedHashSet<>();
        Deque<Integer> stack = new ArrayDeque<>();

        for (int dependant :
                reverse.getOrDefault(blockerId, Collections.emptyList())) {
            stack.push(dependant);
        }

        while (!stack.isEmpty()) {
            int passengerId = stack.pop();

            if (!descendants.add(passengerId)) {
                continue;
            }

            for (int child :
                    reverse.getOrDefault(
                            passengerId,
                            Collections.emptyList())) {
                stack.push(child);
            }
        }

        descendants.remove(blockerId);
        return descendants;
    }

    int dependencyTreeDepth(
            int blockerId,
            Map<Integer, List<Integer>> reverse,
            Set<Integer> visiting) {

        if (!visiting.add(blockerId)) {
            return 0;
        }

        int maximumChildDepth = 0;

        for (int child :
                reverse.getOrDefault(
                        blockerId,
                        Collections.emptyList())) {

            maximumChildDepth = Math.max(
                    maximumChildDepth,
                    dependencyTreeDepth(child, reverse, visiting)
            );
        }

        visiting.remove(blockerId);
        return 1 + maximumChildDepth;
    }

    Passenger criticalBlocker(
            Map<Integer, List<Integer>> reverse) {

        Passenger best = null;
        int bestDescendants = -1;
        int bestDirectFanOut = -1;

        for (Passenger passenger : passengers.values()) {
            if (passenger.state == PassengerState.SEATED) {
                continue;
            }

            int descendants =
                    collectDescendants(passenger.id, reverse).size();

            int directFanOut =
                    reverse.getOrDefault(
                            passenger.id,
                            Collections.emptyList()).size();

            if (descendants > bestDescendants ||
                    (descendants == bestDescendants &&
                     directFanOut > bestDirectFanOut) ||
                    (descendants == bestDescendants &&
                     directFanOut == bestDirectFanOut &&
                     best != null &&
                     passenger.id < best.id)) {

                best = passenger;
                bestDescendants = descendants;
                bestDirectFanOut = directFanOut;
            }
        }

        return best;
    }

    void appendDependencyTree(
            StringBuilder sb,
            int passengerId,
            Map<Integer, List<Integer>> reverse,
            String indent,
            Set<Integer> visited,
            int remainingNodeBudget) {

        if (remainingNodeBudget <= 0) {
            sb.append(indent).append("... tree output truncated ...\n");
            return;
        }

        Passenger passenger = passengers.get(passengerId);

        sb.append(indent)
          .append("P")
          .append(passengerId);

        if (passenger != null) {
            sb.append("(")
              .append(passenger.seatName())
              .append("/")
              .append(passenger.servingAisle)
              .append("/")
              .append(passenger.lastBlockingReason)
              .append(")");
        }

        sb.append("\n");

        if (!visited.add(passengerId)) {
            sb.append(indent)
              .append("  [cycle already visited]\n");
            return;
        }

        List<Integer> children =
                reverse.getOrDefault(
                        passengerId,
                        Collections.emptyList());

        int childBudget =
                Math.max(0, remainingNodeBudget - 1);

        for (int child : children) {
            appendDependencyTree(
                    sb,
                    child,
                    reverse,
                    indent + "  ",
                    visited,
                    childBudget
            );
            childBudget--;
            if (childBudget <= 0 && children.indexOf(child) < children.size() - 1) {
                sb.append(indent)
                  .append("  ... additional branches omitted ...\n");
                break;
            }
        }
    }

    String formatCriticalBlockerTree(
            Passenger blocker,
            Map<Integer, List<Integer>> reverse) {

        if (blocker == null) {
            return "NONE";
        }

        StringBuilder sb = new StringBuilder();
        appendDependencyTree(
                sb,
                blocker.id,
                reverse,
                "",
                new LinkedHashSet<>(),
                80
        );

        return sb.toString().trim();
    }

    int terminalRowEventNodeCount() {
        int count = 0;

        for (Passenger passenger : passengers.values()) {
            if (passenger.state != PassengerState.SEATED &&
                    passenger.directBlockingPassengerId == EMPTY &&
                    "WAITING_FOR_SEAT_EVENT".equals(
                            passenger.lastBlockingReason)) {
                count++;
            }
        }

        return count;
    }

    int frozenComponentCount(
            Map<Integer, List<Integer>> reverse) {

        Set<Integer> unresolvedInternal = new LinkedHashSet<>();

        for (Passenger passenger : passengers.values()) {
            if (passenger.state != PassengerState.SEATED &&
                    passenger.state != PassengerState.WAITING_TO_ENTER) {
                unresolvedInternal.add(passenger.id);
            }
        }

        Map<Integer, Set<Integer>> undirected = new LinkedHashMap<>();

        for (int passengerId : unresolvedInternal) {
            undirected.put(passengerId, new LinkedHashSet<>());
        }

        for (Passenger passenger : passengers.values()) {
            if (!unresolvedInternal.contains(passenger.id)) {
                continue;
            }

            int blockerId = passenger.directBlockingPassengerId;

            if (unresolvedInternal.contains(blockerId)) {
                undirected.get(passenger.id).add(blockerId);
                undirected.get(blockerId).add(passenger.id);
            }
        }

        int components = 0;
        Set<Integer> visited = new LinkedHashSet<>();

        for (int passengerId : unresolvedInternal) {
            if (!visited.add(passengerId)) {
                continue;
            }

            components++;
            Deque<Integer> queue = new ArrayDeque<>();
            queue.add(passengerId);

            while (!queue.isEmpty()) {
                int current = queue.removeFirst();

                for (int neighbour :
                        undirected.getOrDefault(
                                current,
                                Collections.emptySet())) {

                    if (visited.add(neighbour)) {
                        queue.addLast(neighbour);
                    }
                }
            }
        }

        return components;
    }


    List<Integer> parsePassengerIds(String encoded) {
        List<Integer> ids = new ArrayList<>();

        if (encoded == null ||
                encoded.isBlank() ||
                "NONE".equals(encoded)) {
            return ids;
        }

        for (String token : encoded.split("\\|")) {
            try {
                int id = Integer.parseInt(token.trim());
                if (id != EMPTY) {
                    ids.add(id);
                }
            } catch (NumberFormatException ignored) {
                // Best-effort diagnostics.
            }
        }

        return ids;
    }

    DependencyTrace traceFromPassenger(int startPassengerId) {
        DependencyTrace trace = new DependencyTrace();
        Map<Integer, Integer> firstIndex =
                new LinkedHashMap<>();

        int currentId = startPassengerId;

        while (currentId != EMPTY) {
            Passenger current = passengers.get(currentId);

            if (current == null) {
                trace.terminalCondition =
                        "DISCONNECTED_OR_STALE_REFERENCE";
                trace.terminalPassengerId = currentId;
                return trace;
            }

            Integer seenAt = firstIndex.get(currentId);

            if (seenAt != null) {
                trace.cycleDetected = true;

                for (int i = seenAt;
                     i < trace.path.size();
                     i++) {
                    trace.cycleMembers.add(trace.path.get(i));
                }

                trace.cycleMembers.add(currentId);
                trace.terminalCondition =
                        "CLOSED_PASSENGER_CYCLE";
                trace.terminalPassengerId = currentId;
                return trace;
            }

            firstIndex.put(currentId, trace.path.size());
            trace.path.add(currentId);

            if (current.directBlockingPassengerId != EMPTY) {
                currentId = current.directBlockingPassengerId;
                continue;
            }

            trace.terminalPassengerId = current.id;

            if ("INSUFFICIENT_REAR_YIELD_SPACE".equals(
                    current.lastPrerequisiteFailure) ||
                    "AISLE_BOUNDARY".equals(
                            current.lastBlockingReason)) {
                trace.terminalCondition =
                        "REAR_BOUNDARY_LOCK";
            } else if ("MIDDLE_BANK_RESERVATION_DENIED".equals(
                    current.lastPrerequisiteFailure) ||
                    "MIDDLE_BANK_RESERVED".equals(
                            current.lastBlockingReason)) {
                trace.terminalCondition =
                        "MIDDLE_BANK_RESERVATION_CHAIN";
            } else if ("ANOTHER_ACTIVE_SEAT_EVENT".equals(
                    current.lastPrerequisiteFailure)) {
                trace.terminalCondition =
                        "ACTIVE_EVENT_TRANSIENT";
            } else if ("WAITING_FOR_SEAT_EVENT".equals(
                    current.lastBlockingReason)) {
                trace.terminalCondition =
                        "OPEN_CHAIN_TO_ROW_EVENT";
            } else {
                trace.terminalCondition =
                        current.lastBlockingReason == null
                                ? "UNCLASSIFIED"
                                : current.lastBlockingReason;
            }

            return trace;
        }

        trace.terminalCondition = "NO_YIELD_OCCUPANT";
        return trace;
    }

    String passengerTraceDetails(int passengerId) {
        Passenger p = passengers.get(passengerId);

        if (p == null) {
            return "P" + passengerId + "{STALE_REFERENCE}";
        }

        return "P" + p.id +
                "{seat=" + p.seatName() +
                ",aisle=" + p.servingAisle +
                ",state=" + p.state +
                ",aisleTile=" + (p.lastAislePosition + 1) +
                ",directBlocker=" +
                p.directBlockingPassengerId +
                ",lastMoveTick=" + p.lastMovementTick +
                ",reason=" + p.lastBlockingReason +
                ",prerequisite=" +
                p.lastPrerequisiteFailure +
                "}";
    }

    boolean allOccupantsSameComponent(
            List<Integer> occupantIds) {

        if (occupantIds.size() <= 1) {
            return true;
        }

        Map<Integer, Set<Integer>> graph =
                new LinkedHashMap<>();

        for (Passenger p : passengers.values()) {
            if (p.state == PassengerState.SEATED ||
                    p.state == PassengerState.WAITING_TO_ENTER) {
                continue;
            }

            graph.computeIfAbsent(
                    p.id,
                    ignored -> new LinkedHashSet<>());

            if (p.directBlockingPassengerId != EMPTY &&
                    passengers.containsKey(
                            p.directBlockingPassengerId)) {
                graph.computeIfAbsent(
                        p.directBlockingPassengerId,
                        ignored -> new LinkedHashSet<>());

                graph.get(p.id).add(
                        p.directBlockingPassengerId);
                graph.get(p.directBlockingPassengerId)
                        .add(p.id);
            }
        }

        int start = occupantIds.get(0);

        if (!graph.containsKey(start)) {
            return false;
        }

        Set<Integer> reached = new LinkedHashSet<>();
        Deque<Integer> queue = new ArrayDeque<>();
        queue.add(start);
        reached.add(start);

        while (!queue.isEmpty()) {
            int current = queue.removeFirst();

            for (int next :
                    graph.getOrDefault(
                            current,
                            Collections.emptySet())) {
                if (reached.add(next)) {
                    queue.addLast(next);
                }
            }
        }

        return reached.containsAll(occupantIds);
    }

    YieldDependencyType classifyYieldDependency(
            List<DependencyTrace> traces,
            int occupantCount) {

        if (occupantCount == 0) {
            return YieldDependencyType.NO_YIELD_OCCUPANT;
        }

        for (DependencyTrace trace : traces) {
            if (trace.cycleDetected) {
                return YieldDependencyType
                        .CLOSED_PASSENGER_CYCLE;
            }
        }

        Set<String> terminalTypes = new LinkedHashSet<>();

        for (DependencyTrace trace : traces) {
            terminalTypes.add(trace.terminalCondition);
        }

        if (terminalTypes.size() > 1) {
            return YieldDependencyType
                    .MIXED_MULTIPLE_YIELD_OCCUPANTS;
        }

        if (terminalTypes.isEmpty()) {
            return YieldDependencyType
                    .DISCONNECTED_OR_STALE_REFERENCE;
        }

        switch (terminalTypes.iterator().next()) {
            case "OPEN_CHAIN_TO_ROW_EVENT":
                return YieldDependencyType
                        .OPEN_CHAIN_TO_ROW_EVENT;
            case "REAR_BOUNDARY_LOCK":
                return YieldDependencyType
                        .REAR_BOUNDARY_LOCK;
            case "ACTIVE_EVENT_TRANSIENT":
                return YieldDependencyType
                        .ACTIVE_EVENT_TRANSIENT;
            case "MIDDLE_BANK_RESERVATION_CHAIN":
                return YieldDependencyType
                        .MIDDLE_BANK_RESERVATION_CHAIN;
            case "DISCONNECTED_OR_STALE_REFERENCE":
                return YieldDependencyType
                        .DISCONNECTED_OR_STALE_REFERENCE;
            default:
                return YieldDependencyType.UNCLASSIFIED;
        }
    }


    AisleContext aisleContextFor(Passenger passenger) {
        return passenger.servingAisle == AisleSide.LEFT
                ? left
                : right;
    }

    FinalSnapshotAnalysis analyseFinalSnapshotFor(
            Passenger passenger) {

        FinalSnapshotAnalysis analysis =
                new FinalSnapshotAnalysis();

        if (passenger == null) {
            analysis.type =
                    FinalSnapshotDependencyType
                            .STALE_OR_DISCONNECTED_REFERENCE;
            analysis.prerequisite =
                    "STALE_OR_DISCONNECTED_REFERENCE";
            analysis.terminalCondition =
                    analysis.prerequisite;
            return analysis;
        }

        AisleContext context =
                aisleContextFor(passenger);

        List<Integer> blockerSeats =
                findBlockingSeatIndices(passenger);

        List<Integer> blockerIds = new ArrayList<>();

        for (int seatIndex : blockerSeats) {
            int blockerId =
                    seats[passenger.row][seatIndex];

            if (blockerId != EMPTY) {
                blockerIds.add(blockerId);
            }
        }

        analysis.requiredBlockers = blockerIds.size();
        analysis.simultaneousEmptyTilesRequired =
                blockerIds.size();
        analysis.activeEventExists =
                context.activeEvent != null;

        int reservationHolder =
                middleBankReservedByPassenger[passenger.row];

        analysis.reservationConflictExists =
                isMiddleBankSeat(passenger.seatIndex) &&
                reservationHolder != EMPTY &&
                reservationHolder != passenger.id;

        boolean boundaryFailure = false;

        for (int offset = 1;
             offset <= blockerIds.size();
             offset++) {

            int tile = passenger.row + offset;
            analysis.requiredTiles.add(tile + 1);

            if (tile >= context.aisle.length) {
                boundaryFailure = true;
                analysis.unavailableTiles.add(tile + 1);
                analysis.occupantIds.add(EMPTY);
                continue;
            }

            int occupantId = context.aisle[tile];

            if (occupantId != EMPTY) {
                analysis.unavailableTiles.add(tile + 1);
                analysis.occupantIds.add(occupantId);
            }
        }

        if (boundaryFailure) {
            analysis.type =
                    FinalSnapshotDependencyType
                            .REAR_BOUNDARY_LOCK;
            analysis.prerequisite =
                    "INSUFFICIENT_REAR_YIELD_SPACE";
            analysis.terminalCondition =
                    analysis.prerequisite;
            return analysis;
        }

        if (analysis.activeEventExists) {
            analysis.type =
                    FinalSnapshotDependencyType
                            .CURRENT_ACTIVE_EVENT;
            analysis.prerequisite =
                    "CURRENT_ACTIVE_EVENT";
            analysis.terminalCondition =
                    analysis.prerequisite;
            return analysis;
        }

        if (analysis.reservationConflictExists) {
            analysis.type =
                    FinalSnapshotDependencyType
                            .CURRENT_RESERVATION_CONFLICT;
            analysis.prerequisite =
                    "CURRENT_RESERVATION_CONFLICT";
            analysis.terminalCondition =
                    analysis.prerequisite;
            return analysis;
        }

        if (analysis.unavailableTiles.isEmpty()) {
            analysis.type =
                    FinalSnapshotDependencyType
                            .YIELD_SPACE_AVAILABLE;
            analysis.prerequisite =
                    "YIELD_SPACE_AVAILABLE";
            analysis.terminalCondition =
                    analysis.prerequisite;
            return analysis;
        }

        analysis.prerequisite =
                "YIELD_TILE_OCCUPIED";

        Set<Integer> visited =
                new LinkedHashSet<>();

        Passenger current = passenger;

        while (current != null &&
                visited.add(current.id)) {

            analysis.rowEventChain.add(current.id);

            FinalSnapshotAnalysis currentAnalysis =
                    current == passenger
                            ? analysis
                            : analyseSingleStepFinalSnapshot(current);

            if (currentAnalysis.type ==
                    FinalSnapshotDependencyType
                            .REAR_BOUNDARY_LOCK) {
                analysis.terminalCondition =
                        "REAR_BOUNDARY_LOCK";
                break;
            }

            if (currentAnalysis.occupantIds.isEmpty()) {
                analysis.terminalCondition =
                        current.state ==
                                PassengerState.WAITING_AT_ROW
                                ? "ROW_EVENT_WITH_AVAILABLE_SPACE"
                                : "OPEN_CHAIN_TERMINAL";
                break;
            }

            int nextId =
                    currentAnalysis.occupantIds.get(0);

            Passenger nextPassenger =
                    passengers.get(nextId);

            if (nextPassenger == null) {
                analysis.terminalCondition =
                        "STALE_OR_DISCONNECTED_REFERENCE";
                break;
            }

            if (nextPassenger.state ==
                    PassengerState.WAITING_AT_ROW) {
                analysis.rowEventChain.add(
                        nextPassenger.id);
                analysis.terminalCondition =
                        "ANOTHER_WAITING_ROW_PASSENGER";
                break;
            }

            current = nextPassenger;
        }

        if (analysis.occupantIds.size() > 1) {
            analysis.type =
                    FinalSnapshotDependencyType
                            .MULTIPLE_MIXED_OCCUPANTS;
        } else {
            int firstOccupantId =
                    analysis.occupantIds.get(0);

            Passenger firstOccupant =
                    passengers.get(firstOccupantId);

            if (firstOccupant != null &&
                    firstOccupant.state ==
                            PassengerState.WAITING_AT_ROW) {
                analysis.type =
                        FinalSnapshotDependencyType
                                .LINKED_ROW_EVENT_CHAIN;
            } else {
                analysis.type =
                        FinalSnapshotDependencyType
                                .YIELD_TILE_OCCUPIED_BY_MOVING_PASSENGER;
            }
        }

        // Analytical counterfactuals only.
        analysis.oneEmptyTileWouldStart =
                analysis.requiredBlockers == 1 &&
                analysis.unavailableTiles.size() == 1;

        analysis.oneTileEarlierHoldWouldPreserve =
                !analysis.unavailableTiles.isEmpty() &&
                analysis.unavailableTiles.size() <=
                        Math.max(1, analysis.requiredBlockers);

        return analysis;
    }

    FinalSnapshotAnalysis analyseSingleStepFinalSnapshot(
            Passenger passenger) {

        FinalSnapshotAnalysis analysis =
                new FinalSnapshotAnalysis();

        if (passenger == null) {
            analysis.type =
                    FinalSnapshotDependencyType
                            .STALE_OR_DISCONNECTED_REFERENCE;
            return analysis;
        }

        AisleContext context =
                aisleContextFor(passenger);

        if (passenger.state !=
                PassengerState.WAITING_AT_ROW) {

            if (passenger.directBlockingPassengerId != EMPTY) {
                analysis.occupantIds.add(
                        passenger.directBlockingPassengerId);
                analysis.type =
                        FinalSnapshotDependencyType
                                .YIELD_TILE_OCCUPIED_BY_MOVING_PASSENGER;
            } else {
                analysis.type =
                        FinalSnapshotDependencyType
                                .STALE_OR_DISCONNECTED_REFERENCE;
            }

            return analysis;
        }

        List<Integer> blockerSeats =
                findBlockingSeatIndices(passenger);

        int blockerCount = 0;

        for (int seatIndex : blockerSeats) {
            if (seats[passenger.row][seatIndex] != EMPTY) {
                blockerCount++;
            }
        }

        analysis.requiredBlockers = blockerCount;

        for (int offset = 1;
             offset <= blockerCount;
             offset++) {

            int tile = passenger.row + offset;
            analysis.requiredTiles.add(tile + 1);

            if (tile >= context.aisle.length) {
                analysis.type =
                        FinalSnapshotDependencyType
                                .REAR_BOUNDARY_LOCK;
                analysis.unavailableTiles.add(tile + 1);
                analysis.occupantIds.add(EMPTY);
                return analysis;
            }

            int occupantId = context.aisle[tile];

            if (occupantId != EMPTY) {
                analysis.unavailableTiles.add(tile + 1);
                analysis.occupantIds.add(occupantId);
            }
        }

        if (analysis.unavailableTiles.isEmpty()) {
            analysis.type =
                    FinalSnapshotDependencyType
                            .YIELD_SPACE_AVAILABLE;
        } else {
            analysis.type =
                    FinalSnapshotDependencyType
                            .LINKED_ROW_EVENT_CHAIN;
        }

        return analysis;
    }

    int countFinalRowEventDependencyChains() {
        int count = 0;

        for (Passenger passenger : passengers.values()) {
            if (passenger.state !=
                    PassengerState.WAITING_AT_ROW) {
                continue;
            }

            FinalSnapshotAnalysis analysis =
                    analyseFinalSnapshotFor(passenger);

            if (analysis.type ==
                        FinalSnapshotDependencyType
                                .LINKED_ROW_EVENT_CHAIN ||
                    analysis.type ==
                        FinalSnapshotDependencyType
                                .YIELD_TILE_OCCUPIED_BY_MOVING_PASSENGER ||
                    analysis.type ==
                        FinalSnapshotDependencyType
                                .MULTIPLE_MIXED_OCCUPANTS ||
                    analysis.type ==
                        FinalSnapshotDependencyType
                                .REAR_BOUNDARY_LOCK) {
                count++;
            }
        }

        return count;
    }

    int currentSeatedCount() {
        int count = 0;
        for (Passenger passenger : passengers.values()) {
            if (passenger.state == PassengerState.SEATED) {
                count++;
            }
        }
        return count;
    }

    void updateGatekeeperEpisodes() {
        int seatedNow = currentSeatedCount();

        for (GatekeeperEpisode e : gatekeeperEpisodes) {
            if (e.closed) {
                continue;
            }

            AisleContext context =
                    e.aisle == AisleSide.LEFT ? left : right;

            if (!e.releaseConfirmed &&
                    e.expectedReleaseTile >= 0 &&
                    e.expectedReleaseTile < context.aisle.length &&
                    context.aisle[e.expectedReleaseTile] == EMPTY) {
                e.releaseConfirmed = true;
                gatekeeperConfirmedReleaseTiles++;
            }

            Passenger upstream = passengers.get(e.upstreamId);
            if (e.upstreamStartedTick < 0 &&
                    upstream != null &&
                    (upstream.state == PassengerState.IN_SEAT_EVENT ||
                     upstream.state == PassengerState.SEATED)) {
                e.upstreamStartedTick = currentTick;
                int delay = currentTick - e.selectionTick;
                if (delay <= 1) gatekeeperUpstreamStartsWithin1++;
                if (delay <= 2) gatekeeperUpstreamStartsWithin2++;
                if (delay <= 5) gatekeeperUpstreamStartsWithin5++;
                if (delay <= 10) gatekeeperUpstreamStartsWithin10++;
            }

            if (currentTick - e.selectionTick >= 10) {
                e.gainAt10Ticks = seatedNow - e.seatedAtSelection;
                e.closed = true;

                if (e.gainAt10Ticks <= 0) {
                    gatekeeperEpisodesWithoutEffect++;
                }

                if (e.gainAt10Ticks > gatekeeperHighestTenTickGain) {
                    gatekeeperHighestTenTickGain = e.gainAt10Ticks;
                    gatekeeperHighestValueEpisodeId = e.id;
                }
            }
        }
    }

    String gatekeeperEpisodeSummary() {
        if (gatekeeperEpisodes.isEmpty()) {
            return "NONE";
        }

        StringBuilder sb = new StringBuilder();
        for (GatekeeperEpisode e : gatekeeperEpisodes) {
            if (sb.length() > 0) sb.append(" || ");
            sb.append("E").append(e.id)
              .append(":upstream=").append(e.upstreamId)
              .append(",gatekeeper=").append(e.gatekeeperId)
              .append(",tile=").append(e.expectedReleaseTile + 1)
              .append(",started=").append(e.gatekeeperStarted)
              .append(",released=").append(e.releaseConfirmed)
              .append(",upstreamStart=").append(e.upstreamStartedTick)
              .append(",gain10=").append(e.gainAt10Ticks);
        }
        return sb.toString();
    }

    ScenarioResult buildScenarioResult() {
        // EVOLVED MEASUREMENT LAYER:
        // The original backbone counted moves, moving entities and average movement.
        // This experiment preserves that evidence-oriented approach and extends it
        // with waiting, blocking, reservation and completion statistics.
        ScenarioResult result = new ScenarioResult();

        result.scenarioNumber = config.scenarioNumber;
        result.seed = config.seed;
        result.cabin = config.cabin.name;
        result.rows = rows;
        result.seatsPerRow = seatLetters.length;
        result.totalSeats = config.cabin.totalSeats();
        result.passengers = passengers.size();
        result.occupancyRate = config.occupancyRate;
        result.minHeadway = config.minEntryHeadway;
        result.maxHeadway = config.maxEntryHeadway;
        result.completedTicks = currentTick;
        result.passengersSeated =
                countPassengersInState(PassengerState.SEATED);
        result.completed =
                result.passengersSeated == result.passengers;

        result.leftMoves = left.totalMoves;
        result.rightMoves = right.totalMoves;
        result.leftMaxOccupied = left.maxOccupiedTiles;
        result.rightMaxOccupied = right.maxOccupiedTiles;
        result.leftSeatEvents = left.seatEvents;
        result.rightSeatEvents = right.seatEvents;
        result.leftSingleBlockers = left.singleBlockerEvents;
        result.rightSingleBlockers = right.singleBlockerEvents;
        result.leftMultipleBlockers = left.multipleBlockerEvents;
        result.rightMultipleBlockers = right.multipleBlockerEvents;
        result.leftTemporaryAisleReoccupations =
                left.temporaryAisleReoccupations;
        result.rightTemporaryAisleReoccupations =
                right.temporaryAisleReoccupations;

        result.middleBankReservationRequests =
                middleBankReservationRequests;
        result.middleBankReservationGranted =
                middleBankReservationGranted;
        result.middleBankReservationDenied =
                middleBankReservationDenied;
        result.simultaneousMiddleBankConflicts =
                simultaneousMiddleBankConflicts;

        int totalWaiting = 0;
        int longestWaiting = -1;
        Passenger longestPassenger = null;

        for (Passenger passenger : passengers.values()) {
            totalWaiting += passenger.waitingTicks;

            if (passenger.waitingTicks > longestWaiting) {
                longestWaiting = passenger.waitingTicks;
                longestPassenger = passenger;
            }
        }

        result.averageWaitingTicks =
                passengers.isEmpty()
                        ? 0.0
                        : (double) totalWaiting / passengers.size();

        result.longestWaitingTicks =
                Math.max(0, longestWaiting);

        result.longestWaitingPassenger =
                longestPassenger == null
                        ? ""
                        : longestPassenger.toString();

        result.stalled = stallDetected;
        result.ticksWithoutNewSeat = currentTick - lastSeatProgressTick;
        result.remainingWaitingToEnter =
                countPassengersInState(PassengerState.WAITING_TO_ENTER);
        result.remainingMovingInAisle =
                countPassengersInState(PassengerState.MOVING_IN_AISLE);
        result.remainingWaitingAtRow =
                countPassengersInState(PassengerState.WAITING_AT_ROW);
        result.remainingInSeatEvent =
                countPassengersInState(PassengerState.IN_SEAT_EVENT);
        result.remainingSeated =
                countPassengersInState(PassengerState.SEATED);
        result.dominantBlockingReason = dominantBlockingReason();
        result.dominantBlockingReasonCount = dominantBlockingReasonCount();

        Passenger stalledPassenger = longestUnresolvedPassenger();
        if (stalledPassenger != null) {
            result.stalledPassengerId = stalledPassenger.id;
            result.stalledRow = stalledPassenger.row + 1;
            result.stalledAisle = stalledPassenger.servingAisle.toString();
        } else {
            result.stalledPassengerId = -1;
            result.stalledRow = -1;
            result.stalledAisle = "";
        }

        Passenger dependencyRoot = longestDependencyRoot();
        List<Integer> longestChain =
                dependencyRoot == null
                        ? Collections.emptyList()
                        : buildDependencyChainFrom(dependencyRoot);

        result.longestDependencyChainLength = longestChain.size();
        result.longestDependencyChain = formatDependencyChain(longestChain);
        result.dependencyRootPassengerId =
                dependencyRoot == null ? EMPTY : dependencyRoot.id;

        Passenger terminalPassenger =
                longestChain.isEmpty()
                        ? null
                        : passengers.get(longestChain.get(longestChain.size() - 1));

        result.dependencyTerminalReason =
                terminalReasonFor(terminalPassenger);
        result.passengersIndirectlyAffected =
                countIndirectlyAffectedBy(result.dependencyRootPassengerId);
        result.passengersWithDirectBlocker =
                countPassengersWithDirectBlocker();

        for (Passenger passenger : passengers.values()) {
            result.totalBlockedTicks += passenger.blockedTicks;
            result.totalStationaryTicks += passenger.stationaryTicks;
            result.totalSeatEventRequests += passenger.seatEventRequests;
            result.totalFailedSeatEventStarts += passenger.failedSeatEventStarts;
        }

        Map<Integer, List<Integer>> reverseDependencyGraph =
                buildReverseDependencyGraph();

        Passenger criticalBlocker =
                criticalBlocker(reverseDependencyGraph);

        result.criticalBlockerPassengerId =
                criticalBlocker == null
                        ? EMPTY
                        : criticalBlocker.id;

        result.criticalBlockerIdentity =
                criticalBlocker == null
                        ? "NONE"
                        : criticalBlocker.toString();

        result.criticalBlockerDirectFanOut =
                criticalBlocker == null
                        ? 0
                        : reverseDependencyGraph
                                .getOrDefault(
                                        criticalBlocker.id,
                                        Collections.emptyList())
                                .size();

        result.criticalBlockerTotalDescendants =
                criticalBlocker == null
                        ? 0
                        : collectDescendants(
                                criticalBlocker.id,
                                reverseDependencyGraph)
                                .size();

        result.criticalBlockerTreeDepth =
                criticalBlocker == null
                        ? 0
                        : dependencyTreeDepth(
                                criticalBlocker.id,
                                reverseDependencyGraph,
                                new LinkedHashSet<>());

        result.criticalBlockerTreeSize =
                criticalBlocker == null
                        ? 0
                        : 1 + result.criticalBlockerTotalDescendants;

        result.criticalBlockerTree =
                formatCriticalBlockerTree(
                        criticalBlocker,
                        reverseDependencyGraph);

        result.frozenComponentCount =
                frozenComponentCount(reverseDependencyGraph);

        result.terminalRowEventNodeCount =
                terminalRowEventNodeCount();

        result.unresolvedOutsidePassengers =
                countPassengersInState(
                        PassengerState.WAITING_TO_ENTER);

        result.unresolvedInternalPassengers =
                countPassengersInState(
                        PassengerState.MOVING_IN_AISLE) +
                countPassengersInState(
                        PassengerState.WAITING_AT_ROW) +
                countPassengersInState(
                        PassengerState.IN_SEAT_EVENT);

        result.unresolvedInternalNotInCriticalTree =
                Math.max(
                        0,
                        result.unresolvedInternalPassengers -
                        result.criticalBlockerTreeSize
                );

        result.dominantPrerequisiteFailure =
                dominantPrerequisiteFailure();
        result.dominantPrerequisiteFailureCount =
                dominantPrerequisiteFailureCount();

        Passenger prerequisiteCritical =
                passengers.get(result.criticalBlockerPassengerId);

        if (prerequisiteCritical != null) {
            result.criticalBlockerFinalPrerequisite =
                    prerequisiteCritical.lastPrerequisiteFailure;
            result.criticalBlockerRequiredBlockers =
                    prerequisiteCritical.lastRequiredBlockerCount;
            result.criticalBlockerRequiredYieldTiles =
                    prerequisiteCritical.lastRequiredYieldTiles;
            result.criticalBlockerUnavailableYieldTiles =
                    prerequisiteCritical.lastUnavailableYieldTiles;
            result.criticalBlockerYieldTileOccupants =
                    prerequisiteCritical.lastYieldTileOccupants;
        } else {
            result.criticalBlockerFinalPrerequisite = "NONE";
            result.criticalBlockerRequiredYieldTiles = "NONE";
            result.criticalBlockerUnavailableYieldTiles = "NONE";
            result.criticalBlockerYieldTileOccupants = "NONE";
        }

        for (Passenger passenger : passengers.values()) {
            if (passenger.state == PassengerState.WAITING_AT_ROW) {
                if (passenger.lastRequiredBlockerCount == 0) {
                    result.rowPassengersZeroBlockers++;
                } else if (passenger.lastRequiredBlockerCount == 1) {
                    result.rowPassengersOneBlocker++;
                } else {
                    result.rowPassengersMultipleBlockers++;
                }
            }
        }

        result.totalYieldTileOccupiedFailures =
                totalYieldTileOccupiedFailures;
        result.totalRearBoundaryFailures =
                totalRearBoundaryFailures;
        result.totalReservationFailures =
                totalReservationFailures;
        result.totalActiveEventDeferrals =
                totalActiveEventDeferrals;
        result.totalArbitrationLosses =
                totalArbitrationLosses;

        List<Integer> yieldOccupants =
                prerequisiteCritical == null
                        ? Collections.emptyList()
                        : parsePassengerIds(
                                prerequisiteCritical
                                        .lastYieldTileOccupants);

        List<DependencyTrace> traces = new ArrayList<>();
        StringBuilder traceText = new StringBuilder();
        StringBuilder details = new StringBuilder();
        LinkedHashSet<Integer> cycleMembers =
                new LinkedHashSet<>();
        LinkedHashSet<Integer> rowTerminals =
                new LinkedHashSet<>();

        int maximumTraceLength = 0;
        String yieldTerminalPassenger = "NONE";
        String yieldTerminalCondition = "NONE";

        for (int i = 0; i < yieldOccupants.size(); i++) {
            int occupantId = yieldOccupants.get(i);
            DependencyTrace trace =
                    traceFromPassenger(occupantId);
            traces.add(trace);

            maximumTraceLength = Math.max(
                    maximumTraceLength,
                    trace.path.size());

            if (i > 0) {
                traceText.append(" || ");
                details.append(" || ");
            }

            traceText.append("yieldOccupant[")
                    .append(occupantId)
                    .append("]: ")
                    .append(trace.format(passengers));

            details.append(
                    passengerTraceDetails(occupantId));

            cycleMembers.addAll(trace.cycleMembers);

            if ("OPEN_CHAIN_TO_ROW_EVENT".equals(
                    trace.terminalCondition) &&
                    trace.terminalPassengerId != null) {
                rowTerminals.add(
                        trace.terminalPassengerId);
            }

            if (trace.terminalPassengerId != null) {
                Passenger terminal =
                        passengers.get(
                                trace.terminalPassengerId);
                yieldTerminalPassenger =
                        terminal == null
                                ? "P" +
                                  trace.terminalPassengerId
                                : terminal.toString();
            }

            yieldTerminalCondition =
                    trace.terminalCondition;
        }

        result.requiredYieldOccupantCount =
                yieldOccupants.size();
        result.criticalYieldOccupantTrace =
                traceText.length() == 0
                        ? "NONE"
                        : traceText.toString();
        result.criticalYieldOccupantTraceLength =
                maximumTraceLength;
        result.dependencyCycleDetected =
                !cycleMembers.isEmpty();
        result.dependencyCycleMembers =
                cycleMembers.isEmpty()
                        ? "NONE"
                        : joinIntegers(
                                new ArrayList<>(cycleMembers));
        result.dependencyTerminalPassenger =
                yieldTerminalPassenger;
        result.dependencyTerminalCondition =
                yieldTerminalCondition;
        result.distinctRowEventTerminals =
                rowTerminals.size();
        result.allYieldOccupantsSameFrozenComponent =
                allOccupantsSameComponent(yieldOccupants);
        result.finalStablePrerequisite =
                prerequisiteCritical == null
                        ? "NONE"
                        : prerequisiteCritical
                                .lastPrerequisiteFailure;
        result.criticalYieldOccupantDetails =
                details.length() == 0
                        ? "NONE"
                        : details.toString();
        result.yieldDependencyType =
                classifyYieldDependency(
                        traces,
                        yieldOccupants.size());

        FinalSnapshotAnalysis finalAnalysis =
                analyseFinalSnapshotFor(
                        prerequisiteCritical);

        result.finalSnapshotDependencyType =
                finalAnalysis.type;
        result.finalSnapshotPrerequisite =
                finalAnalysis.prerequisite;
        result.finalSnapshotRequiredBlockers =
                finalAnalysis.requiredBlockers;
        result.finalSnapshotRequiredYieldTiles =
                joinIntegers(finalAnalysis.requiredTiles);
        result.finalSnapshotUnavailableYieldTiles =
                joinIntegers(finalAnalysis.unavailableTiles);
        result.finalSnapshotYieldOccupants =
                joinIntegers(finalAnalysis.occupantIds);
        result.finalSnapshotRowEventChain =
                joinIntegers(finalAnalysis.rowEventChain);
        result.finalSnapshotRowEventChainLength =
                finalAnalysis.rowEventChain.size();

        Set<Integer> rowEventIds =
                new LinkedHashSet<>(
                        finalAnalysis.rowEventChain);
        result.finalSnapshotDistinctRowEvents =
                rowEventIds.size();
        result.finalSnapshotTerminalCondition =
                finalAnalysis.terminalCondition;
        result.oneEmptyTileWouldStartEvent =
                finalAnalysis.oneEmptyTileWouldStart;
        result.oneTileEarlierHoldWouldPreserveSpace =
                finalAnalysis.oneTileEarlierHoldWouldPreserve;
        result.simultaneousEmptyTilesRequired =
                finalAnalysis.simultaneousEmptyTilesRequired;
        result.finalRowEventDependencyChainCount =
                countFinalRowEventDependencyChains();
        result.activeEventExistsAtStall =
                finalAnalysis.activeEventExists;
        result.reservationConflictExistsAtStall =
                finalAnalysis.reservationConflictExists;

        result.releaseImminentProtectionEnabled =
                config.releaseImminentProtectionEnabled;
        result.experimentMode =
                config.releaseImminentProtectionEnabled
                        ? "DEPENDENCY_AWARE_PRECEDENCE"
                        : "BASELINE";
        // Legacy storage fields are retained for CSV compatibility, but in
        // Experiment 11 they carry dependency-aware scheduling evidence.
        result.releaseImminentEvaluations =
                precedenceCandidatesExamined;
        result.releaseImminentWindowsActivated =
                precedenceSubstitutionsMade;
        result.releaseImminentTilesProtected =
                precedenceChainsFollowed;
        result.releaseImminentPassengerHolds =
                precedenceBlockedByRowEvent;
        result.releaseImminentNextTickEventStarts =
                precedenceSubstitutedEventsStarted;
        result.releaseImminentWindowsWithoutEvent =
                precedenceRejectedOccupiedSpace;
        result.lastProtectedCandidateId =
                precedenceMaximumChainDepth;

        result.gatekeeperUniqueEpisodes = gatekeeperEpisodes.size();
        result.gatekeeperRepeatedSelections = gatekeeperRepeatedSelections;
        result.gatekeeperEventStarts = precedenceSubstitutedEventsStarted;
        result.gatekeeperConfirmedReleaseTiles =
                gatekeeperConfirmedReleaseTiles;
        result.gatekeeperUpstreamStartsWithin1 =
                gatekeeperUpstreamStartsWithin1;
        result.gatekeeperUpstreamStartsWithin2 =
                gatekeeperUpstreamStartsWithin2;
        result.gatekeeperUpstreamStartsWithin5 =
                gatekeeperUpstreamStartsWithin5;
        result.gatekeeperUpstreamStartsWithin10 =
                gatekeeperUpstreamStartsWithin10;
        result.gatekeeperEpisodesWithoutEffect =
                gatekeeperEpisodesWithoutEffect;
        result.gatekeeperHighestTenTickGain =
                gatekeeperHighestTenTickGain;
        result.gatekeeperHighestValueEpisodeId =
                gatekeeperHighestValueEpisodeId;
        result.gatekeeperEpisodeSummary = gatekeeperEpisodeSummary();

        return result;
    }

    // ======================== FILE OUTPUT ========================

    static void ensureResultsDirectory() {
        try {
            Path parent = HUMAN_RESULTS_PATH.getParent();

            if (parent != null) {
                Files.createDirectories(parent);
            }

        } catch (IOException ex) {
            throw new IllegalStateException(
                    "Unable to create results directory: " +
                    HUMAN_RESULTS_PATH.getParent(),
                    ex
            );
        }
    }

    static void ensureCsvHeader() {
        if (Files.exists(CSV_RESULTS_PATH)) {
            return;
        }

        String header =
                "experimentNumber,experimentTitle,timestamp,experimentSeed,scenario,scenarioSeed," +
                "cabin,rows,seatsPerRow,totalSeats,passengers," +
                "occupancyPercent,minHeadway,maxHeadway," +
                "completedTicks,completed,passengersSeated," +
                "leftMoves,rightMoves,leftMaxOccupied,rightMaxOccupied," +
                "leftSeatEvents,rightSeatEvents," +
                "leftSingleBlockers,rightSingleBlockers," +
                "leftMultipleBlockers,rightMultipleBlockers," +
                "leftTemporaryAisleReoccupations," +
                "rightTemporaryAisleReoccupations," +
                "middleBankReservationRequests," +
                "middleBankReservationGranted," +
                "middleBankReservationDenied," +
                "simultaneousMiddleBankConflicts," +
                "averageWaitingTicks,longestWaitingTicks," +
                "longestWaitingPassenger,stalled,ticksWithoutNewSeat," +
                "remainingWaitingToEnter,remainingMovingInAisle," +
                "remainingWaitingAtRow,remainingInSeatEvent," +
                "dominantBlockingReason,dominantBlockingReasonCount," +
                "stalledPassengerId,stalledRow,stalledAisle," +
                "longestDependencyChainLength,longestDependencyChain," +
                "dependencyRootPassengerId,dependencyTerminalReason," +
                "passengersIndirectlyAffected,passengersWithDirectBlocker," +
                "totalBlockedTicks,totalStationaryTicks," +
                "totalSeatEventRequests,totalFailedSeatEventStarts," +
                "criticalBlockerPassengerId,criticalBlockerIdentity," +
                "criticalBlockerDirectFanOut,criticalBlockerTotalDescendants," +
                "criticalBlockerTreeSize,criticalBlockerTreeDepth," +
                "frozenComponentCount,terminalRowEventNodeCount," +
                "unresolvedInternalPassengers,unresolvedOutsidePassengers," +
                "unresolvedInternalNotInCriticalTree,criticalBlockerTree," +
                "dominantPrerequisiteFailure,dominantPrerequisiteFailureCount," +
                "criticalBlockerFinalPrerequisite," +
                "criticalBlockerRequiredBlockers," +
                "criticalBlockerRequiredYieldTiles," +
                "criticalBlockerUnavailableYieldTiles," +
                "criticalBlockerYieldTileOccupants," +
                "rowPassengersZeroBlockers,rowPassengersOneBlocker," +
                "rowPassengersMultipleBlockers," +
                "totalYieldTileOccupiedFailures,totalRearBoundaryFailures," +
                "totalReservationFailures,totalActiveEventDeferrals," +
                "totalArbitrationLosses,yieldDependencyType," +
                "criticalYieldOccupantTrace,criticalYieldOccupantTraceLength," +
                "dependencyCycleDetected,dependencyCycleMembers," +
                "dependencyTerminalPassenger,dependencyTerminalCondition," +
                "distinctRowEventTerminals,requiredYieldOccupantCount," +
                "allYieldOccupantsSameFrozenComponent,finalStablePrerequisite," +
                "criticalYieldOccupantDetails," +
                "finalSnapshotDependencyType,finalSnapshotPrerequisite," +
                "finalSnapshotRequiredBlockers," +
                "finalSnapshotRequiredYieldTiles," +
                "finalSnapshotUnavailableYieldTiles," +
                "finalSnapshotYieldOccupants," +
                "finalSnapshotRowEventChain," +
                "finalSnapshotRowEventChainLength," +
                "finalSnapshotDistinctRowEvents," +
                "finalSnapshotTerminalCondition," +
                "oneEmptyTileWouldStartEvent," +
                "oneTileEarlierHoldWouldPreserveSpace," +
                "simultaneousEmptyTilesRequired," +
                "finalRowEventDependencyChainCount," +
                "activeEventExistsAtStall," +
                "reservationConflictExistsAtStall,experimentMode," +
                "dependencyAwarePrecedenceEnabled," +
                "precedenceCandidatesExamined," +
                "precedenceSubstitutionsMade," +
                "precedenceChainsFollowed," +
                "precedenceBlockedByRowEvent," +
                "precedenceSubstitutedEventsStarted," +
                "precedenceRejectedOccupiedSpace," +
                "precedenceMaximumChainDepth," +
                "gatekeeperUniqueEpisodes," +
                "gatekeeperRepeatedSelections," +
                "gatekeeperEventStarts," +
                "gatekeeperConfirmedReleaseTiles," +
                "gatekeeperUpstreamStartsWithin1," +
                "gatekeeperUpstreamStartsWithin2," +
                "gatekeeperUpstreamStartsWithin5," +
                "gatekeeperUpstreamStartsWithin10," +
                "gatekeeperEpisodesWithoutEffect," +
                "gatekeeperHighestTenTickGain," +
                "gatekeeperHighestValueEpisodeId," +
                "gatekeeperEpisodeSummary,status";

        appendLine(CSV_RESULTS_PATH, header);
    }

    static void appendHumanReadableResult(ScenarioResult result) {
        String timestamp =
                LocalDateTime.now().format(
                        DateTimeFormatter.ofPattern(
                                "yyyy-MM-dd HH:mm:ss"
                        )
                );

        String text =
                "\n============================================================\n" +
                "WHOLE-CABIN HYBRID BOARDING SCENARIO\n" +
                "RESEARCH EXPERIMENT " +
                String.format(Locale.ROOT, "%02d", EXPERIMENT_NUMBER) + "\n" +
                EXPERIMENT_TITLE + "\n" +
                "============================================================\n" +
                "Date/Time: " + timestamp + "\n" +
                "Experiment mode: " +
                result.experimentMode + "\n" +
                "Dependency-aware precedence enabled: " +
                result.releaseImminentProtectionEnabled + "\n" +
                "Experiment seed: " + EXPERIMENT_SEED + "\n" +
                "Scenario number: " + result.scenarioNumber + "\n" +
                "Scenario seed: " + result.seed + "\n\n" +

                "CABIN\n" +
                "-----\n" +
                "Configuration: " + result.cabin + "\n" +
                "Rows: " + result.rows + "\n" +
                "Seats per row: " + result.seatsPerRow + "\n" +
                "Total seats: " + result.totalSeats + "\n" +
                "Passengers: " + result.passengers + "\n" +
                String.format(
                        Locale.ROOT,
                        "Occupancy: %.0f%%%n",
                        result.occupancyRate * 100.0
                ) +
                "Entry headway range: " +
                result.minHeadway + "-" + result.maxHeadway + "\n\n" +

                "OUTCOME\n" +
                "-------\n" +
                "Status: " + result.status() + "\n" +
                "Completed ticks: " + result.completedTicks + "\n" +
                "Passengers seated: " +
                result.passengersSeated + "/" + result.passengers + "\n" +
                String.format(
                        Locale.ROOT,
                        "Average waiting ticks: %.2f%n",
                        result.averageWaitingTicks
                ) +
                "Longest waiting passenger: " +
                result.longestWaitingPassenger + "\n" +
                "Longest waiting ticks: " +
                result.longestWaitingTicks + "\n\n" +

                "LEFT AISLE\n" +
                "----------\n" +
                "Total moves: " + result.leftMoves + "\n" +
                "Maximum occupied tiles: " +
                result.leftMaxOccupied + "\n" +
                "Seat events: " + result.leftSeatEvents + "\n" +
                "Single-blocker events: " +
                result.leftSingleBlockers + "\n" +
                "Multiple-blocker events: " +
                result.leftMultipleBlockers + "\n" +
                "Temporary aisle reoccupations: " +
                result.leftTemporaryAisleReoccupations + "\n\n" +

                "RIGHT AISLE\n" +
                "-----------\n" +
                "Total moves: " + result.rightMoves + "\n" +
                "Maximum occupied tiles: " +
                result.rightMaxOccupied + "\n" +
                "Seat events: " + result.rightSeatEvents + "\n" +
                "Single-blocker events: " +
                result.rightSingleBlockers + "\n" +
                "Multiple-blocker events: " +
                result.rightMultipleBlockers + "\n" +
                "Temporary aisle reoccupations: " +
                result.rightTemporaryAisleReoccupations + "\n\n" +

                "MIDDLE-BANK COORDINATION\n" +
                "------------------------\n" +
                "Reservation requests: " +
                result.middleBankReservationRequests + "\n" +
                "Reservations granted: " +
                result.middleBankReservationGranted + "\n" +
                "Reservations denied/waited: " +
                result.middleBankReservationDenied + "\n" +
                "Simultaneous opposite-aisle conflicts: " +
                result.simultaneousMiddleBankConflicts + "\n\n" +

                "STALL DIAGNOSTICS\n" +
                "-----------------\n" +
                "Stall detected: " + result.stalled + "\n" +
                "Ticks without new seated passenger: " +
                result.ticksWithoutNewSeat + "\n" +
                "Remaining waiting to enter: " +
                result.remainingWaitingToEnter + "\n" +
                "Remaining moving in aisle: " +
                result.remainingMovingInAisle + "\n" +
                "Remaining waiting at row: " +
                result.remainingWaitingAtRow + "\n" +
                "Remaining inside seat event: " +
                result.remainingInSeatEvent + "\n" +
                "Dominant blocking reason: " +
                result.dominantBlockingReason + "\n" +
                "Dominant blocking reason count: " +
                result.dominantBlockingReasonCount + "\n" +
                "Longest unresolved passenger ID: " +
                result.stalledPassengerId + "\n" +
                "Longest unresolved row: " +
                result.stalledRow + "\n" +
                "Longest unresolved aisle: " +
                result.stalledAisle + "\n\n" +

                "DEPENDENCY-CHAIN ANALYSIS\n" +
                "-------------------------\n" +
                "Longest dependency-chain length: " +
                result.longestDependencyChainLength + "\n" +
                "Longest dependency chain: " +
                result.longestDependencyChain + "\n" +
                "Dependency root passenger ID: " +
                result.dependencyRootPassengerId + "\n" +
                "Dependency terminal reason: " +
                result.dependencyTerminalReason + "\n" +
                "Passengers indirectly affected by root: " +
                result.passengersIndirectlyAffected + "\n" +
                "Passengers with a direct blocker: " +
                result.passengersWithDirectBlocker + "\n" +
                "Total blocked ticks: " +
                result.totalBlockedTicks + "\n" +
                "Total stationary ticks: " +
                result.totalStationaryTicks + "\n" +
                "Seat-event requests: " +
                result.totalSeatEventRequests + "\n" +
                "Failed seat-event starts: " +
                result.totalFailedSeatEventStarts + "\n\n" +

                "DEPENDENCY FAN-OUT / CRITICAL-BLOCKER TREE\n" +
                "-----------------------------------------\n" +
                "Critical blocker passenger ID: " +
                result.criticalBlockerPassengerId + "\n" +
                "Critical blocker identity: " +
                result.criticalBlockerIdentity + "\n" +
                "Direct fan-out: " +
                result.criticalBlockerDirectFanOut + "\n" +
                "Total indirect descendants: " +
                result.criticalBlockerTotalDescendants + "\n" +
                "Critical tree size including blocker: " +
                result.criticalBlockerTreeSize + "\n" +
                "Critical tree depth: " +
                result.criticalBlockerTreeDepth + "\n" +
                "Frozen internal components: " +
                result.frozenComponentCount + "\n" +
                "Terminal WAITING_FOR_SEAT_EVENT nodes: " +
                result.terminalRowEventNodeCount + "\n" +
                "Unresolved internal passengers: " +
                result.unresolvedInternalPassengers + "\n" +
                "Unresolved outside passengers: " +
                result.unresolvedOutsidePassengers + "\n" +
                "Unresolved internal passengers outside critical tree: " +
                result.unresolvedInternalNotInCriticalTree + "\n" +
                "Critical blocker tree:\n" +
                result.criticalBlockerTree + "\n\n" +

                "SEAT-EVENT PREREQUISITE ANALYSIS\n" +
                "--------------------------------\n" +
                "Dominant prerequisite failure: " +
                result.dominantPrerequisiteFailure + "\n" +
                "Dominant prerequisite failure count: " +
                result.dominantPrerequisiteFailureCount + "\n" +
                "Critical blocker final prerequisite: " +
                result.criticalBlockerFinalPrerequisite + "\n" +
                "Critical blocker required blockers: " +
                result.criticalBlockerRequiredBlockers + "\n" +
                "Critical blocker required yield tiles: " +
                result.criticalBlockerRequiredYieldTiles + "\n" +
                "Critical blocker unavailable yield tiles: " +
                result.criticalBlockerUnavailableYieldTiles + "\n" +
                "Critical blocker yield-tile occupants: " +
                result.criticalBlockerYieldTileOccupants + "\n" +
                "Waiting-row passengers with zero blockers: " +
                result.rowPassengersZeroBlockers + "\n" +
                "Waiting-row passengers with one blocker: " +
                result.rowPassengersOneBlocker + "\n" +
                "Waiting-row passengers with multiple blockers: " +
                result.rowPassengersMultipleBlockers + "\n" +
                "Yield-tile occupied failures: " +
                result.totalYieldTileOccupiedFailures + "\n" +
                "Rear-boundary failures: " +
                result.totalRearBoundaryFailures + "\n" +
                "Middle-bank reservation failures: " +
                result.totalReservationFailures + "\n" +
                "Active-event deferrals: " +
                result.totalActiveEventDeferrals + "\n" +
                "Simultaneous arbitration losses: " +
                result.totalArbitrationLosses + "\n\n" +

                "YIELD-TILE OCCUPANT TRACE / CYCLE ANALYSIS\n" +
                "-----------------------------------------\n" +
                "Yield dependency classification: " +
                result.yieldDependencyType + "\n" +
                "Required yield occupants: " +
                result.requiredYieldOccupantCount + "\n" +
                "Trace length: " +
                result.criticalYieldOccupantTraceLength + "\n" +
                "Yield-occupant trace: " +
                result.criticalYieldOccupantTrace + "\n" +
                "Cycle detected: " +
                result.dependencyCycleDetected + "\n" +
                "Cycle members: " +
                result.dependencyCycleMembers + "\n" +
                "Terminal passenger: " +
                result.dependencyTerminalPassenger + "\n" +
                "Terminal condition: " +
                result.dependencyTerminalCondition + "\n" +
                "Distinct row-event terminals: " +
                result.distinctRowEventTerminals + "\n" +
                "All yield occupants in same frozen component: " +
                result.allYieldOccupantsSameFrozenComponent + "\n" +
                "Final stable prerequisite: " +
                result.finalStablePrerequisite + "\n" +
                "Yield-occupant details: " +
                result.criticalYieldOccupantDetails + "\n\n" +

                "FINAL STABLE ROW-EVENT DEPENDENCY ANALYSIS\n" +
                "------------------------------------------\n" +
                "Final snapshot dependency type: " +
                result.finalSnapshotDependencyType + "\n" +
                "Final snapshot prerequisite: " +
                result.finalSnapshotPrerequisite + "\n" +
                "Current required blockers: " +
                result.finalSnapshotRequiredBlockers + "\n" +
                "Current required yield tiles: " +
                result.finalSnapshotRequiredYieldTiles + "\n" +
                "Current unavailable yield tiles: " +
                result.finalSnapshotUnavailableYieldTiles + "\n" +
                "Current yield-tile occupants: " +
                result.finalSnapshotYieldOccupants + "\n" +
                "Final row-event chain: " +
                result.finalSnapshotRowEventChain + "\n" +
                "Final row-event chain length: " +
                result.finalSnapshotRowEventChainLength + "\n" +
                "Distinct row events in chain: " +
                result.finalSnapshotDistinctRowEvents + "\n" +
                "Final terminal condition: " +
                result.finalSnapshotTerminalCondition + "\n" +
                "One empty tile would start event: " +
                result.oneEmptyTileWouldStartEvent + "\n" +
                "One-tile-earlier hold would preserve space: " +
                result.oneTileEarlierHoldWouldPreserveSpace + "\n" +
                "Simultaneous empty tiles required: " +
                result.simultaneousEmptyTilesRequired + "\n" +
                "Final row-event dependency chains in aircraft: " +
                result.finalRowEventDependencyChainCount + "\n" +
                "Active event exists at stall: " +
                result.activeEventExistsAtStall + "\n" +
                "Reservation conflict exists at stall: " +
                result.reservationConflictExistsAtStall + "\n\n" +
                "EXPERIMENT 11 DEPENDENCY-AWARE EVIDENCE\\n" +
                "---------------------------------------\\n" +
                "Experiment mode: " +
                result.experimentMode + "\\n" +
                "Dependency-aware precedence enabled: " +
                result.releaseImminentProtectionEnabled + "\\n" +
                "Normal candidates examined: " +
                result.releaseImminentEvaluations + "\\n" +
                "Precedence substitutions made: " +
                result.releaseImminentWindowsActivated + "\\n" +
                "Dependency chains followed: " +
                result.releaseImminentTilesProtected + "\\n" +
                "Blocked by waiting-row occupant: " +
                result.releaseImminentPassengerHolds + "\\n" +
                "Substituted events started: " +
                result.releaseImminentNextTickEventStarts + "\\n" +
                "Occupied-space rejections: " +
                result.releaseImminentWindowsWithoutEvent + "\\n" +
                "Maximum scheduling-chain depth: " +
                result.lastProtectedCandidateId + "\\n" +
                "============================================================\\n";

        appendLine(HUMAN_RESULTS_PATH, text);
    }

    static void appendCsvResult(ScenarioResult result) {
        String timestamp =
                LocalDateTime.now().format(
                        DateTimeFormatter.ofPattern(
                                "yyyy-MM-dd HH:mm:ss"
                        )
                );

        String line = String.join(",",
                String.valueOf(EXPERIMENT_NUMBER),
                csv(EXPERIMENT_TITLE),
                csv(timestamp),
                String.valueOf(EXPERIMENT_SEED),
                String.valueOf(result.scenarioNumber),
                String.valueOf(result.seed),
                csv(result.cabin),
                String.valueOf(result.rows),
                String.valueOf(result.seatsPerRow),
                String.valueOf(result.totalSeats),
                String.valueOf(result.passengers),
                String.format(
                        Locale.ROOT,
                        "%.0f",
                        result.occupancyRate * 100.0
                ),
                String.valueOf(result.minHeadway),
                String.valueOf(result.maxHeadway),
                String.valueOf(result.completedTicks),
                String.valueOf(result.completed),
                String.valueOf(result.passengersSeated),
                String.valueOf(result.leftMoves),
                String.valueOf(result.rightMoves),
                String.valueOf(result.leftMaxOccupied),
                String.valueOf(result.rightMaxOccupied),
                String.valueOf(result.leftSeatEvents),
                String.valueOf(result.rightSeatEvents),
                String.valueOf(result.leftSingleBlockers),
                String.valueOf(result.rightSingleBlockers),
                String.valueOf(result.leftMultipleBlockers),
                String.valueOf(result.rightMultipleBlockers),
                String.valueOf(
                        result.leftTemporaryAisleReoccupations
                ),
                String.valueOf(
                        result.rightTemporaryAisleReoccupations
                ),
                String.valueOf(
                        result.middleBankReservationRequests
                ),
                String.valueOf(
                        result.middleBankReservationGranted
                ),
                String.valueOf(
                        result.middleBankReservationDenied
                ),
                String.valueOf(
                        result.simultaneousMiddleBankConflicts
                ),
                String.format(
                        Locale.ROOT,
                        "%.2f",
                        result.averageWaitingTicks
                ),
                String.valueOf(result.longestWaitingTicks),
                csv(result.longestWaitingPassenger),
                String.valueOf(result.stalled),
                String.valueOf(result.ticksWithoutNewSeat),
                String.valueOf(result.remainingWaitingToEnter),
                String.valueOf(result.remainingMovingInAisle),
                String.valueOf(result.remainingWaitingAtRow),
                String.valueOf(result.remainingInSeatEvent),
                csv(result.dominantBlockingReason),
                String.valueOf(result.dominantBlockingReasonCount),
                String.valueOf(result.stalledPassengerId),
                String.valueOf(result.stalledRow),
                csv(result.stalledAisle),
                String.valueOf(result.longestDependencyChainLength),
                csv(result.longestDependencyChain),
                String.valueOf(result.dependencyRootPassengerId),
                csv(result.dependencyTerminalReason),
                String.valueOf(result.passengersIndirectlyAffected),
                String.valueOf(result.passengersWithDirectBlocker),
                String.valueOf(result.totalBlockedTicks),
                String.valueOf(result.totalStationaryTicks),
                String.valueOf(result.totalSeatEventRequests),
                String.valueOf(result.totalFailedSeatEventStarts),
                String.valueOf(result.criticalBlockerPassengerId),
                csv(result.criticalBlockerIdentity),
                String.valueOf(result.criticalBlockerDirectFanOut),
                String.valueOf(result.criticalBlockerTotalDescendants),
                String.valueOf(result.criticalBlockerTreeSize),
                String.valueOf(result.criticalBlockerTreeDepth),
                String.valueOf(result.frozenComponentCount),
                String.valueOf(result.terminalRowEventNodeCount),
                String.valueOf(result.unresolvedInternalPassengers),
                String.valueOf(result.unresolvedOutsidePassengers),
                String.valueOf(result.unresolvedInternalNotInCriticalTree),
                csv(result.criticalBlockerTree),
                csv(result.dominantPrerequisiteFailure),
                String.valueOf(result.dominantPrerequisiteFailureCount),
                csv(result.criticalBlockerFinalPrerequisite),
                String.valueOf(result.criticalBlockerRequiredBlockers),
                csv(result.criticalBlockerRequiredYieldTiles),
                csv(result.criticalBlockerUnavailableYieldTiles),
                csv(result.criticalBlockerYieldTileOccupants),
                String.valueOf(result.rowPassengersZeroBlockers),
                String.valueOf(result.rowPassengersOneBlocker),
                String.valueOf(result.rowPassengersMultipleBlockers),
                String.valueOf(result.totalYieldTileOccupiedFailures),
                String.valueOf(result.totalRearBoundaryFailures),
                String.valueOf(result.totalReservationFailures),
                String.valueOf(result.totalActiveEventDeferrals),
                String.valueOf(result.totalArbitrationLosses),
                csv(String.valueOf(result.yieldDependencyType)),
                csv(result.criticalYieldOccupantTrace),
                String.valueOf(result.criticalYieldOccupantTraceLength),
                String.valueOf(result.dependencyCycleDetected),
                csv(result.dependencyCycleMembers),
                csv(result.dependencyTerminalPassenger),
                csv(result.dependencyTerminalCondition),
                String.valueOf(result.distinctRowEventTerminals),
                String.valueOf(result.requiredYieldOccupantCount),
                String.valueOf(
                        result.allYieldOccupantsSameFrozenComponent),
                csv(result.finalStablePrerequisite),
                csv(result.criticalYieldOccupantDetails),
                csv(String.valueOf(
                        result.finalSnapshotDependencyType)),
                csv(result.finalSnapshotPrerequisite),
                String.valueOf(
                        result.finalSnapshotRequiredBlockers),
                csv(result.finalSnapshotRequiredYieldTiles),
                csv(result.finalSnapshotUnavailableYieldTiles),
                csv(result.finalSnapshotYieldOccupants),
                csv(result.finalSnapshotRowEventChain),
                String.valueOf(
                        result.finalSnapshotRowEventChainLength),
                String.valueOf(
                        result.finalSnapshotDistinctRowEvents),
                csv(result.finalSnapshotTerminalCondition),
                String.valueOf(
                        result.oneEmptyTileWouldStartEvent),
                String.valueOf(
                        result.oneTileEarlierHoldWouldPreserveSpace),
                String.valueOf(
                        result.simultaneousEmptyTilesRequired),
                String.valueOf(
                        result.finalRowEventDependencyChainCount),
                String.valueOf(
                        result.activeEventExistsAtStall),
                String.valueOf(
                        result.reservationConflictExistsAtStall),
                csv(result.experimentMode),
                String.valueOf(
                        result.releaseImminentProtectionEnabled),
                String.valueOf(
                        result.releaseImminentEvaluations),
                String.valueOf(
                        result.releaseImminentWindowsActivated),
                String.valueOf(
                        result.releaseImminentTilesProtected),
                String.valueOf(
                        result.releaseImminentPassengerHolds),
                String.valueOf(
                        result.releaseImminentNextTickEventStarts),
                String.valueOf(
                        result.releaseImminentWindowsWithoutEvent),
                String.valueOf(
                        result.lastProtectedCandidateId),
                String.valueOf(result.gatekeeperUniqueEpisodes),
                String.valueOf(result.gatekeeperRepeatedSelections),
                String.valueOf(result.gatekeeperEventStarts),
                String.valueOf(result.gatekeeperConfirmedReleaseTiles),
                String.valueOf(result.gatekeeperUpstreamStartsWithin1),
                String.valueOf(result.gatekeeperUpstreamStartsWithin2),
                String.valueOf(result.gatekeeperUpstreamStartsWithin5),
                String.valueOf(result.gatekeeperUpstreamStartsWithin10),
                String.valueOf(result.gatekeeperEpisodesWithoutEffect),
                String.valueOf(result.gatekeeperHighestTenTickGain),
                String.valueOf(result.gatekeeperHighestValueEpisodeId),
                csv(result.gatekeeperEpisodeSummary),
                csv(result.status())
        );

        appendLine(CSV_RESULTS_PATH, line);
    }

    static void appendExperimentSummary(
            List<ScenarioResult> results) {

        long successful =
                results.stream()
                        .filter(result -> result.completed)
                        .count();

        double averageTicks =
                results.stream()
                        .mapToInt(result -> result.completedTicks)
                        .average()
                        .orElse(0.0);

        double averageWaiting =
                results.stream()
                        .mapToDouble(
                                result -> result.averageWaitingTicks
                        )
                        .average()
                        .orElse(0.0);

        String text =
                "\n************************************************************\n" +
                "BATCH EXPERIMENT SUMMARY\n" +
                "RESEARCH EXPERIMENT " +
                String.format(Locale.ROOT, "%02d", EXPERIMENT_NUMBER) + "\n" +
                EXPERIMENT_TITLE + "\n" +
                "************************************************************\n" +
                "Experiment seed: " + EXPERIMENT_SEED + "\n" +
                "Scenarios executed: " + results.size() + "\n" +
                "Successful completions: " + successful + "\n" +
                "Incomplete/MAX_TICKS: " +
                (results.size() - successful) + "\n" +
                String.format(
                        Locale.ROOT,
                        "Average completed ticks: %.2f%n",
                        averageTicks
                ) +
                String.format(
                        Locale.ROOT,
                        "Average waiting ticks: %.2f%n",
                        averageWaiting
                ) +
                "************************************************************\n";

        appendLine(HUMAN_RESULTS_PATH, text);
    }

    static void appendLine(Path path, String text) {
        try (BufferedWriter writer =
                     Files.newBufferedWriter(
                             path,
                             StandardCharsets.UTF_8,
                             StandardOpenOption.CREATE,
                             StandardOpenOption.APPEND)) {

            writer.write(text);

            if (!text.endsWith(System.lineSeparator())) {
                writer.newLine();
            }

        } catch (IOException ex) {
            throw new IllegalStateException(
                    "Unable to write results file: " + path,
                    ex
            );
        }
    }

    static String csv(String value) {
        if (value == null) {
            return "";
        }

        return "\"" + value.replace("\"", "\"\"") + "\"";
    }

    // ======================== CONSOLE SUMMARIES ========================

    static void printScenarioSummary(ScenarioResult result) {
        String summary = String.format(
                Locale.ROOT,
                "Scenario %02d/%02d | %s | seats=%d | passengers=%d | " +
                "occupancy=%.0f%% | headway=%d-%d | ticks=%d | seated=%d/%d | %s%n",
                result.scenarioNumber,
                NUMBER_OF_SCENARIOS,
                result.cabin,
                result.totalSeats,
                result.passengers,
                result.occupancyRate * 100.0,
                result.minHeadway,
                result.maxHeadway,
                result.completedTicks,
                result.passengersSeated,
                result.passengers,
                result.status() +
                " | mode=" +
                result.experimentMode
        );

        System.out.print(summary);

        if (!result.completed) {
            String diagnostic =
                    "   Experiment " +
                    String.format(Locale.ROOT, "%02d", EXPERIMENT_NUMBER) +
                    " diagnostic: dominantReason=" +
                    result.dominantBlockingReason +
                    " | noSeatProgressTicks=" +
                    result.ticksWithoutNewSeat +
                    " | unresolvedAtRow=" +
                    result.remainingWaitingAtRow +
                    " | unresolvedInAisle=" +
                    result.remainingMovingInAisle +
                    " | dependencyLength=" +
                    result.longestDependencyChainLength +
                    " | chain=" +
                    result.longestDependencyChain +
                    " | criticalBlocker=" +
                    result.criticalBlockerIdentity +
                    " | criticalTreeSize=" +
                    result.criticalBlockerTreeSize +
                    " | criticalTreeDepth=" +
                    result.criticalBlockerTreeDepth +
                    " | frozenComponents=" +
                    result.frozenComponentCount +
                    " | criticalPrerequisite=" +
                    result.criticalBlockerFinalPrerequisite +
                    " | requiredBlockers=" +
                    result.criticalBlockerRequiredBlockers +
                    " | unavailableYieldTiles=" +
                    result.criticalBlockerUnavailableYieldTiles +
                    " | yieldTileOccupants=" +
                    result.criticalBlockerYieldTileOccupants +
                    " | yieldDependencyType=" +
                    result.yieldDependencyType +
                    " | cycleDetected=" +
                    result.dependencyCycleDetected +
                    " | occupantTraceLength=" +
                    result.criticalYieldOccupantTraceLength +
                    " | occupantTrace=" +
                    result.criticalYieldOccupantTrace +
                    " | finalSnapshotType=" +
                    result.finalSnapshotDependencyType +
                    " | finalPrerequisite=" +
                    result.finalSnapshotPrerequisite +
                    " | oneEmptyTileWouldStart=" +
                    result.oneEmptyTileWouldStartEvent +
                    " | oneTileEarlierHoldWouldPreserve=" +
                    result.oneTileEarlierHoldWouldPreserveSpace +
                    " | finalRowEventChain=" +
                    result.finalSnapshotRowEventChain;

            System.out.println(diagnostic);
        }
    }

    static double completionPercent(
            ScenarioResult result) {

        return result.passengers == 0
                ? 0.0
                : 100.0 *
                  result.passengersSeated /
                  result.passengers;
    }

    static String pairComparisonText(
            ScenarioResult baseline,
            ScenarioResult protectedResult) {

        int seatedDelta =
                protectedResult.passengersSeated -
                baseline.passengersSeated;

        double completionDelta =
                completionPercent(protectedResult) -
                completionPercent(baseline);

        int outsideDelta =
                protectedResult.remainingWaitingToEnter -
                baseline.remainingWaitingToEnter;

        boolean convertedToSuccess =
                !baseline.completed &&
                protectedResult.completed;

        boolean protectedWorse =
                protectedResult.passengersSeated <
                baseline.passengersSeated;

        return String.format(
                Locale.ROOT,
                "PAIR %02d | baseline=%d/%d (%.2f%%) | " +
                "protected=%d/%d (%.2f%%) | seatedDelta=%+d | " +
                "completionDelta=%+.2f%% | outsideDelta=%+d | " +
                "substitutions=%d | rowBlocks=%d | substitutedStarts=%d | " +
                "convertedToSuccess=%s | protectedWorse=%s%n",
                baseline.scenarioNumber,
                baseline.passengersSeated,
                baseline.passengers,
                completionPercent(baseline),
                protectedResult.passengersSeated,
                protectedResult.passengers,
                completionPercent(protectedResult),
                seatedDelta,
                completionDelta,
                outsideDelta,
                protectedResult.releaseImminentWindowsActivated,
                protectedResult.releaseImminentPassengerHolds,
                protectedResult.releaseImminentNextTickEventStarts,
                convertedToSuccess,
                protectedWorse
        );
    }

    static void printPairComparison(
            ScenarioResult baseline,
            ScenarioResult protectedResult) {

        System.out.print(
                pairComparisonText(
                        baseline,
                        protectedResult)
        );
    }

    static void appendPairComparison(
            ScenarioResult baseline,
            ScenarioResult protectedResult) {

        String text =
                "\nPAIRED SCENARIO COMPARISON\n" +
                "--------------------------\n" +
                pairComparisonText(
                        baseline,
                        protectedResult);

        appendLine(HUMAN_RESULTS_PATH, text);
    }

    static void printExperimentSummary(
            List<ScenarioResult> results) {

        long successful =
                results.stream()
                        .filter(result -> result.completed)
                        .count();

        System.out.println("\n============================================================");
        System.out.println("WHOLE-CABIN HYBRID BOARDING SIMULATION");
        System.out.println("RESEARCH EXPERIMENT " +
                String.format(Locale.ROOT, "%02d", EXPERIMENT_NUMBER));
        System.out.println(EXPERIMENT_TITLE);
        System.out.println("BATCH EXPERIMENT COMPLETE");
        System.out.println("============================================================");
        System.out.println("Scenarios executed: " + results.size());
        System.out.println("Successful completions: " + successful);
        System.out.println("Incomplete/MAX_TICKS: " +
                (results.size() - successful));
        System.out.println("Results appended to: " + HUMAN_RESULTS_PATH);
        System.out.println("CSV appended to: " + CSV_RESULTS_PATH);
        System.out.println("============================================================");
    }
}
