<?php

namespace App\Libraries;

use App\Libraries\TripJourneyDateHelper;

/**
 * Admin hold_seats — uses same EgyptTime (Africa/Cairo) as the rest of the booking site.
 */
class HoldSeatSupport
{
    /** Minimum minutes between startdate and enddate on admin hold_seats. */
    public const MIN_BLOCK_MINUTES = 5;

    public static function normalizeDateTime(?string $value): ?string
    {
        return parse_egypt_wall_clock($value);
    }

    /** Current Egypt instant (includes stale-tzdata DST correction when applicable). */
    public static function nowDateTime(): \DateTime
    {
        return EgyptTime::parse('now');
    }

    public static function nowString(): string
    {
        return self::nowDateTime()->format('Y-m-d H:i:s');
    }

    /**
     * Validate manual start/end (Egypt time). Does not auto-adjust — returns an error message when invalid.
     *
     * @return array{ok: bool, start: string|null, end: string|null, error: string|null}
     */
    public static function parseBlockWindow(?string $start, ?string $end): array
    {
        $start = self::normalizeDateTime($start);
        $end = self::normalizeDateTime($end);

        if ($start === null || $start === '' || $end === null || $end === '') {
            return [
                'ok' => false,
                'start' => $start,
                'end' => $end,
                'error' => 'Start date and end date are required.',
            ];
        }

        try {
            $startDt = EgyptTime::parse($start);
            $endDt = EgyptTime::parse($end);
        } catch (\Exception $e) {
            return [
                'ok' => false,
                'start' => $start,
                'end' => $end,
                'error' => 'Invalid start or end date.',
            ];
        }

        if ($endDt->getTimestamp() <= $startDt->getTimestamp()) {
            return [
                'ok' => false,
                'start' => $start,
                'end' => $end,
                'error' => 'End date must be after start date.',
            ];
        }

        $durationMinutes = (int) floor(($endDt->getTimestamp() - $startDt->getTimestamp()) / 60);
        if ($durationMinutes < self::MIN_BLOCK_MINUTES) {
            return [
                'ok' => false,
                'start' => $start,
                'end' => $end,
                'error' => 'Hold window must be at least ' . self::MIN_BLOCK_MINUTES . ' minutes (Egypt time).',
            ];
        }

        return ['ok' => true, 'start' => $start, 'end' => $end, 'error' => null];
    }

    /**
     * Admin panel: hold is "active" only inside [startdate, enddate] (used for badges / overlap checks).
     */
    public static function applyActiveWindow($builder, ?string $now = null): void
    {
        sync_mysql_egypt_timezone();
        $now = $now ?? self::nowString();
        $builder->where('startdate <=', $now);
        $builder->where('enddate >=', $now);
    }

    /**
     * Customer seat map / booking: block only inside [startdate, enddate] (Egypt time).
     * Before start → seats available; after end → seats open again.
     */
    public static function applyClientBlockWindow($builder, ?string $now = null): void
    {
        self::applyActiveWindow($builder, $now);
    }

    /**
     * Active admin hold rows for a trip (time window checked in PHP only).
     *
     * @param list<string> $departureDateList
     * @return list<array<string,mixed>>
     */
    /**
     * @param bool $expireStale When true, sets status=0 on rows past enddate before loading.
     *                          Seat maps should pass false and rely on isBlockingNow() so a row
     *                          is not hidden in the same request after enddate passes.
     */
    public static function fetchActiveHoldRowsForTrip(int $tripId, array $departureDateList, bool $expireStale = false): array
    {
        if ($tripId <= 0) {
            return [];
        }

        sync_mysql_egypt_timezone();
        if ($expireStale) {
            self::expireStaleAdminHolds();
        }

        $db = \Config\Database::connect();
        $builder = $db->table('hold_seats')
            ->select('seat_number, subtrip_id, startdate, enddate, status')
            ->where('trip_id', $tripId)
            ->where('status', 1);

        self::applyJourneyDateFilterForClient($builder, $departureDateList);

        $rows = [];
        foreach ($builder->get()->getResultArray() as $row) {
            if ((int) ($row['status'] ?? 0) !== 1) {
                continue;
            }
            if (! self::isBlockingNow($row['startdate'] ?? null, $row['enddate'] ?? null)) {
                continue;
            }
            $rows[] = $row;
        }

        return $rows;
    }

