<?php



namespace Modules\Ticket\Controllers\Api;



use App\Controllers\BaseController;

use CodeIgniter\API\ResponseTrait;

use Modules\Ticket\Config\TicketValidation;

use Modules\Ticket\Exceptions\TicketExceptions;

use Modules\Ticket\Models\TemporaryBook as TemporaryBookModel;

use Modules\Ticket\Models\MaxtimeModel as MaxtimeModel;

use CodeIgniter\Database\RawSql;

use CodeIgniter\I18n\Time;

use Modules\Ticket\Models\TicketModel;

use Modules\Trip\Models\SubtripModel;
use App\Libraries\BookingInventorySupport;
use App\Libraries\OvernightTripRunSupport;
use App\Libraries\TemporaryBookHoldService;
use App\Libraries\TripJourneyDateHelper;
use App\Libraries\SegmentSeatAvailability;
use App\Libraries\SegmentSeatInventoryService;



class TemporaryBook extends BaseController
{

    use ResponseTrait;



    /**

     * Temporary book model

     *

     * @var TemporaryBookModel

     */

    protected $temporaryBookModel;

    protected $maxtimeModel;

    protected $ticketModel;

    protected $subtripModel;

    protected $db;



    public function __construct()
    {

        // Init temporary book validator

        $this->ticketModel = new TicketModel();

        $ticketValidation = new TicketValidation;

        $this->validation = \Config\Services::validation($ticketValidation);

        $this->validation->setRuleGroup('temporarybooks');



        // Init temporary book model

        $this->temporaryBookModel = new TemporaryBookModel;

        $this->subtripModel = new SubtripModel();

        $this->maxtimeModel = new MaxtimeModel();

        $this->db = \Config\Database::connect();

    }



    public function checkSeats($book_from = "")
    {

        $updateBookId = null;

        $subtrip_id = $this->requestInput('subtrip_id');

        $seat_names = $this->requestInput('seat_names');

        if (is_array($seat_names)) {

            $seat_nameArr = array_map('trim', $seat_names);

        } else {

            $seat_nameArr = array_map('trim', array_filter(explode(',', $seat_names)));

        }



        $journey_date = $this->requestInput('journey_date') ?? $this->requestInput('journeydate') ?? egypt_today();

        BookingInventorySupport::syncBookingTimezone();
        (new \App\Libraries\TemporaryBookHoldService())->cleanupExpiredHolds();
        $subtrip = $subtrip_id ? $this->subtripModel->find($subtrip_id) : null;
        if (! is_object($subtrip)) {
            return $this->response->setJSON([
                'status' => 'failed',
                'response' => 422,
                'message' => 'Invalid subtrip_id.',
            ]);
        }

        $mainTripId = (int) $subtrip->trip_id;

        $pickstandError = $this->rejectIfPickstandRequiredForInventory($mainTripId, (int) $subtrip_id);
        if ($pickstandError !== null) {
            return $pickstandError;
        }

        $pickStandId = (int) $this->requestInput('pickstand');
        $boardingSearchDate = BookingInventorySupport::normalizeCalendarDate((string) $journey_date);
        $ignoreToken = $this->resolveCheckoutToken();

        try {
            if ($seat_nameArr !== []) {
                $unavailable = $this->collectSegmentUnavailableSeats(
                    (int) $subtrip_id,
                    $boardingSearchDate,
                    $pickStandId,
                    $ignoreToken
                );
                $seatMatches = array_values(array_intersect($seat_nameArr, $unavailable));
                if ($seatMatches !== []) {
                    throw new TicketExceptions(sprintf(
                        'Seat %s already processed or occupied by overlapping segment!',
                        implode(', ', $seatMatches)
                    ));
                }
            }

            $holdRow = $this->store(null, $book_from);

            if (empty($holdRow['persisted_committed'])) {
                throw new TicketExceptions(
                    'Seat hold was not committed to the database. Open GET hold-status/' . (int) ($holdRow['id'] ?? 0)
                    . ' and compare db_database with phpMyAdmin.'
                );
            }

        } catch (TicketExceptions $th) {
            return $this->response->setJSON([
                'status' => 'failed',
                'response' => 204,
                'message' => $th->getErrors()
            ]);
        }



        return $this->response->setJSON([

            'status' => 'success',

            'response' => 200,

            'message' => true,

            'data' => array_merge($holdRow, [
                'temporary_book_id' => $holdRow['id'] ?? null,
                'subtrip_id' => (int) $subtrip_id,
                'server_time_egypt' => egypt_now_sql(),
                'row_confirmed' => true,
                'phpmyadmin_hint' => 'Database "' . ($holdRow['db_database'] ?? '') . '" on host '
                    . ($holdRow['mysql_host'] ?? $holdRow['db_hostname'] ?? 'localhost')
                    . ' — run: SELECT * FROM temporarybooks WHERE id = ' . (int) ($holdRow['id'] ?? 0),
            ]),

        ]);

    }



