<?php

namespace App\Libraries;

use Modules\Schedule\Models\ScheduleModel;
use Modules\Trip\Models\TripModel;
use Modules\Trip\Models\SubtripModel;
use Modules\Trip\Models\PickdropModel;

/**
 * Helper to compute the "bus start date" (main trip date) for a journey.
 *
 * IMPORTANT: pickdrops.trip_id stores subtrips.id (NOT trips.id).
 * We must first find the main subtrip (type='main') for the given trips.id,
 * then query pickdrops using that subtrip's id.
 */
class TripJourneyDateHelper
{
    protected $tripModel;
    protected $subtripModel;
    protected $pickdropModel;
    protected $scheduleModel;

    /** @var array<int, object|null> */
    protected $tripCache = [];

    /** @var array<int, object|null> */
    protected $scheduleCache = [];

    /** @var array<int, array> */
    protected $mainPickdropsCache = [];

    /** @var array<int, array> */
    protected $subtripPickupsCache = [];

    /** @var array<int, object|null> */
    protected $pickdropByIdCache = [];

    /** @var array<string, string> */
    protected $boardingDateCache = [];

    /** @var array<string, string> */
    protected $busStartDateCache = [];

    public function __construct()
    {
        $this->tripModel      = new TripModel();
        $this->subtripModel   = new SubtripModel();
        $this->pickdropModel  = new PickdropModel();
        $this->scheduleModel  = new ScheduleModel();
    }

    /**
     * Get the bus start date (main trip date) for journeylist storage.
     *
     * @param int    $mainTripId      trips.id
     * @param int    $subtripId       subtrips.id (the segment being booked)
     * @param string $userJourneyDate User-selected boarding date (Y-m-d)
     * @param int    $pickStandId     pickdrops.id OR stand_id of the pickup point
     * @return string Bus start date in Y-m-d
     */
    /**
     * Bus manifest day for journeylists.journeydate (always the service run date).
     */
    public function getManifestDateForJourneylist(
        int $mainTripId,
        int $subtripId,
        string $selectedCalendarDate,
        int $pickStandId
    ): string {
        return $this->resolveServiceRunDateForInventory(
            $mainTripId,
            $subtripId,
            $selectedCalendarDate,
            $pickStandId
        );
    }

    /**
     * @deprecated Prefer resolveServiceRunDateForInventory when input is the search run day,
     *             or getManifestDateForJourneylist for journeylists storage.
     */
    public function getBusStartDateForJourneylist(int $mainTripId, int $subtripId, string $userJourneyDate, int $pickStandId): string
    {
        $userJourneyDate = date('Y-m-d', strtotime($userJourneyDate));
        $cacheKey        = "{$mainTripId}|{$subtripId}|{$userJourneyDate}|{$pickStandId}|bus";

        if (isset($this->busStartDateCache[$cacheKey])) {
            return $this->busStartDateCache[$cacheKey];
        }

        $mainTrip = $this->getTrip($mainTripId);

        if (!$mainTrip) {
            return $this->busStartDateCache[$cacheKey] = $userJourneyDate;
        }

        $pickupDayOffset = $this->getPickupDayOffset($mainTrip, $mainTripId, $subtripId, $pickStandId);

        if ($pickupDayOffset === null) {
            return $this->busStartDateCache[$cacheKey] = $userJourneyDate;
        }

        return $this->busStartDateCache[$cacheKey] = date('Y-m-d', strtotime($userJourneyDate . " -{$pickupDayOffset} day"));
    }

    /**
     * Compute the passenger's actual boarding date from the normalized bus start date.
     * Boarding Date = Bus Start Date + offset of pickup stand.
     */
    public function getBoardingDateFromBusStartDate(int $mainTripId, int $subtripId, string $busStartDate, int $pickStandId): string
    {
        $busStartDate = date('Y-m-d', strtotime($busStartDate));
        $cacheKey     = "{$mainTripId}|{$subtripId}|{$busStartDate}|{$pickStandId}|board";

        if (isset($this->boardingDateCache[$cacheKey])) {
            return $this->boardingDateCache[$cacheKey];
        }

        $mainTrip = $this->getTrip($mainTripId);

        if (!$mainTrip) {
            return $this->boardingDateCache[$cacheKey] = $busStartDate;
        }

        $pickupDayOffset = $this->getPickupDayOffset($mainTrip, $mainTripId, $subtripId, $pickStandId);

        if ($pickupDayOffset === null) {
            return $this->boardingDateCache[$cacheKey] = $busStartDate;
        }

        return $this->boardingDateCache[$cacheKey] = date('Y-m-d', strtotime($busStartDate . " +{$pickupDayOffset} day"));
    }

