<?php

namespace App\Libraries;

use CodeIgniter\Database\BaseBuilder;
use CodeIgniter\Database\BaseConnection;
use Modules\Ticket\Models\TicketModel;
use Modules\Trip\Models\TripModel;
use Modules\Trip\Models\SubtripModel;
use App\Libraries\HoldSeatSupport;

/**
 * Trip-level seat inventory: main route stoppage + bus departure dates (incl. midnight trips).
 *
 * Calendar-date entry points (use these instead of ad-hoc strtotime/date):
 * - syncBookingTimezone() — Egypt PHP + MySQL before seat/hold queries
 * - normalizeCalendarDate() — any user/API date → Y-m-d (Africa/Cairo)
 * - busRunForTripSearch() — normalize only (legacy; prefer resolveSearchDepartureDates with subtrip)
 * - resolveSearchDepartureDates() — trip search / seat map (user date = boarding day at pickup)
 * - busRunForBooking() — payment / journeylists / save hold (bus run from trip search, normalize only)
 * - ticketBoardingDateForStorage() — tickets.journeydata from bus run + pick stand
 * - holdBelongsToBusRun() — temporarybooks.journey_date vs searched bus run
 */
class BookingInventorySupport
{
    public static function syncBookingTimezone(): void
    {
        EgyptTime::ensurePhpDefault();
        if (function_exists('sync_mysql_egypt_timezone')) {
            sync_mysql_egypt_timezone();
        }
    }

    public static function normalizeCalendarDate(string $dateOrDatetime): string
    {
        return EgyptTime::toDateString($dateOrDatetime);
    }

    /** @deprecated Prefer resolveSearchDepartureDates() when subtrip context is known. */
    public static function busRunForTripSearch(string $userSelectedDate): string
    {
        return (new TripJourneyDateHelper())->resolveBusRunDateForTripSearch(
            self::normalizeCalendarDate($userSelectedDate)
        );
    }

    /**
     * Trip search / seat map / booking validation: resolve service + passenger boarding dates.
     *
     * When the pick stand boards on the search calendar day, search date is boarding day.
     * When it does not (e.g. 07 Jul evening-bus search + 01:00 origin stand), search date
     * is the intended service/manifest day for that stand.
     *
     * @return array{bus_start: string, boarding: string, all: list<string>}
     */
    public static function resolveSearchDepartureDates(
        int $tripId,
        int $subtripId,
        string $userSelectedDate,
        int $pickStandId = 0
    ): array {
        $search = self::normalizeCalendarDate($userSelectedDate);

        if ($tripId <= 0 || $subtripId <= 0) {
            return self::bundleDepartureDates([
                'bus_start' => $search,
                'boarding'  => $search,
            ], $tripId, $subtripId);
        }

        if ($pickStandId <= 0) {
            $pickStandId = self::resolveDefaultPickStandId($tripId, $subtripId);
        }

        $helper    = new TripJourneyDateHelper();
        $eligible  = $helper->pickStandIdsBoardingOnSearchDate($tripId, $subtripId, $search);

        if ($pickStandId > 0 && $eligible !== [] && ! in_array($pickStandId, $eligible, true)) {
            $adminDates         = self::resolveAdminBookingDates($tripId, $subtripId, $search, $pickStandId);
            $busIfSearchIsBoarding = $helper->boardingDayToBusRunDate($tripId, $subtripId, $search, $pickStandId);
            $boardsOnSearchFromPriorRun = $helper->getBoardingDateFromBusStartDate(
                $tripId,
                $subtripId,
                $busIfSearchIsBoarding,
                $pickStandId
            ) === $search;
            $hasEveningOnSearch = $helper->segmentHasEveningPickupOnSearchDate($tripId, $subtripId, $search);

            // Evening bus departs on search day; 1 AM pickup boards the next calendar day (Scenario B: 17→18, 29→30).
            if ($hasEveningOnSearch
                && $adminDates['bus_start'] === $search
                && $adminDates['boarding'] > $search
                && $boardsOnSearchFromPriorRun
            ) {
                return self::bundleDepartureDates($adminDates, $tripId, $subtripId);
            }

            // Search is boarding day for 1 AM on that morning from the prior evening bus (Scenario A: 15 Jun + 01:00).
            if ($boardsOnSearchFromPriorRun) {
                return self::bundleDepartureDates([
                    'bus_start' => $busIfSearchIsBoarding,
                    'boarding'  => $search,
                ], $tripId, $subtripId);
            }

            return self::bundleDepartureDates($adminDates, $tripId, $subtripId);
        }

        $busStart  = $helper->boardingDayToBusRunDate($tripId, $subtripId, $search, $pickStandId);
        $boarding  = $helper->getBoardingDateFromBusStartDate($tripId, $subtripId, $busStart, $pickStandId);

        return self::bundleDepartureDates([
            'bus_start' => $busStart,
            'boarding'  => $boarding,
        ], $tripId, $subtripId);
    }