    /**
     * Create or refresh a row in temporarybooks for this ticket_token + subtrip.
     *
     * @return array{id: int, expires_at: string, journey_date: string}
     */
    protected function store($seatBookId = null, $book_from = null): array
    {
        $subtrip_id = $this->requestInput('subtrip_id');
        $journey_date_raw = $this->requestInput('journey_date')
            ?? $this->requestInput('journeydate')
            ?? egypt_today();

        $seatNamesRaw = $this->requestInput('seat_names');
        if (is_array($seatNamesRaw)) {
            $seatNames = implode(',', array_map('trim', $seatNamesRaw));
        } else {
            $seatNames = trim((string) $seatNamesRaw);
        }

        $ticketToken = trim((string) $this->requestInput('ticket_token'));
        if ($ticketToken === '') {
            throw new TicketExceptions('ticket_token is required.');
        }

        $subtrip = $subtrip_id ? $this->subtripModel->find($subtrip_id) : null;
        if (! is_object($subtrip)) {
            throw new TicketExceptions('Invalid subtrip_id.');
        }

        $mainTripId = (int) $subtrip->trip_id;

        $pickStandId = (int) $this->requestInput('pickstand');
        $boardingDate = BookingInventorySupport::normalizeCalendarDate((string) $journey_date_raw);
        $busStartDate = BookingInventorySupport::busRunForBooking(
            $mainTripId,
            (int) $subtrip_id,
            $boardingDate,
            $pickStandId
        );

        BookingInventorySupport::logBookingDateContext('temporarybook_store', [
            'trip_id' => $mainTripId,
            'subtrip_id' => (int) $subtrip_id,
            'search_date' => $boardingDate,
            'pickstand' => $pickStandId,
            'service_date' => $busStartDate,
            'boarding_date' => $boardingDate,
            'stored_ticket_date' => null,
            'stored_manifest_date' => null,
            'ticket_token' => trim((string) $this->requestInput('ticket_token')),
        ]);

        if ($book_from === 'admin') {
            $maxtime = $this->maxtimeModel->where('book_from', $book_from)->first();
            $expiryMinutes = (int) ($maxtime->maxtime ?? 5);
        } else {
            $maxtime = $this->maxtimeModel->where('book_from', 'client')->first();
            $expiryMinutes = (int) ($maxtime->maxtime ?? 10);
        }
        if ($expiryMinutes < 1) {
            $expiryMinutes = 10;
        }

        $temporaryBookData = [
            'subtrip_id' => (int) $subtrip_id,
            'trip_id' => $mainTripId,
            'ticket_token' => $ticketToken,
            'seat_names' => $seatNames,
            'journey_date' => $busStartDate . ' 00:00:00',
            'expires_at' => egypt_add_minutes_sql($expiryMinutes),
        ];

        if (! $this->validation->run($temporaryBookData, 'temporarybooks')) {
            throw new TicketExceptions(null, 0, null, array_values($this->validation->getErrors()));
        }

        try {
            $holdService = new TemporaryBookHoldService();

            return $holdService->saveHold(
                (int) $subtrip_id,
                $mainTripId,
                $ticketToken,
                $seatNames,
                $busStartDate,
                $expiryMinutes,
                [
                    'boarding_search_date' => $boardingDate,
                    'pick_stand_id' => $pickStandId,
                    'pick_location_id' => $subtrip->pick_location_id,
                    'drop_location_id' => $subtrip->drop_location_id,
                ]
            );
        } catch (\RuntimeException $e) {
            throw new TicketExceptions($e->getMessage());
        }
    }

