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

    public static function nowString(): string
    {
        return egypt_now_sql();
    }

    /**
     * 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.',
            ];
        }

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

        $durationMinutes = (int) floor((strtotime($end) - strtotime($start)) / 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
    {
        $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>>
     */
    public static function fetchActiveHoldRowsForTrip(int $tripId, array $departureDateList): array
    {
        if ($tripId <= 0) {
            return [];
        }

        sync_mysql_egypt_timezone();
        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;
    }

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

        $rows = [];
        foreach (self::fetchActiveHoldRowsForTrip($tripId, $departureDateList) 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;
    }

    /**
     * Seat numbers blocked right now for trip/subtrip/journey (Egypt wall clock).
     *
     * @param list<string> $departureDateList
     * @return list<string>
     */
    public static function getActiveBlockedSeatNumbers(int $tripId, int $subtripId, array $departureDateList): array
    {
        $seats = [];
        foreach (self::fetchActiveHoldRowsForTripSubtrip($tripId, $subtripId, $departureDateList) as $row) {
            foreach (self::explodeSeatNumbers($row['seat_number'] ?? '') as $seat) {
                $seats[] = $seat;
            }
        }

        return array_values(array_unique($seats));
    }

    /**
     * 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 = EgyptTime::parse($now ?? self::nowString());
            $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;
    }

    /**
     * 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();
        $db->table('hold_seats')
            ->where('status', 1)
            ->where('enddate <', $now)
            ->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 = date('Y-m-d', strtotime($journeyStart));
        $journeyEnd = date('Y-m-d', strtotime($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->getBusStartDateForJourneylist($tripId, $subtripId, $calendarDay, $pickStandId);
                    $dates[] = $busStart;
                    $dates[] = $helper->getBoardingDateFromBusStartDate($tripId, $subtripId, $busStart, $pickStandId);
                }
            }
        }

        $dates = array_values(array_unique(array_filter(array_map(static function ($d) {
            $t = strtotime((string) $d);

            return $t !== false ? date('Y-m-d', $t) : 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 date('Y-m-d', strtotime((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 = date('Y-m-d', strtotime($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 = EgyptTime::parse($now ?? self::nowString());
            $startDt = EgyptTime::parse((string) $startdate);
            $endDt = EgyptTime::parse((string) $enddate);
        } catch (\Exception $e) {
            return 'active';
        }

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

        if ($nowTs < $startTs) {
            return 'scheduled';
        }
        if ($nowTs > $endTs) {
            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 !== ''));
    }
}