    /**
     * @param array{bus_start: string, boarding: string, all?: list<string>} $dates
     * @return array{bus_start: string, boarding: string, all: list<string>}
     */
    protected static function bundleDepartureDates(array $dates, int $tripId, int $subtripId): array
    {
        $normalized = self::normalizeDepartureDates($dates);
        if ($tripId > 0 && $subtripId > 0) {
            $normalized['all'] = self::expandInventoryDatesForBusRun(
                $tripId,
                $subtripId,
                $normalized['bus_start'],
                $normalized['all']
            );
        }

        return $normalized;
    }

    /**
     * Persist ticket + journey list dates from the admin booking form.
     *
     * Default: form journeydate is passenger boarding day (trip search semantics).
     * Optional serviceDateOverride: manifest / bus-run day when the UI explicitly passes bus_service_date.
     *
     * @return array{bus_start: string, boarding: string, all: list<string>}
     */
    public static function resolveBookingStorageDates(
        int $mainTripId,
        int $subtripId,
        string $formCalendarDate,
        int $pickStandId,
        ?string $serviceDateOverride = null,
        ?string $boardingDateHint = null
    ): array {
        if ($serviceDateOverride !== null && trim($serviceDateOverride) !== '') {
            return self::resolveAdminBookingDates(
                $mainTripId,
                $subtripId,
                self::normalizeCalendarDate($serviceDateOverride),
                $pickStandId
            );
        }

        if ($boardingDateHint !== null && trim($boardingDateHint) !== '') {
            $boarding = self::normalizeCalendarDate($boardingDateHint);
            $helper   = new TripJourneyDateHelper();
            $busStart = $helper->boardingDayToBusRunDate($mainTripId, $subtripId, $boarding, $pickStandId);

            return self::bundleDepartureDates([
                'bus_start' => $busStart,
                'boarding'  => $boarding,
            ], $mainTripId, $subtripId);
        }

        return self::resolveSearchDepartureDates(
            $mainTripId,
            $subtripId,
            $formCalendarDate,
            $pickStandId
        );
    }

    /**
     * Explicit manifest / service-day booking (bus_service_date hidden field).
     * Search date is the bus run day; pickstand determines passenger boarding day.
     *
     * @return array{bus_start: string, boarding: string, all: list<string>}
     */
    public static function resolveAdminBookingDates(
        int $mainTripId,
        int $subtripId,
        string $searchCalendarDate,
        int $pickStandId
    ): array {
        $search = self::normalizeCalendarDate($searchCalendarDate);

        if ($mainTripId <= 0 || $subtripId <= 0) {
            return self::normalizeDepartureDates([
                'bus_start' => $search,
                'boarding'  => $search,
            ]);
        }

        if ($pickStandId <= 0) {
            $pickStandId = self::resolveDefaultPickStandId($mainTripId, $subtripId);
        }

        $helper          = new TripJourneyDateHelper();
        $boardingAtStand = $helper->getBoardingDateFromBusStartDate(
            $mainTripId,
            $subtripId,
            $search,
            $pickStandId
        );

        if ($boardingAtStand !== $search) {
            return self::normalizeDepartureDates([
                'bus_start' => $search,
                'boarding'  => $boardingAtStand,
            ]);
        }

        $runIfSearchIsBoarding = $helper->boardingDayToBusRunDate(
            $mainTripId,
            $subtripId,
            $search,
            $pickStandId
        );

        return self::normalizeDepartureDates([
            'bus_start' => $runIfSearchIsBoarding,
            'boarding'  => $search,
        ]);
    }