    /**
     * Delete only expired holds (optional: keep one id during the same request).
     */
    protected function cleanDatabase(?int $exceptHoldId = null): void
    {
        unset($exceptHoldId);
        (new \App\Libraries\TemporaryBookHoldService())->cleanupExpiredHolds();
    }



    protected function delete(int $seatBookId)
    {

        return $this->temporaryBookModel->where('id', $seatBookId)->delete();

    }

    /**
     * Debug: read a hold row with a new DB connection (proves commit vs phpMyAdmin).
     */
    public function holdStatus($holdId = 0)
    {
        $id = (int) $holdId;
        if ($id < 1) {
            return $this->response->setJSON([
                'status' => 'failed',
                'message' => 'Invalid hold id.',
            ]);
        }

        $persist = (new TemporaryBookHoldService())->confirmPersistedOnFreshConnection($id);

        return $this->response->setJSON([
            'status' => $persist['found'] ? 'success' : 'failed',
            'message' => $persist['found']
                ? 'Row exists in database (same data phpMyAdmin should show).'
                : 'Row not found — hold expired, wrong database, or never committed.',
            'data' => $persist,
        ]);
    }

    public function getHoldTime()
    {
        (new TemporaryBookHoldService())->cleanupExpiredHolds();

        $timeRow = $this->maxtimeModel
            ->select('maxtime')
            ->where('book_from', 'client') // ✅ correct CI4 where syntax
            ->first();

        return $this->response->setJSON([
            'status' => 'success',
            'response' => 200,
            'holdTime' => $timeRow ? $timeRow->maxtime : 10, // ✅ return actual value
        ]);
    }


    /**
     * Release seat hold(s). Website/mobile: POST deleteHoldSeat/{seat_names} with subtrip_id, seat_names, journey_date.
     * Delete priority: temporary_book_id, ticket_token + subtrip_id, seat_names + subtrip_id.
     * Accepts form POST or JSON body (application/json).
     */
    public function deleteHoldSeat($seatNumber = null)
    {
        BookingInventorySupport::syncBookingTimezone();
        (new TemporaryBookHoldService())->cleanupExpiredHolds();

        $subtrip_id = (int) ($this->requestInput('subtrip_id') ?? 0);
        $seat_names = $this->normalizeSeatNamesInput($this->requestInput('seat_names'));
        $ticket_token = trim((string) ($this->requestInput('ticket_token') ?? ''));
        $temporary_book_id = (int) ($this->requestInput('temporary_book_id') ?? $this->requestInput('id') ?? 0);
        $journey_date = trim((string) ($this->requestInput('journey_date') ?? $this->requestInput('journeydate') ?? ''));
        $pickstand = (int) ($this->requestInput('pickstand') ?? 0);

        $pathSegment = $seatNumber !== null ? trim(rawurldecode((string) $seatNumber)) : '';
        if ($seat_names === '' && $pathSegment !== '' && ! $this->pathSegmentLooksLikeSubtripId($pathSegment, $subtrip_id)) {
            $seat_names = TemporaryBookHoldService::normalizeSeatNamesForMatch($pathSegment);
        }

        if ($temporary_book_id < 1 && $subtrip_id < 1) {
            return $this->response->setJSON([
                'status' => 'failed',
                'response' => 422,
                'message' => 'subtrip_id is required.',
            ]);
        }

        if ($temporary_book_id < 1 && $ticket_token === '' && $seat_names === '') {
            return $this->response->setJSON([
                'status' => 'failed',
                'response' => 422,
                'message' => 'Provide ticket_token, seat_names, or temporary_book_id.',
            ]);
        }

        $deleted = (new TemporaryBookHoldService())->releaseHold(
            $temporary_book_id > 0 ? $temporary_book_id : null,
            $subtrip_id > 0 ? $subtrip_id : null,
            $ticket_token !== '' ? $ticket_token : null,
            $seat_names !== '' ? $seat_names : null,
            $journey_date !== '' ? $journey_date : null,
            $pickstand
        );

        if ($deleted > 0) {
            return $this->response->setJSON([
                'status' => 'success',
                'response' => 200,
                'message' => true,
            ]);
        }

        return $this->response->setJSON([
            'status' => 'failed',
            'response' => 204,
            'message' => 'Seat not unHolded',
        ]);
    }