    /**
     * @return array{seats: list<string>, egypt_now: string, matched_rows: int}
     */
    public static function getActiveBlockedSeatNumbersWithMeta(int $tripId, int $subtripId, array $departureDateList): array
    {
        $seats = [];
        $matched = 0;
        foreach (self::fetchActiveHoldRowsForTripSubtrip($tripId, $subtripId, $departureDateList, false) as $row) {
            $matched++;
            foreach (self::explodeSeatNumbers($row['seat_number'] ?? '') as $seat) {
                $seats[] = $seat;
            }
        }

        return [
            'seats' => array_values(array_unique($seats)),
            'egypt_now' => self::nowString(),
            'matched_rows' => $matched,
        ];
    }

    public static function getActiveBlockedSeatNumbers(int $tripId, int $subtripId, array $departureDateList): array
    {
        return self::getActiveBlockedSeatNumbersWithMeta($tripId, $subtripId, $departureDateList)['seats'];
    }

    /**
     * Explain why holds do or do not apply (admin seat map debugging).
     *
     * @param list<string> $departureDateList
     * @return list<array<string,mixed>>
     */
    public static function diagnoseHoldSeatLookup(int $tripId, int $subtripId, array $departureDateList, int $limit = 8): array
    {
        if ($tripId <= 0) {
            return [];
        }

        sync_mysql_egypt_timezone();
        $db = \Config\Database::connect();
        $builder = $db->table('hold_seats')
            ->select('id, seat_number, subtrip_id, journeydata, journeydataend, startdate, enddate, status')
            ->where('trip_id', $tripId)
            ->orderBy('id', 'DESC')
            ->limit(max(1, $limit));

        self::applyJourneyDateFilterForClient($builder, $departureDateList);

        $out = [];
        foreach ($builder->get()->getResultArray() as $row) {
            $subtripCsv = array_values(array_filter(array_map('trim', explode(',', (string) ($row['subtrip_id'] ?? '')))));
            $status = (int) ($row['status'] ?? 0);
            $out[] = [
                'id' => (int) ($row['id'] ?? 0),
                'seats' => (string) ($row['seat_number'] ?? ''),
                'includes_subtrip_' . $subtripId => in_array((string) $subtripId, $subtripCsv, true),
                'journey' => (string) ($row['journeydata'] ?? '') . ' → ' . (string) ($row['journeydataend'] ?? ''),
                'egypt_window' => (string) ($row['startdate'] ?? '') . ' → ' . (string) ($row['enddate'] ?? ''),
                'status' => $status,
                'egypt_now_at_check' => self::nowString(),
                'timing' => self::timingStatus($status, $row['startdate'] ?? null, $row['enddate'] ?? null),
                'blocking_now' => self::isBlockingNow($row['startdate'] ?? null, $row['enddate'] ?? null),
            ];
        }

        return $out;
    }

    public static function fetchActiveHoldRowsForTripSubtrip(int $tripId, int $subtripId, array $departureDateList, bool $expireStale = false): array
    {
        if ($tripId <= 0 || $subtripId <= 0) {
            return [];
        }

        $rows = [];
        foreach (self::fetchActiveHoldRowsForTrip($tripId, $departureDateList, $expireStale) as $row) {
            $subtripCsv = (string) ($row['subtrip_id'] ?? '');
            if ($subtripCsv === (string) $subtripId || in_array((string) $subtripId, array_map('trim', explode(',', $subtripCsv)), true)) {
                $rows[] = $row;
            }
        }

        return $rows;
    }

    /**
     * Admin holds for checkout (alias).
     *
     * @param list<string> $departureDateList
     * @return list<string>
     */
    public static function getSegmentBlockedSeatNumbers(
        int $tripId,
        int $subtripId,
        $requestedPick,
        $requestedDrop,
        array $departureDateList
    ): array {
        unset($requestedPick, $requestedDrop);

        return self::getActiveBlockedSeatNumbers($tripId, $subtripId, $departureDateList);
    }

    /**
     * True when Egypt "now" is inside the hold start/end window (inclusive).
     */
    public static function isBlockingNow(?string $startdate, ?string $enddate, ?string $now = null): bool
    {
        if ($startdate === null || $startdate === '' || $enddate === null || $enddate === '') {
            return false;
        }

        try {
            $nowDt = $now !== null ? EgyptTime::parse($now) : self::nowDateTime();
            $startDt = EgyptTime::parse((string) $startdate);
            $endDt = EgyptTime::parse((string) $enddate);
        } catch (\Exception $e) {
            return false;
        }

        $nowTs = $nowDt->getTimestamp();
        $startTs = $startDt->getTimestamp();
        $endTs = $endDt->getTimestamp();

        return $nowTs >= $startTs && $nowTs <= $endTs;
    }