    /**
     * Booking / payment / hold storage: same search + pickstand rules as resolveSearchDepartureDates().
     */
    public static function busRunForBooking(int $mainTripId, int $subtripId, string $userCalendarDate, int $pickStandId): string
    {
        return self::resolveSearchDepartureDates(
            $mainTripId,
            $subtripId,
            $userCalendarDate,
            $pickStandId
        )['bus_start'];
    }

    public static function ticketBoardingDateForStorage(int $mainTripId, int $subtripId, string $busRunDate, int $pickStandId): string
    {
        return (new TripJourneyDateHelper())->getTicketJourneyDateForStorage(
            $mainTripId,
            $subtripId,
            self::normalizeCalendarDate($busRunDate),
            $pickStandId
        );
    }

    public static function holdBelongsToBusRun(string $holdJourneyDate, string $busStartDate): bool
    {
        return self::normalizeCalendarDate($holdJourneyDate) === self::normalizeCalendarDate($busStartDate);
    }

    public static function resolveTripStoppageForOverlap(int $tripId, int $subtripId = 0): string
    {
        if ($tripId > 0) {
            $mainTrip = (new TripModel())->find($tripId);
            if ($mainTrip && trim((string) ($mainTrip->stoppage ?? '')) !== '') {
                return trim((string) $mainTrip->stoppage);
            }
        }

        if ($subtripId > 0) {
            $sub = (new SubtripModel())->find($subtripId);

            return $sub ? trim((string) ($sub->stoppage ?? '')) : '';
        }

        return '';
    }

    /**
     * Bus start date (inventory key) and passenger boarding calendar date (midnight trips).
     *
     * @return array{bus_start: string, boarding: string, all: list<string>}
     */
    public static function resolveDepartureDates(
        int $tripId,
        int $subtripId,
        string $busStartDate,
        ?string $boardingDate = null,
        int $pickStandId = 0
    ): array {
        $busStart = self::normalizeCalendarDate($busStartDate);

        if ($boardingDate !== null && $boardingDate !== '') {
            $boarding = self::normalizeCalendarDate($boardingDate);
        } elseif ($tripId > 0 && $subtripId > 0) {
            $boarding = (new TripJourneyDateHelper())->getBoardingDateFromBusStartDate(
                $tripId,
                $subtripId,
                $busStart,
                $pickStandId
            );
        } else {
            $boarding = $busStart;
        }

        $all = self::buildInventoryMatchDates($busStart, $boarding);
        if ($tripId > 0 && $subtripId > 0) {
            $all = self::expandInventoryDatesForBusRun($tripId, $subtripId, $busStart, $all);
        }

        return [
            'bus_start' => $busStart,
            'boarding' => $boarding,
            'all' => $all,
        ];
    }