    /**
     * Read POST form field or JSON body key (mobile clients often send application/json).
     *
     * @param mixed $default
     * @return mixed
     */
    protected function requestInput(string $key, $default = null)
    {
        $json = $this->request->getJSON(true);
        if (is_array($json) && array_key_exists($key, $json)) {
            $value = $json[$key];

            return $value !== null && $value !== '' ? $value : $default;
        }

        $value = $this->request->getVar($key);

        return $value !== null && $value !== '' ? $value : $default;
    }

    /**
     * Seat-hold token from JSON body, POST fields, cookie, or session.
     */
    protected function resolveCheckoutToken(): ?string
    {
        $json = $this->request->getJSON(true);
        $jsonToken = is_array($json) ? ($json['ticket_token'] ?? $json['ticketToken'] ?? null) : null;

        $candidates = [
            $jsonToken,
            $this->requestInput('ticket_token'),
            $this->requestInput('ticketToken'),
            $this->request->getCookie('_chkseats_token'),
        ];
        $sessionToken = session('ticket_token');
        if ($sessionToken !== null && $sessionToken !== '') {
            $candidates[] = $sessionToken;
        }

        foreach ($candidates as $candidate) {
            $token = trim((string) ($candidate ?? ''));
            if ($token !== '') {
                return $token;
            }
        }

        return null;
    }

    /**
     * @param mixed $raw
     */
    protected function normalizeSeatNamesInput($raw): string
    {
        if (is_array($raw)) {
            return TemporaryBookHoldService::normalizeSeatNamesForMatch(implode(',', array_map('trim', $raw)));
        }

        return TemporaryBookHoldService::normalizeSeatNamesForMatch(trim((string) $raw));
    }

    protected function pathSegmentLooksLikeSubtripId(string $pathSegment, int $subtripId): bool
    {
        if ($subtripId < 1) {
            return false;
        }

        return preg_match('/^\d+$/', $pathSegment) === 1 && (int) $pathSegment === $subtripId;
    }

