<?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
     */
    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"));
    }

    /**
     * Passenger boarding date for UI/search.
     *
     * tickets.journeydata stores the bus start date. A brief regression stored boarding
     * date directly; detect that legacy shape so we never add the pickup offset twice.
     */
    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;
        }
        // Stored journey date is treated as the service run date (bus start date).
        // Convert once for display to passenger boarding date.
        return $this->getBoardingDateFromBusStartDate($mainTripId, $subtripId, $stored, $pickStandId);
    }

    /**
     * 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.
     *
     * - Search date = passenger boarding date → include (e.g. Ras Sudr after midnight on 11-06).
     * - Search date = bus start date → include full bus run (e.g. all passengers on 10-06 departure).
     */
    public function rowMatchesJourneyListSearchDate(string $busStartDate, string $boardingDate, string $searchDate): bool
    {
        $busStartDate   = date('Y-m-d', strtotime($busStartDate));
        $boardingDate   = date('Y-m-d', strtotime($boardingDate));
        $searchDate     = date('Y-m-d', strtotime($searchDate));

        if ($boardingDate === $searchDate) {
            return true;
        }

        return $busStartDate === $searchDate;
    }

    /**
     * Bus start dates to query when searching a journey list by passenger boarding date.
     *
     * @return string[]
     */
    public function getBusStartDatesForBoardingSearch(string $searchDate): array
    {
        $searchDate = date('Y-m-d', strtotime($searchDate));

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

    /**
     * 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 when the admin searches by passenger boarding date.
     * Includes legacy rows stored as bus-start date (previous calendar day).
     */
    public function applyTicketBoardingDateSearch(\CodeIgniter\Database\BaseBuilder $builder, string $searchDate): void
    {
        $searchDate = date('Y-m-d', strtotime($searchDate));
        $matchDates = $this->getBusStartDatesForBoardingSearch($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();
    }
}