    /**
     * All calendar days that may appear in tickets.journeydata for one physical bus run.
     *
     * @param list<string> $seedDates
     * @return list<string>
     */
    public static function expandInventoryDatesForBusRun(
        int $tripId,
        int $subtripId,
        string $busStartDate,
        array $seedDates = []
    ): array {
        $busStart = self::normalizeCalendarDate($busStartDate);
        if ($busStart === '1970-01-01') {
            return [];
        }

        $dates   = $seedDates;
        $dates[] = $busStart;

        $helper = new TripJourneyDateHelper();
        foreach ($helper->listSegmentPickupStandIds($tripId, $subtripId) as $pickStandId) {
            $dates[] = $helper->getBoardingDateFromBusStartDate($tripId, $subtripId, $busStart, $pickStandId);
        }

        $normalized = array_values(array_unique(array_filter(array_map(static function ($d) {
            $n = self::normalizeCalendarDate((string) $d);

            return $n !== '1970-01-01' ? $n : null;
        }, $dates))));

        sort($normalized);

        return $normalized;
    }

    /**
     * Calendar dates for seat inventory on one physical bus departure.
     *
     * Same-day trips use only bus_start. Midnight trips add boarding day so Cairo and
     * after-midnight pickups on one run match in SQL; callers must still post-filter with
     * ticketBelongsToBusRun() so adjacent calendar searches do not double-count seats.
     *
     * @return list<string>
     */
    public static function buildInventoryMatchDates(string $busStart, string $boarding): array
    {
        $busStart = self::normalizeCalendarDate($busStart);
        $boarding = self::normalizeCalendarDate($boarding);

        if ($busStart === $boarding) {
            return [$busStart];
        }

        return [$busStart, $boarding];
    }

    /**
     * Whether a paid ticket belongs on the bus run for the searched manifest day.
     */
    public static function ticketBelongsToBusRun(
        int $tripId,
        int $subtripId,
        string $ticketJourneyData,
        int $pickStandId,
        string $busStartDate,
        ?string $manifestJourneyDate = null
    ): bool {
        $busStartDate = self::normalizeCalendarDate($busStartDate);
        $ticketDay    = self::normalizeCalendarDate($ticketJourneyData);

        if ($busStartDate === '1970-01-01' || $ticketDay === '1970-01-01') {
            return false;
        }

        if ($tripId <= 0) {
            return $ticketDay === $busStartDate;
        }

        if ($pickStandId <= 0) {
            $pickStandId = self::resolveDefaultPickStandId($tripId, $subtripId);
        }

        if ($subtripId <= 0) {
            return $ticketDay === $busStartDate;
        }

        $helper = new TripJourneyDateHelper();
        $ticketBusRun = $helper->resolveBusRunDayFromTicketJourneyData(
            $tripId,
            $subtripId,
            $ticketJourneyData,
            $pickStandId,
            $manifestJourneyDate
        );

        return $ticketBusRun === $busStartDate;
    }

    /**
     * @param array{bus_start?: string, boarding?: string, all?: list<string>}|list<string> $departureDates
     * @return array{bus_start: string, boarding: string, all: list<string>}
     */
    public static function normalizeDepartureDates($departureDates): array
    {
        if (isset($departureDates['all']) || isset($departureDates['bus_start'])) {
            $busStart = isset($departureDates['bus_start'])
                ? date('Y-m-d', strtotime((string) $departureDates['bus_start']))
                : date('Y-m-d', strtotime((string) ($departureDates['all'][0] ?? 'today')));
            $boarding = isset($departureDates['boarding'])
                ? date('Y-m-d', strtotime((string) $departureDates['boarding']))
                : $busStart;
            $all = $departureDates['all'] ?? self::buildInventoryMatchDates($busStart, $boarding);

            return [
                'bus_start' => $busStart,
                'boarding' => $boarding,
                'all' => array_values(array_unique(array_filter(array_map(static function ($d) {
                    return date('Y-m-d', strtotime((string) $d));
                }, $all)))),
            ];
        }

        $list = array_values(array_unique(array_filter(array_map(static function ($d) {
            return date('Y-m-d', strtotime((string) $d));
        }, (array) $departureDates))));

        $busStart = $list[0] ?? date('Y-m-d');
        $boarding = $list[1] ?? $busStart;

        return [
            'bus_start' => $busStart,
            'boarding' => $boarding,
            'all' => $list !== [] ? $list : self::buildInventoryMatchDates($busStart, $boarding),
        ];
    }

