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
BATCH EXPERIMENT EDITION
================================================================================

PURPOSE
-------
This version automatically executes many boarding scenarios so the experiment is
not limited to one manually selected cabin, occupancy, headway or random order.

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.

NEXT-EXPERIMENT PURPOSE
-----------------------
The first batch showed that many scenarios progressed normally and then stopped
during the final recovery stage. This version is diagnostic-first. It does not
attempt to hide or automatically repair a deadlock. Instead it identifies:

- when seating progress stops;
- how long the stall persists;
- which passenger states remain;
- which passenger has remained unchanged longest;
- the row and aisle associated with that passenger;
- the dominant repeated blocking reason;
- final aisle snapshots at the stall.

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 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");

    // 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
    }

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

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

    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;

        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";

        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 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 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<>();

    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();

        System.out.println("============================================================");
        System.out.println("WHOLE-CABIN BATCH BOARDING EXPERIMENT");
        System.out.println("============================================================");
        System.out.println("Experiment seed: " + EXPERIMENT_SEED);
        System.out.println("Scenarios scheduled: " + NUMBER_OF_SCENARIOS);
        System.out.println("Human-readable log: " + HUMAN_RESULTS_PATH);
        System.out.println("CSV dataset: " + CSV_RESULTS_PATH);
        System.out.println("============================================================\n");

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

            ScenarioConfig scenarioConfig =
                    randomScenarioConfig(scenarioNumber, experimentRandom);

            WholeCabinHybridBoardingSimulation simulation =
                    new WholeCabinHybridBoardingSimulation(scenarioConfig);

            ScenarioResult result = simulation.runScenario();
            results.add(result);

            appendHumanReadableResult(result);
            appendCsvResult(result);

            printScenarioSummary(result);
        }

        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]
        );
    }

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

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

            advanceSeatEvent(left);
            advanceSeatEvent(right);

            detectAndStartSeatEventsWithMiddleBankArbitration();

            synchronousAisleMove(left);
            synchronousAisleMove(right);

            insertPassengerIfPossible(left);
            insertPassengerIfPossible(right);

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

            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 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);

        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;
                passenger.lastBlockingReason = "WAITING_FOR_SEAT_EVENT";
                recordBlockingReason(passenger.lastBlockingReason);
                continue;
            }

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

            if (blockedRow >= 0 && position <= blockedRow) {
                passenger.lastAislePosition = position;
                passenger.lastBlockingReason = "ACTIVE_SEAT_EVENT_AHEAD";
                recordBlockingReason(passenger.lastBlockingReason);
                continue;
            }

            int nextPosition = position + 1;

            if (nextPosition >= snapshot.length) {
                passenger.lastAislePosition = position;
                passenger.lastBlockingReason = "AISLE_BOUNDARY";
                recordBlockingReason(passenger.lastBlockingReason);
                continue;
            }

            if (snapshot[nextPosition] == EMPTY) {
                // 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.lastAislePosition = nextPosition;
                passenger.lastMovementTick = currentTick;
                passenger.lastBlockingReason = "NONE";

                if (nextPosition == passenger.row) {
                    passenger.state = PassengerState.WAITING_AT_ROW;
                    passenger.lastStateChangeTick = currentTick;
                }
            } else {
                passenger.lastAislePosition = position;
                passenger.lastBlockingReason = "NEXT_AISLE_TILE_OCCUPIED";
                recordBlockingReason(passenger.lastBlockingReason);
            }
        }

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

    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 = findWaitingPassengerAtRow(left);
        Passenger rightCandidate = 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;

            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;
        }

        if (!startSeatEventForPassenger(context, candidate) &&
                isMiddleBankSeat(candidate.seatIndex)) {

            releaseMiddleBankReservation(
                    candidate.row,
                    candidate.id
            );
        }
    }

    Passenger findWaitingPassengerAtRow(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 &&
                    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.lastBlockingReason = "MIDDLE_BANK_RESERVED";
        recordBlockingReason(passenger.lastBlockingReason);
        return false;
    }

    void releaseMiddleBankReservation(
            int row,
            int passengerId) {

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

    boolean startSeatEventForPassenger(
            AisleContext context,
            Passenger passenger) {

        // EVOLVED LOCAL-ADJACENCY PRINCIPLE:
        // The original code reasoned from neighbouring occupied/empty positions.
        // Here that same local inspection principle is applied to seats between the
        // chosen aisle and the target seat, producing a list of blockers.

        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);
            }
        }

        if (!hasRequiredYieldSpace(
                context,
                passenger.row,
                blockerIds.size())) {
            return false;
        }

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

        context.seatEvents++;

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

        passenger.state = PassengerState.IN_SEAT_EVENT;
        passenger.lastStateChangeTick = currentTick;
        passenger.lastBlockingReason = "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++;
            }
        }
    }

    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;
    }

    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 = "";
        }

        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 =
                "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,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" +
                "============================================================\n" +
                "Date/Time: " + timestamp + "\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";

        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(",",
                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),
                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" +
                "************************************************************\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) {
        System.out.printf(
                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()
        );

        if (!result.completed) {
            System.out.println("   Diagnostic: dominantReason="
                    + result.dominantBlockingReason
                    + " | noSeatProgressTicks="
                    + result.ticksWithoutNewSeat
                    + " | unresolvedAtRow="
                    + result.remainingWaitingAtRow
                    + " | unresolvedInAisle="
                    + result.remainingMovingInAisle);
        }
    }

    static void printExperimentSummary(
            List<ScenarioResult> results) {

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

        System.out.println("\n============================================================");
        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("============================================================");
    }
}