    /**
     * Journey date stored on tickets (and shown on ticket list).
     *
     * User-selected date is the bus run / manifest day. When the route mixes evening and
     * after-midnight pickups, an after-midnight stand stores run-day minus offset so the
     * ticket date matches the manifest (e.g. 01:15 on 21 Jul → 20 Jul). Sub-trips whose
     * pickups are all after midnight keep the selected calendar date unchanged.
     */
    public function getTicketJourneyDateForStorage(
        int $mainTripId,
        int $subtripId,
        string $userJourneyDate,
        int $pickStandId
    ): string {
        $userJourneyDate = date('Y-m-d', strtotime($userJourneyDate));

        if ($mainTripId <= 0 || $userJourneyDate === '1970-01-01') {
            return $userJourneyDate;
        }

        if (
            $this->segmentHasOnlyAfterMidnightPickups($mainTripId, $subtripId)
            && ! $this->mainRouteHasSameEveningPickup($mainTripId)
        ) {
            return $userJourneyDate;
        }

        $mainTrip = $this->getTrip($mainTripId);
        if (! $mainTrip) {
            return $userJourneyDate;
        }

        $pickupDayOffset = $this->getPickupDayOffset($mainTrip, $mainTripId, $subtripId, $pickStandId);

        if ($pickupDayOffset !== null && $pickupDayOffset >= 1) {
            return date('Y-m-d', strtotime($userJourneyDate . " +{$pickupDayOffset} day -1 day"));
        }

        return $userJourneyDate;
    }

    /**
     * Service run date (bus manifest day) for seat inventory and temporary holds.
     *
     * Accepts either the run day from search/booking or a legacy boarding calendar day.
     */
    public function resolveServiceRunDateForInventory(
        int $mainTripId,
        int $subtripId,
        string $calendarDate,
        int $pickStandId
    ): string {
        $calendarDate = date('Y-m-d', strtotime($calendarDate));

        if ($mainTripId <= 0 || $calendarDate === '1970-01-01') {
            return $calendarDate;
        }

        $mainTrip = $this->getTrip($mainTripId);
        if (! $mainTrip) {
            return $calendarDate;
        }

        $pickupDayOffset = $this->getPickupDayOffset($mainTrip, $mainTripId, $subtripId, $pickStandId);
        if ($pickupDayOffset === null || $pickupDayOffset < 1) {
            return $calendarDate;
        }

        $inferredRun = date('Y-m-d', strtotime($calendarDate . " -{$pickupDayOffset} day"));
        $boardingOfInferred = $this->getBoardingDateFromBusStartDate(
            $mainTripId,
            $subtripId,
            $inferredRun,
            $pickStandId
        );

        if ($calendarDate === $boardingOfInferred) {
            return $inferredRun;
        }

        $boardingIfCalendarIsRun = $this->getBoardingDateFromBusStartDate(
            $mainTripId,
            $subtripId,
            $calendarDate,
            $pickStandId
        );

        if ($boardingIfCalendarIsRun !== $calendarDate) {
            return $calendarDate;
        }

        return $calendarDate;
    }

    /**
     * Journey date for ticket list, invoices, and API — normalized ticket/manifest day.
     */
    public function getPassengerBoardingDateForDisplay(
        int $mainTripId,
        int $subtripId,
        string $storedJourneyDate,
        int $pickStandId
    ): string {
        $stored = date('Y-m-d', strtotime($storedJourneyDate));
        if ($stored === '1970-01-01' || $mainTripId <= 0) {
            return $stored;
        }

        $mainTrip = $this->getTrip($mainTripId);
        if (! $mainTrip) {
            return $stored;
        }

        $pickupDayOffset = $this->getPickupDayOffset($mainTrip, $mainTripId, $subtripId, $pickStandId);

        if ($pickupDayOffset !== null && $pickupDayOffset >= 1) {
            $ticketIfStoredIsRunDay = $this->getTicketJourneyDateForStorage(
                $mainTripId,
                $subtripId,
                $stored,
                $pickStandId
            );
            if ($ticketIfStoredIsRunDay === $stored) {
                return $stored;
            }

            $inferredRun = date('Y-m-d', strtotime($stored . " -{$pickupDayOffset} day"));
            $boardingOfInferred = $this->getBoardingDateFromBusStartDate(
                $mainTripId,
                $subtripId,
                $inferredRun,
                $pickStandId
            );
            if ($stored === $boardingOfInferred) {
                return $this->getTicketJourneyDateForStorage(
                    $mainTripId,
                    $subtripId,
                    $inferredRun,
                    $pickStandId
                );
            }
        }

        $boardingIfStoredIsRun = $this->getBoardingDateFromBusStartDate(
            $mainTripId,
            $subtripId,
            $stored,
            $pickStandId
        );

        if ($boardingIfStoredIsRun !== $stored) {
            return $stored;
        }

        return $stored;
    }