    /** True when Egypt "now" is strictly after enddate (hold window finished). */
    public static function hasPassedEndWindow(?string $enddate, ?\DateTime $nowDt = null): bool
    {
        if ($enddate === null || $enddate === '') {
            return false;
        }

        try {
            $nowDt = $nowDt ?? self::nowDateTime();
            $endDt = EgyptTime::parse((string) $enddate);

            return $nowDt->getTimestamp() > $endDt->getTimestamp();
        } catch (\Exception $e) {
            return false;
        }
    }

    /**
     * Manual admin release (unhold) — same Egypt clock as seat map / auto-expire.
     */
    public static function releaseAdminHold(int $holdId, ?string $unholdBy = null, ?\CodeIgniter\Database\BaseConnection $db = null): bool
    {
        if ($holdId <= 0) {
            return false;
        }

        sync_mysql_egypt_timezone();
        $db = $db ?? \Config\Database::connect();
        $now = self::nowString();
        $data = [
            'status' => 0,
            'updated_at' => $now,
        ];
        if ($unholdBy !== null && $unholdBy !== '') {
            $data['unhold_by'] = $unholdBy;
        }

        $db->table('hold_seats')
            ->where('id', $holdId)
            ->where('status', 1)
            ->update($data);

        return (int) $db->affectedRows() > 0;
    }

    /**
     * Mark admin holds finished when enddate has passed (keeps status column accurate).
     */
    public static function expireStaleAdminHolds(?\CodeIgniter\Database\BaseConnection $db = null): int
    {
        sync_mysql_egypt_timezone();
        $db = $db ?? \Config\Database::connect();
        $now = self::nowString();
        $nowDt = self::nowDateTime();
        $ids = [];

        foreach ($db->table('hold_seats')->select('id, enddate')->where('status', 1)->get()->getResultArray() as $row) {
            if (self::hasPassedEndWindow($row['enddate'] ?? null, $nowDt)) {
                $ids[] = (int) ($row['id'] ?? 0);
            }
        }

        $ids = array_values(array_filter($ids, static fn (int $id) => $id > 0));
        if ($ids === []) {
            return 0;
        }

        $db->table('hold_seats')
            ->whereIn('id', $ids)
            ->update(['status' => 0, 'updated_at' => $now]);

        return (int) $db->affectedRows();
    }

    /**
     * @param mixed $subtripIds Comma string or list from form/API
     * @return list<int>
     */
    public static function parseSubtripIds($subtripIds): array
    {
        if (is_array($subtripIds)) {
            $parts = $subtripIds;
        } else {
            $parts = array_map('trim', explode(',', (string) $subtripIds));
        }

        $out = [];
        foreach ($parts as $id) {
            $id = (int) $id;
            if ($id > 0) {
                $out[] = $id;
            }
        }

        return array_values(array_unique($out));
    }

    /**
     * Bus-start + boarding calendar dates for hold_seats.journeydata / journeydataend (midnight trips).
     *
     * @param list<int> $subtripIds
     * @return array{journeydata: string, journeydataend: string, all: list<string>}
     */
    public static function resolveJourneyInventorySpan(
        int $tripId,
        array $subtripIds,
        string $journeyStart,
        string $journeyEnd,
        int $pickStandId = 0
    ): array {
        $journeyStart = EgyptTime::toDateString($journeyStart);
        $journeyEnd = EgyptTime::toDateString($journeyEnd !== '' ? $journeyEnd : $journeyStart);

        if ($journeyEnd < $journeyStart) {
            $journeyEnd = $journeyStart;
        }

        $dates = [$journeyStart, $journeyEnd];

        if ($tripId > 0 && $subtripIds !== []) {
            $helper = new TripJourneyDateHelper();
            foreach ($subtripIds as $subtripId) {
                foreach ([$journeyStart, $journeyEnd] as $calendarDay) {
                    $busStart = $helper->resolveBusRunDateForTripSearch($calendarDay);
                    $dates[] = $busStart;
                    $dates[] = $helper->getBoardingDateFromBusStartDate($tripId, $subtripId, $busStart, $pickStandId);
                }
            }
        }

        $dates = array_values(array_unique(array_filter(array_map(static function ($d) {
            try {
                return EgyptTime::toDateString((string) $d);
            } catch (\Exception $e) {
                return null;
            }
        }, $dates))));

        if ($dates === []) {
            $today = egypt_today();

            return ['journeydata' => $today, 'journeydataend' => $today, 'all' => [$today]];
        }

        sort($dates);

        return [
            'journeydata' => $dates[0],
            'journeydataend' => $dates[count($dates) - 1],
            'all' => $dates,
        ];
    }