    public function bookingcheck()
    {

        $updateBookId = null;

        $subtrip_id = $this->requestInput('subtrip_id');

        $seat_names = $this->requestInput('seat_names');

        $pickstand = $this->requestInput('pickstand');

        $dropstand = $this->requestInput('dropstand');

        $journeydate = $this->requestInput('journeydate') ?? $this->requestInput('journey_date');
        if (empty($journeydate)) {
            $journeydate = egypt_today();
        }

        if (is_array($seat_names)) {

            $seat_nameArr = array_map('trim', $seat_names);

        } else {

            $seat_nameArr = array_map('trim', array_filter(explode(',', $seat_names)));

        }



        $subtripInformation = $subtrip_id ? $this->subtripModel->find($subtrip_id) : null;

        if (!$subtripInformation || !is_object($subtripInformation)) {
            return $this->response->setJSON([
                'status' => 'error',
                'message' => 'Invalid subtrip information.'
            ]);
        }

        $mainTripId = (int) $subtripInformation->trip_id;

        $pickstandError = $this->rejectIfPickstandRequiredForInventory($mainTripId, (int) $subtrip_id);
        if ($pickstandError !== null) {
            return $pickstandError;
        }

        $boardingSearchDate = BookingInventorySupport::normalizeCalendarDate((string) $journeydate);
        $pickStandId = (int) ($pickstand ?? 0);
        $serviceDate = BookingInventorySupport::busRunForBooking(
            $mainTripId,
            (int) $subtrip_id,
            $boardingSearchDate,
            $pickStandId
        );

        BookingInventorySupport::logBookingDateContext('bookingcheck', [
            'trip_id' => $mainTripId,
            'subtrip_id' => (int) $subtrip_id,
            'search_date' => $boardingSearchDate,
            'pickstand' => $pickStandId,
            'service_date' => $serviceDate,
            'boarding_date' => $boardingSearchDate,
            'stored_ticket_date' => null,
            'stored_manifest_date' => null,
        ]);

        $ticket_token = $this->resolveCheckoutToken();
        $finalBookedSeats = $this->collectSegmentUnavailableSeats(
            (int) $subtrip_id,
            $boardingSearchDate,
            $pickStandId,
            $ticket_token
        );

        $requested_seats = is_array($seat_names)
            ? array_map('trim', $seat_names)
            : array_map('trim', array_filter(explode(',', (string) $seat_names)));

        $any_seat_matches = false;



        foreach ($requested_seats as $seat) {

            if (in_array($seat, $finalBookedSeats)) {

                $any_seat_matches = true;

                break;

            }

        }

        // if($subtripInformation->id == 344){

        //     var_dump($any_seat_matches);

        //     exit;

        // }



        if ($any_seat_matches) {

            $seatMsg = admin_bilingual_lang_strings('Localize.seat_already_booked_please_check');

            $response = [

                'status' => 'error',

                'message' => $seatMsg['en'],

                'message_ar' => $seatMsg['ar'],

            ];

        } else {

            $response = [
                'status' => 'success',
                'message' => 'Seats are available.'
            ];

        }



        return $this->response->setJSON($response);



    }