    /**
     * Calendar dates to match when searching the ticket list by journey date.
     *
     * @return list<string>
     */
    public function getTicketSearchMatchDates(string $searchDate): array
    {
        $searchDate = date('Y-m-d', strtotime($searchDate));

        return array_values(array_unique([
            $searchDate,
            date('Y-m-d', strtotime($searchDate . ' -1 day')),
            date('Y-m-d', strtotime($searchDate . ' +1 day')),
        ]));
    }

    /**
     * Pre-warm boarding-date cache for a set of ticket/journey rows (same request).
     *
     * @param iterable<object> $rows Objects with trip_id, subtrip_id, journeydata|journeydate, pick_stand_id
     */
    public function warmBoardingDateCache(iterable $rows): void
    {
        foreach ($rows as $row) {
            $this->getPassengerBoardingDateForDisplay(
                (int) ($row->trip_id ?? 0),
                (int) ($row->subtrip_id ?? 0),
                (string) ($row->journeydata ?? $row->journeydate ?? ''),
                (int) ($row->pick_stand_id ?? 0)
            );
        }
    }

    /**
     * Normalize time string to 24h format for comparison (e.g. "11:30PM" -> "23:30").
     */
    protected function normalizeTime(string $time): string
    {
        $time = trim($time);
        if (empty($time)) {
            return '00:00';
        }
        $timestamp = strtotime('1970-01-01 ' . $time);

        return $timestamp !== false ? date('H:i', $timestamp) : '00:00';
    }

    protected function getTrip(int $mainTripId): ?object
    {
        if ($mainTripId <= 0) {
            return null;
        }

        if (!array_key_exists($mainTripId, $this->tripCache)) {
            $this->tripCache[$mainTripId] = $this->tripModel->find($mainTripId) ?: null;
        }

        return $this->tripCache[$mainTripId];
    }

    protected function getSchedule(int $scheduleId): ?object
    {
        if ($scheduleId <= 0) {
            return null;
        }

        if (!array_key_exists($scheduleId, $this->scheduleCache)) {
            $this->scheduleCache[$scheduleId] = $this->scheduleModel->find($scheduleId) ?: null;
        }

        return $this->scheduleCache[$scheduleId];
    }

    /**
     * Return how many days after the bus start date this pickup happens.
     */
    protected function getPickupDayOffset(object $mainTrip, int $mainTripId, int $subtripId, int $pickStandId): ?int
    {
        $schedule          = $this->getSchedule((int) ($mainTrip->schedule_id ?? 0));
        $mainTripStartTime = $schedule->start_time ?? '00:00';

        $mainPickdrops = $this->getMainSubtripPickdrops($mainTripId);
        if (!empty($mainPickdrops)) {
            $offset = $this->getOffsetFromPickdropSequence($mainPickdrops, $mainTripStartTime, $pickStandId);
            if ($offset !== null) {
                return $offset;
            }

            $offset = $this->getOffsetFromMainRouteByPickdropStand($mainPickdrops, $mainTripStartTime, $pickStandId);
            if ($offset !== null) {
                return $offset;
            }
        }

        return $this->getOffsetFromActualPickup($subtripId, $pickStandId, $mainTripStartTime);
    }

    /**
     * Match a subtrip-only pickup stand to the main trip pickdrop list by stand_id.
     */
    protected function getOffsetFromMainRouteByPickdropStand(array $mainPickdrops, string $mainTripStartTime, int $pickStandId): ?int
    {
        $pickdrop = $this->getPickdropById($pickStandId);
        if (!$pickdrop) {
            return null;
        }

        $standId = (int) ($pickdrop->stand_id ?? 0);
        if ($standId <= 0) {
            return null;
        }

        foreach ($mainPickdrops as $item) {
            if ((int) ($item->stand_id ?? 0) !== $standId) {
                continue;
            }

            return $this->getOffsetFromPickdropSequence($mainPickdrops, $mainTripStartTime, (int) $item->id);
        }

        return null;
    }