    /**
     * Match journey date(s) against hold row journeydata / journeydataend (NULL = any date).
     * Pass multiple dates for midnight trips (bus start + boarding calendar day).
     *
     * @param string|list<string> $journeyDate
     */
    public static function applyJourneyDateFilter($builder, $journeyDate, bool $matchNullJourneyAsAnyDate = true): void
    {
        $dates = is_array($journeyDate) ? $journeyDate : [$journeyDate];
        $dates = array_values(array_unique(array_filter(array_map(static function ($d) {
            return EgyptTime::toDateString((string) $d);
        }, $dates))));

        if (empty($dates)) {
            return;
        }

        if (count($dates) === 1) {
            self::applyJourneyDateFilterSingle($builder, $dates[0], $matchNullJourneyAsAnyDate);

            return;
        }

        $builder->groupStart();
        foreach ($dates as $i => $d) {
            if ($i === 0) {
                $builder->groupStart();
                self::applyJourneyDateFilterSingle($builder, $d, $matchNullJourneyAsAnyDate);
                $builder->groupEnd();
            } else {
                $builder->orGroupStart();
                self::applyJourneyDateFilterSingle($builder, $d, $matchNullJourneyAsAnyDate);
                $builder->groupEnd();
            }
        }
        $builder->groupEnd();
    }

    /**
     * Website seat map / booking — ignore legacy rows with no journey date.
     *
     * @param string|list<string> $journeyDate
     */
    public static function applyJourneyDateFilterForClient($builder, $journeyDate): void
    {
        self::applyJourneyDateFilter($builder, $journeyDate, false);
    }

    private static function applyJourneyDateFilterSingle($builder, string $journeyDate, bool $matchNullJourneyAsAnyDate): void
    {
        $journeyDate = EgyptTime::toDateString($journeyDate);

        if ($matchNullJourneyAsAnyDate) {
            $builder->groupStart()
                ->groupStart()
                    ->where('journeydata', null)
                    ->orWhere('journeydata', '')
                    ->orWhere('journeydata', '0000-00-00')
                ->groupEnd()
                ->orGroupStart()
                    ->where('journeydata <=', $journeyDate)
                    ->groupStart()
                        ->where('journeydataend', null)
                        ->orWhere('journeydataend', '')
                        ->orWhere('journeydataend', '0000-00-00')
                        ->orWhere('journeydataend >=', $journeyDate)
                    ->groupEnd()
                ->groupEnd()
            ->groupEnd();

            return;
        }

        $builder->groupStart()
            ->where('journeydata IS NOT NULL', null, false)
            ->where('journeydata !=', '')
            ->where('journeydata !=', '0000-00-00')
            ->where('journeydata <=', $journeyDate)
            ->groupStart()
                ->where('journeydataend', null)
                ->orWhere('journeydataend', '')
                ->orWhere('journeydataend', '0000-00-00')
                ->orWhere('journeydataend >=', $journeyDate)
            ->groupEnd()
        ->groupEnd();
    }

    /**
     * @return 'scheduled'|'active'|'expired'|'unheld'
     */
    public static function timingStatus(int $status, ?string $startdate, ?string $enddate, ?string $now = null): string
    {
        if ($status !== 1) {
            return 'unheld';
        }
        if (empty($startdate) || empty($enddate)) {
            return 'active';
        }

        try {
            $nowDt = $now !== null ? EgyptTime::parse($now) : self::nowDateTime();
            $startDt = EgyptTime::parse((string) $startdate);
            $endDt = EgyptTime::parse((string) $enddate);
        } catch (\Exception $e) {
            return 'active';
        }

        if ($nowDt->getTimestamp() < $startDt->getTimestamp()) {
            return 'scheduled';
        }
        if ($nowDt->getTimestamp() > $endDt->getTimestamp()) {
            return 'expired';
        }

        return 'active';
    }

    /**
     * @return list<string>
     */
    public static function explodeSeatNumbers(?string $seatNumber): array
    {
        if ($seatNumber === null || $seatNumber === '') {
            return [];
        }

        $parts = array_map('trim', explode(',', $seatNumber));

        return array_values(array_filter($parts, static fn ($s) => $s !== ''));
    }
}