    /**
     * First pickup pickdrops.id for a subtrip (pickdrops.trip_id = subtrips.id).
     */
    public static function resolveDefaultPickStandId(int $tripId, int $subtripId): int
    {
        if ($subtripId <= 0) {
            return 0;
        }

        $db = \Config\Database::connect();

        $row = $db->table('pickdrops')
            ->select('id')
            ->where('trip_id', $subtripId)
            ->where('type', 1)
            ->orderBy('id', 'ASC')
            ->get()
            ->getRow();

        if ($row) {
            return (int) $row->id;
        }

        $mainSub = $db->table('subtrips')
            ->select('id')
            ->where('trip_id', $tripId)
            ->where('type', 'main')
            ->get()
            ->getRow();

        if ($mainSub) {
            $row = $db->table('pickdrops')
                ->select('id')
                ->where('trip_id', (int) $mainSub->id)
                ->where('type', 1)
                ->orderBy('id', 'ASC')
                ->get()
                ->getRow();

            if ($row) {
                return (int) $row->id;
            }
        }

        return 0;
    }

    /**
     * All seat numbers on a trip departure (any segment). Use only for reporting — not for
     * segment seat maps; the same seat may be sold again after drop-off on a later leg.
     *
     * @return list<string>
     */
    public static function getAllBookedSeatsOnTripDeparture(
        int $tripId,
        int $subtripId,
        string $busStartDate,
        ?string $boardingDate = null,
        int $pickStandId = 0
    ): array {
        if ($tripId <= 0) {
            return [];
        }

        $dates = self::resolveDepartureDates($tripId, $subtripId, $busStartDate, $boardingDate, $pickStandId);

        $builder = (new TicketModel())->builder();
        $builder->select('tickets.seatnumber, tickets.booking_id, tickets.journeydata, tickets.pick_stand_id, tickets.subtrip_id')
            ->where('tickets.trip_id', $tripId)
            ->where('tickets.cancel_status', 0)
            ->where('tickets.refund', 0);

        self::applyTicketJourneyDateFilter($builder, $tripId, $dates);

        $rows = $builder->get()->getResultArray();
        $busStart = $dates['bus_start'];
        $filtered = [];

        foreach ($rows as $row) {
            if (! self::ticketBelongsToBusRun(
                $tripId,
                $subtripId > 0 ? $subtripId : (int) ($row['subtrip_id'] ?? 0),
                (string) ($row['journeydata'] ?? ''),
                (int) ($row['pick_stand_id'] ?? 0),
                $busStart
            )) {
                continue;
            }

            $filtered[] = $row;
        }

        return self::flattenSeatNumbers($filtered, 'seatnumber');
    }

    /**
     * Confirmed + temp overlap queries: match journeydata and journeylists on bus departure dates.
     *
     * @param array{bus_start?: string, boarding?: string, all?: list<string>}|list<string> $departureDates
     */
    public static function applyTicketJourneyDateFilter(BaseBuilder $builder, int $tripId, $departureDates): void
    {
        $dates = self::normalizeDepartureDates($departureDates);
        $matchDates = $dates['all'];

        if ($matchDates === []) {
            return;
        }

        $builder->groupStart();

        foreach ($matchDates as $i => $d) {
            if ($i === 0) {
                $builder->where('DATE(tickets.journeydata)', $d);
            } else {
                $builder->orWhere('DATE(tickets.journeydata)', $d);
            }
        }

        if ($tripId > 0) {
            $db = \Config\Database::connect();
            $jl = $db->table('journeylists')->select('journeylists.booking_id')
                ->join('tickets tjl', 'tjl.booking_id = journeylists.booking_id', 'inner')
                ->where('journeylists.trip_id', $tripId)
                ->where('tjl.trip_id', $tripId)
                ->where('tjl.cancel_status', 0)
                ->where('tjl.refund', 0)
                ->where('journeylists.deleted_at', null);

            $jl->groupStart();
            foreach ($matchDates as $j => $d) {
                if ($j === 0) {
                    $jl->where('DATE(journeylists.journeydate)', $d);
                } else {
                    $jl->orWhere('DATE(journeylists.journeydate)', $d);
                }
            }
            $jl->groupEnd();

            $bookingIds = array_values(array_unique(array_column($jl->get()->getResultArray(), 'booking_id')));
            if ($bookingIds !== []) {
                $builder->orWhereIn('tickets.booking_id', $bookingIds);
            }
        }

        $builder->groupEnd();
    }