    protected function getMainSubtripPickdrops(int $mainTripId): array
    {
        if (array_key_exists($mainTripId, $this->mainPickdropsCache)) {
            return $this->mainPickdropsCache[$mainTripId];
        }

        $mainSubtrip = $this->subtripModel
            ->where('trip_id', $mainTripId)
            ->where('type', 'main')
            ->first();

        if (!$mainSubtrip) {
            return $this->mainPickdropsCache[$mainTripId] = [];
        }

        return $this->mainPickdropsCache[$mainTripId] = $this->pickdropModel
            ->select('pickdrops.id, pickdrops.stand_id, pickdrops.time, pickdrops.type')
            ->where('trip_id', (int) $mainSubtrip->id)
            ->orderBy('pickdrops.id', 'ASC')
            ->findAll();
    }

    protected function getOffsetFromPickdropSequence(array $pickdrops, string $mainTripStartTime, int $pickStandId): ?int
    {
        $targetStandId = $this->resolveTargetStandId($pickdrops, $pickStandId);

        $lastStopTime  = $mainTripStartTime;
        $currentOffset = 0;

        foreach ($pickdrops as $item) {
            $itemTime     = $this->normalizeTime($item->time ?? '00:00');
            $lastStopTime = $this->normalizeTime($lastStopTime);

            if (strtotime('1970-01-01 ' . $itemTime) < strtotime('1970-01-01 ' . $lastStopTime)) {
                $currentOffset++;
            }
            $lastStopTime = $itemTime;

            if ($pickStandId > 0 && (int) $item->id === $pickStandId) {
                return $currentOffset;
            }

            if ($targetStandId !== null && (int) $item->stand_id === $targetStandId) {
                return $currentOffset;
            }
        }

        return null;
    }

    protected function resolveTargetStandId(array $pickdrops, int $pickStandId): ?int
    {
        if ($pickStandId <= 0) {
            return null;
        }

        foreach ($pickdrops as $item) {
            if ((int) $item->id === $pickStandId) {
                return (int) $item->stand_id;
            }
        }

        $pdRecord = $this->getPickdropById($pickStandId);

        return $pdRecord ? (int) $pdRecord->stand_id : $pickStandId;
    }

    protected function getPickdropById(int $pickStandId): ?object
    {
        if ($pickStandId <= 0) {
            return null;
        }

        if (!array_key_exists($pickStandId, $this->pickdropByIdCache)) {
            $this->pickdropByIdCache[$pickStandId] = $this->pickdropModel
                ->select('id, stand_id, time, type, trip_id')
                ->find($pickStandId) ?: null;
        }

        return $this->pickdropByIdCache[$pickStandId];
    }

    protected function getSubtripPickups(int $subtripId): array
    {
        if ($subtripId <= 0) {
            return [];
        }

        if (array_key_exists($subtripId, $this->subtripPickupsCache)) {
            return $this->subtripPickupsCache[$subtripId];
        }

        return $this->subtripPickupsCache[$subtripId] = $this->pickdropModel
            ->select('id, stand_id, time, type')
            ->where('trip_id', $subtripId)
            ->where('type', 1)
            ->orderBy('id', 'ASC')
            ->findAll();
    }

    protected function getOffsetFromActualPickup(int $subtripId, int $pickStandId, string $mainTripStartTime): ?int
    {
        if ($pickStandId <= 0) {
            return null;
        }

        $pickdrop = $this->getPickdropById($pickStandId);

        if (!$pickdrop) {
            $subtripPickups = $this->getSubtripPickups($subtripId);
            foreach ($subtripPickups as $row) {
                if ((int) $row->stand_id === $pickStandId) {
                    $pickdrop = $row;
                    break;
                }
            }
        }

        if (!$pickdrop) {
            return null;
        }

        $subtripPickups = $this->getSubtripPickups($subtripId);

        if (!empty($subtripPickups)) {
            $offset = $this->getOffsetFromPickdropSequence($subtripPickups, $mainTripStartTime, $pickStandId);
            if ($offset !== null) {
                return $offset;
            }
        }

        $pickupTime = $this->normalizeTime($pickdrop->time ?? '00:00');
        $startTime  = $this->normalizeTime($mainTripStartTime);

        return strtotime('1970-01-01 ' . $pickupTime) < strtotime('1970-01-01 ' . $startTime) ? 1 : 0;
    }