    public function getDynamicPickupDropCombinations($pickup, $drop, $tripSeries, $busStartDate, $tripId, $requestingSubtripId = null, $userJourneyDate = null, $pickStandId = 0)
    {
        $this->db = \Config\Database::connect();

        $stops = array_values(array_filter(array_map('trim', explode(',', (string) $tripSeries))));
        if (empty($stops)) {
            return [];
        }

        $positionMap = [];
        foreach ($stops as $idx => $stopId) {
            $positionMap[(string) $stopId] = $idx;
        }

        $spanPick = $pickup;
        $spanDrop = $drop;

        if (!isset($positionMap[(string) $spanPick]) || !isset($positionMap[(string) $spanDrop])) {
            return [];
        }

        $requestStoppage = '';
        if ((int) ($requestingSubtripId ?? 0) > 0) {
            $reqSub = $this->subtripModel->find((int) $requestingSubtripId);
            $requestStoppage = $reqSub ? trim((string) ($reqSub->stoppage ?? '')) : '';
        }

        $departureDates = BookingInventorySupport::resolveSearchDepartureDates(
            (int) $tripId,
            (int) ($requestingSubtripId ?? 0),
            (string) $busStartDate,
            (int) $pickStandId
        );

        $builder = $this->db->table('tickets');
        $builder->select(
            'tickets.*, subtrips.pick_location_id AS subtrip_pick_id, subtrips.drop_location_id AS subtrip_drop_id, '
            . 'subtrips.stoppage AS subtrip_stoppage, '
            . '(SELECT MIN(DATE(jl.journeydate)) FROM journeylists jl '
            . 'WHERE jl.booking_id = tickets.booking_id AND jl.deleted_at IS NULL) AS manifest_journeydate',
            false
        );
        $builder->join('subtrips', 'tickets.subtrip_id = subtrips.id', 'LEFT');
        $builder->where('tickets.trip_id', $tripId);
        BookingInventorySupport::applyTicketJourneyDateFilter($builder, (int) $tripId, $departureDates);
        $builder->where('tickets.cancel_status', 0);
        $builder->where('tickets.refund', 0);
        $tickets = $builder->get()->getResultArray();

        $overlappingTickets = [];
        $busStart = $departureDates['bus_start'];

        foreach ($tickets as $ticket) {
            $ticketSubtripId = (int) ($ticket['subtrip_id'] ?? 0) ?: (int) ($requestingSubtripId ?? 0);
            if (! BookingInventorySupport::ticketBelongsToBusRun(
                (int) $tripId,
                $ticketSubtripId,
                (string) ($ticket['journeydata'] ?? ''),
                (int) ($ticket['pick_stand_id'] ?? 0),
                $busStart,
                isset($ticket['manifest_journeydate']) ? (string) $ticket['manifest_journeydate'] : null
            )) {
                continue;
            }

            $segment = SegmentSeatAvailability::resolveBookingSegmentFromRow($stops, $ticket);
            if ($segment === null) {
                continue;
            }

            if (SegmentSeatAvailability::segmentsOverlapWithPath(
                $stops,
                $spanPick,
                $spanDrop,
                $requestStoppage,
                $segment['pick'],
                $segment['drop'],
                (string) ($ticket['subtrip_stoppage'] ?? '')
            )) {
                $overlappingTickets[] = $ticket;
            }
        }

        return $overlappingTickets;
    }

    /**
     * @param array<int,string> $stops
     * @param array<string,mixed> $ticket
     * @return array{pick:string,drop:string}|null
     */
    private function resolveBookingSegmentForOverlap(array $stops, array $ticket): ?array
    {
        $candidates = [];

        $ticketPick = (string) ($ticket['pick_location_id'] ?? '');
        $ticketDrop = (string) ($ticket['drop_location_id'] ?? '');
        if ($ticketPick !== '' && $ticketDrop !== '') {
            $candidates[] = [$ticketPick, $ticketDrop];
        }

        $subPick = (string) ($ticket['subtrip_pick_id'] ?? '');
        $subDrop = (string) ($ticket['subtrip_drop_id'] ?? '');
        if ($subPick !== '' && $subDrop !== '') {
            $candidates[] = [$subPick, $subDrop];
        }

        $positionMap = [];
        foreach ($stops as $idx => $stopId) {
            $positionMap[(string) $stopId] = $idx;
        }

        foreach ($candidates as [$pick, $drop]) {
            if (isset($positionMap[$pick], $positionMap[$drop]) && $positionMap[$pick] !== $positionMap[$drop]) {
                return ['pick' => $pick, 'drop' => $drop];
            }
        }

        $otherSub = (object) [
            'pick_location_id' => $subPick !== '' ? $subPick : $ticketPick,
            'drop_location_id' => $subDrop !== '' ? $subDrop : $ticketDrop,
            'stoppage' => (string) ($ticket['subtrip_stoppage'] ?? ''),
        ];

        return $this->expandSubtripToTripSpan($stops, $otherSub);
    }