    public static function applyTemporaryBookJourneyDateFilter(BaseBuilder $builder, $departureDates): void
    {
        $normalized = self::normalizeDepartureDates($departureDates);
        $dates = self::buildInventoryMatchDates($normalized['bus_start'], $normalized['boarding']);

        if (count($dates) === 1) {
            $builder->where('DATE(temporarybooks.journey_date)', $dates[0]);

            return;
        }

        $builder->groupStart();
        foreach ($dates as $i => $d) {
            if ($i === 0) {
                $builder->where('DATE(temporarybooks.journey_date)', $d);
            } else {
                $builder->orWhere('DATE(temporarybooks.journey_date)', $d);
            }
        }
        $builder->groupEnd();
    }

    /**
     * Active admin hold seats for a trip departure (trip-level holds).
     *
     * @return list<string>
     */
    public static function getAdminHeldSeatsOnTripDeparture(
        int $tripId,
        string $busStartDate,
        ?string $boardingDate = null
    ): array {
        if ($tripId <= 0) {
            return [];
        }

        $dates = self::resolveDepartureDates($tripId, 0, $busStartDate, $boardingDate);

        return self::flattenSeatNumbers(
            HoldSeatSupport::fetchActiveHoldRowsForTrip($tripId, $dates['all']),
            'seat_number'
        );
    }

    /**
     * Same-bus seats + admin holds (segment overlap handled separately).
     *
     * @return list<string>
     */
    public static function getTripDepartureBlockedSeats(
        int $tripId,
        int $subtripId,
        string $busStartDate,
        ?string $boardingDate = null,
        int $pickStandId = 0
    ): array {
        return array_values(array_unique(array_merge(
            self::getAllBookedSeatsOnTripDeparture($tripId, $subtripId, $busStartDate, $boardingDate, $pickStandId),
            self::getAdminHeldSeatsOnTripDeparture($tripId, $busStartDate, $boardingDate)
        )));
    }

    public static function lockTripRowForUpdate(BaseConnection $db, int $tripId): void
    {
        if ($tripId > 0) {
            $db->query('SELECT id FROM trips WHERE id = ? FOR UPDATE', [$tripId]);
        }
    }

    /**
     * Debug trace for overnight date handling (writable/logs/booking_dates.log).
     *
     * @param array<string, mixed> $context
     */
    public static function logBookingDateContext(string $operation, array $context): void
    {
        $payload = array_merge(
            ['operation' => $operation, 'logged_at' => egypt_now_sql()],
            $context
        );
        $line = json_encode($payload, JSON_UNESCAPED_UNICODE) . PHP_EOL;
        @file_put_contents(WRITEPATH . 'logs/booking_dates.log', $line, FILE_APPEND | LOCK_EX);
    }

    /**
     * @param list<array<string,mixed>> $rows
     * @return list<string>
     */
    public static function flattenSeatNumbers(array $rows, string $seatColumn): array
    {
        $out = [];
        foreach ($rows as $row) {
            foreach (array_filter(array_map('trim', explode(',', (string) ($row[$seatColumn] ?? '')))) as $seat) {
                if ($seat !== '') {
                    $out[] = $seat;
                }
            }
        }

        return array_values(array_unique($out));
    }
}