    /**
     * Whether a journeylist row belongs on the manifest for the searched calendar date.
     *
     * Manifest day is journeylists.journeydate (bus run date), not passenger boarding day.
     */
    public function rowMatchesJourneyListSearchDate(string $busStartDate, string $boardingDate, string $searchDate): bool
    {
        unset($boardingDate);

        return date('Y-m-d', strtotime($busStartDate)) === date('Y-m-d', strtotime($searchDate));
    }

    /**
     * Calendar dates to load from journeylists for a manifest search.
     *
     * @return string[]
     */
    public function getBusStartDatesForBoardingSearch(string $searchDate): array
    {
        return [date('Y-m-d', strtotime($searchDate))];
    }

    /**
     * Parse a user search string that is meant to be a calendar date (Y-m-d, d-m-Y, d/m/Y).
     */
    public static function parseSearchDate(string $value): ?string
    {
        $value = trim($value);
        if ($value === '') {
            return null;
        }

        $formats = ['Y-m-d', 'd-m-Y', 'd/m/Y', 'm/d/Y'];
        foreach ($formats as $format) {
            $dt = \DateTime::createFromFormat($format, $value);
            if ($dt && $dt->format($format) === $value) {
                return $dt->format('Y-m-d');
            }
        }

        if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) {
            return date('Y-m-d', strtotime($value));
        }

        return null;
    }

    /**
     * Match tickets.journeydata for ticket-list date search (stored journey date).
     */
    public function applyTicketBoardingDateSearch(\CodeIgniter\Database\BaseBuilder $builder, string $searchDate): void
    {
        $matchDates = $this->getTicketSearchMatchDates($searchDate);

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

    /**
     * Main route has at least one pickup at or after the scheduled departure time (same calendar evening).
     */
    protected function mainRouteHasSameEveningPickup(int $mainTripId): bool
    {
        $mainTrip = $this->getTrip($mainTripId);
        if (! $mainTrip) {
            return false;
        }

        $schedule          = $this->getSchedule((int) ($mainTrip->schedule_id ?? 0));
        $mainTripStartTime = $this->normalizeTime($schedule->start_time ?? '00:00');
        $mainPickdrops     = $this->getMainSubtripPickdrops($mainTripId);

        foreach ($mainPickdrops as $pickup) {
            if ((int) ($pickup->type ?? 0) !== 1) {
                continue;
            }

            $pickupTime = $this->normalizeTime($pickup->time ?? '00:00');
            if (strtotime('1970-01-01 ' . $pickupTime) >= strtotime('1970-01-01 ' . $mainTripStartTime)) {
                return true;
            }
        }

        return false;
    }

    /**
     * True when every pickup on the booked segment is after midnight relative to the main schedule start.
     */
    protected function segmentHasOnlyAfterMidnightPickups(int $mainTripId, int $subtripId): bool
    {
        $mainTrip = $this->getTrip($mainTripId);
        if (! $mainTrip) {
            return false;
        }

        $schedule          = $this->getSchedule((int) ($mainTrip->schedule_id ?? 0));
        $mainTripStartTime = $this->normalizeTime($schedule->start_time ?? '00:00');
        $pickups           = $this->getBookableSegmentPickups($mainTripId, $subtripId);

        if ($pickups === []) {
            return false;
        }

        foreach ($pickups as $pickup) {
            $pickupTime = $this->normalizeTime($pickup->time ?? '00:00');
            if (strtotime('1970-01-01 ' . $pickupTime) >= strtotime('1970-01-01 ' . $mainTripStartTime)) {
                return false;
            }
        }

        return true;
    }

    /**
     * Pickup points for the segment being booked (main route or subtrip).
     *
     * @return array<int, object>
     */
    protected function getBookableSegmentPickups(int $mainTripId, int $subtripId): array
    {
        $subtrip = $this->subtripModel->find($subtripId);
        if (! $subtrip) {
            return $this->getMainSubtripPickdrops($mainTripId);
        }

        if (($subtrip->type ?? '') === 'main') {
            return $this->getMainSubtripPickdrops($mainTripId);
        }

        $subtripPickups = $this->getSubtripPickups($subtripId);

        return $subtripPickups !== [] ? $subtripPickups : $this->getMainSubtripPickdrops($mainTripId);
    }
}