    /**
     * Expand subtrip pick/drop (+ subtrip stoppage) to the full span on the main trip stop list,
     * so temporary holds block seats for the whole subtrip on the bus, not a thin slice.
     *
     * @param array<int,string> $stops Main trip stoppage in order
     * @param object|array|null $subtrip Subtrip row (pick_location_id, drop_location_id, stoppage)
     * @return array{pick:string,drop:string}|null
     */
    private function expandSubtripToTripSpan(array $stops, $subtrip): ?array
    {
        if (empty($stops) || $subtrip === null) {
            return null;
        }

        $pick = (string) (is_array($subtrip) ? ($subtrip['pick_location_id'] ?? '') : ($subtrip->pick_location_id ?? ''));
        $drop = (string) (is_array($subtrip) ? ($subtrip['drop_location_id'] ?? '') : ($subtrip->drop_location_id ?? ''));
        $stoppageRaw = is_array($subtrip) ? ($subtrip['stoppage'] ?? '') : ($subtrip->stoppage ?? '');
        $extra = array_filter(array_map('trim', explode(',', (string) $stoppageRaw)));

        $ids = array_unique(array_filter(array_merge([$pick, $drop], $extra)));

        $positionMap = [];
        foreach ($stops as $idx => $stopId) {
            $positionMap[(string) $stopId] = $idx;
        }

        $indices = [];
        foreach ($ids as $id) {
            if ($id !== '' && isset($positionMap[$id])) {
                $indices[] = $positionMap[$id];
            }
        }

        if (empty($indices)) {
            return null;
        }

        $minIdx = min($indices);
        $maxIdx = max($indices);

        return [
            'pick' => (string) $stops[$minIdx],
            'drop' => (string) $stops[$maxIdx],
        ];
    }

    public function getDynamicPickupDropCombinationsTemp($pickup, $drop, $tripSeries, $date, $subtripId, $ignoreToken = null, $userJourneyDate = null, $pickStandId = 0)
    {
        $this->db = \Config\Database::connect();

        $stops = array_values(array_filter(array_map('trim', explode(',', (string) $tripSeries))));
        $positionMap = [];
        foreach ($stops as $idx => $stopId) {
            $positionMap[(string) $stopId] = $idx;
        }

        $reqSub = $subtripId ? $this->db->table('subtrips')->where('id', $subtripId)->get()->getRow() : null;
        $tripFilter = $reqSub ? (int) $reqSub->trip_id : 0;
        $spanPick = $pickup;
        $spanDrop = $drop;

        if (! SegmentSeatAvailability::canResolveSegment($stops, $spanPick, $spanDrop)) {
            return [];
        }

        $requestStoppage = $reqSub ? trim((string) ($reqSub->stoppage ?? '')) : '';

        $departureDates = BookingInventorySupport::resolveSearchDepartureDates(
            $tripFilter,
            (int) $subtripId,
            (string) $date,
            (int) $pickStandId
        );

        $builder = $this->db->table('temporarybooks');
        $builder->select('temporarybooks.*, temporarybooks.journey_date, subtrips.pick_location_id, subtrips.drop_location_id, subtrips.stoppage AS existing_stoppage');
        $builder->join('subtrips', 'temporarybooks.subtrip_id = subtrips.id', 'LEFT');
        $builder->join('trips', 'subtrips.trip_id = trips.id', 'LEFT');
        BookingInventorySupport::applyTemporaryBookJourneyDateFilter($builder, $departureDates);
        \App\Libraries\TemporaryBookHoldService::applyActiveHoldExpiryFilter($builder);
        $builder->where('trips.status', 1);

        if ($reqSub) {
            $builder->where('subtrips.trip_id', $reqSub->trip_id);
        }

        if ($ignoreToken) {
            $builder->where('TRIM(temporarybooks.ticket_token) !=', trim($ignoreToken));
        }

        $tempBooks = $builder->get()->getResultArray();

        $overlappingTempBooks = [];
        $busStart = $departureDates['bus_start'];

        foreach ($tempBooks as $tbook) {
            if (! BookingInventorySupport::holdBelongsToBusRun((string) ($tbook['journey_date'] ?? ''), $busStart)) {
                continue;
            }

            $segment = SegmentSeatAvailability::resolveBookingSegmentFromRow($stops, [
                'pick_location_id' => $tbook['pick_location_id'] ?? '',
                'drop_location_id' => $tbook['drop_location_id'] ?? '',
                'subtrip_pick_id' => $tbook['pick_location_id'] ?? '',
                'subtrip_drop_id' => $tbook['drop_location_id'] ?? '',
                'subtrip_stoppage' => $tbook['existing_stoppage'] ?? '',
            ]);
            if ($segment === null) {
                continue;
            }

            if (SegmentSeatAvailability::segmentsOverlapWithPath(
                $stops,
                $spanPick,
                $spanDrop,
                $requestStoppage,
                $segment['pick'],
                $segment['drop'],
                (string) ($tbook['existing_stoppage'] ?? '')
            )) {
                $overlappingTempBooks[] = $tbook;
            }
        }

        return $overlappingTempBooks;
    }

    private function stopIndexOnRoute(array $stops, $locationId): ?int
    {
        foreach ($stops as $idx => $stopId) {
            if ((string) $stopId === (string) $locationId) {
                return (int) $idx;
            }
        }

        return null;
    }

    /**
     * @return list<int>
     */
    private function traveledStopIndices(array $stops, $pick, $drop): array
    {
        $pickIdx = $this->stopIndexOnRoute($stops, $pick);
        $dropIdx = $this->stopIndexOnRoute($stops, $drop);
        if ($pickIdx === null || $dropIdx === null || $pickIdx === $dropIdx) {
            return [];
        }

        if ($pickIdx < $dropIdx) {
            return range($pickIdx, $dropIdx);
        }

        return range($dropIdx, $pickIdx);
    }

    private function segmentsOverlapInStops(
        array $stops,
        $pickup,
        $drop,
        $existingPickup,
        $existingDrop,
        ?string $requestSubtripStoppage = null,
        ?string $existingSubtripStoppage = null
    ): bool {
        return SegmentSeatAvailability::segmentsOverlapWithPath(
            $stops,
            $pickup,
            $drop,
            $requestSubtripStoppage,
            $existingPickup,
            $existingDrop,
            $existingSubtripStoppage
        );
    }

    private function getSegmentRange(array $stops, $pickup, $drop): ?array
    {
        if (empty($stops)) {
            return null;
        }

        $positionMap = [];
        foreach ($stops as $idx => $stopId) {
            $positionMap[(string) $stopId] = $idx;
        }

        $pickupKey = (string) $pickup;
        $dropKey = (string) $drop;
        if (!isset($positionMap[$pickupKey]) || !isset($positionMap[$dropKey])) {
            return null;
        }

        $min = min($positionMap[$pickupKey], $positionMap[$dropKey]);
        $max = max($positionMap[$pickupKey], $positionMap[$dropKey]);

        return $min === $max ? null : [$min, $max];
    }

    /**
     * Same segment + bus-run inventory as busSeatNew / collectApiSeatInventory.
     *
     * @return list<string>
     */
    protected function collectSegmentUnavailableSeats(
        int $subtripId,
        string $boardingSearchDate,
        int $pickStandId,
        ?string $ignoreTempToken = null
    ): array {
        $subtrip = $this->subtripModel->find($subtripId);
        if (! $subtrip) {
            return [];
        }

        return (new SegmentSeatInventoryService($this->db))->collectUnavailableSeatNumbers(
            $subtripId,
            $boardingSearchDate,
            $pickStandId,
            $ignoreTempToken,
            $subtrip->pick_location_id,
            $subtrip->drop_location_id
        );
    }

    protected function rejectIfPickstandRequiredForInventory(int $mainTripId, int $subtripId): ?\CodeIgniter\HTTP\ResponseInterface
    {
        $pickStandId  = (int) $this->requestInput('pickstand');
        $overnightRun = new OvernightTripRunSupport();
        $message      = $overnightRun->validatePickStandForSeatInventory($mainTripId, $subtripId, $pickStandId);
        if ($message === null) {
            return null;
        }

        return $this->response->setJSON([
            'status' => 'failed',
            'response' => 422,
            'message' => $message,
        ]);
    }
}
