<?php

namespace App\Libraries;

/**
 * Subtrip ticket booking — segment overlap on the main trip stop list (ordered location IDs).
 *
 * Used when checking whether a seat is already sold for the requested pick/drop on the
 * same bus departure, including tickets booked under other subtrips on that trip.
 *
 * Rule: conflict when both journeys share a stop that is not a handoff
 * (one drops where the other picks at the same index → seat may be sold again).
 */
class SegmentSeatAvailability
{
    /**
     * @return list<string>
     */
    public static function parseStoppage(?string $stoppage): array
    {
        return array_values(array_filter(array_map('trim', explode(',', (string) $stoppage))));
    }

    public static function canResolveSegment(array $stops, $pickup, $drop): bool
    {
        return self::stopIndexOnRoute($stops, $pickup) !== null
            && self::stopIndexOnRoute($stops, $drop) !== null
            && self::stopIndexOnRoute($stops, $pickup) !== self::stopIndexOnRoute($stops, $drop);
    }

    /**
     * @param array<string,mixed> $row Ticket or temporarybook row joined with subtrip fields.
     * @return array{pick: string, drop: string}|null
     */
    public static function resolveBookingSegmentFromRow(array $stops, array $row): ?array
    {
        $candidates = [];

        $ticketPick = (string) ($row['pick_location_id'] ?? '');
        $ticketDrop = (string) ($row['drop_location_id'] ?? '');
        if ($ticketPick !== '' && $ticketDrop !== '') {
            $candidates[] = [$ticketPick, $ticketDrop];
        }

        $subPick = (string) ($row['subtrip_pick_id'] ?? $row['pick_location_id'] ?? '');
        $subDrop = (string) ($row['subtrip_drop_id'] ?? $row['drop_location_id'] ?? '');
        if ($subPick !== '' && $subDrop !== '') {
            $candidates[] = [$subPick, $subDrop];
        }

        foreach ($candidates as [$pick, $drop]) {
            if (self::canResolveSegment($stops, $pick, $drop)) {
                return ['pick' => $pick, 'drop' => $drop];
            }
        }

        return null;
    }

    /**
     * Overlap using subtrip path when available (multi-stop legs on the same bus).
     *
     * Falls back to {@see segmentsOverlap()} when path data is missing.
     */
    public static function segmentsOverlapWithPath(
        array $stops,
        $requestedPick,
        $requestedDrop,
        ?string $requestedSubtripStoppage,
        $existingPick,
        $existingDrop,
        ?string $existingSubtripStoppage,
        $requestedPickStand = 0,
        $requestedDropStand = 0,
        $existingPickStand = 0,
        $existingDropStand = 0,
        ?array $pickdropRoute = null
    ): bool {
        if ($pickdropRoute !== null && $pickdropRoute !== []) {
            $standOverlap = self::segmentsOverlapOnPickdropRoute(
                $pickdropRoute,
                $requestedPickStand,
                $requestedDropStand,
                $existingPickStand,
                $existingDropStand
            );
            if ($standOverlap !== null) {
                return $standOverlap;
            }
        }

        $reqVisited = self::visitedIndicesForBooking($stops, $requestedSubtripStoppage, $requestedPick, $requestedDrop);
        $exVisited = self::visitedIndicesForBooking($stops, $existingSubtripStoppage, $existingPick, $existingDrop);

        if ($reqVisited !== null && $exVisited !== null) {
            $isHandoffPair = (string) $requestedDrop === (string) $existingPick
                || (string) $existingDrop === (string) $requestedPick;

            if (! $isHandoffPair && self::segmentsOverlap($stops, $requestedPick, $requestedDrop, $existingPick, $existingDrop)) {
                return true;
            }

            $reqPathInterior = self::pathInteriorStopIds($requestedSubtripStoppage, $requestedPick, $requestedDrop);
            $exPathInterior = self::pathInteriorStopIds($existingSubtripStoppage, $existingPick, $existingDrop);

            if ($reqPathInterior !== [] && in_array((string) $existingPick, $reqPathInterior, true)) {
                return true;
            }

            if ($exPathInterior !== [] && in_array((string) $requestedPick, $exPathInterior, true)) {
                return true;
            }

            $reqInterior = self::interiorVisitedIndices($stops, $requestedPick, $requestedDrop, $reqVisited);
            $exInterior = self::interiorVisitedIndices($stops, $existingPick, $existingDrop, $exVisited);
            if (array_intersect($reqInterior, $exInterior) !== []) {
                return true;
            }

            $shared = array_values(array_intersect($reqVisited, $exVisited));
            if (count($shared) === 1) {
                return ! self::isHandoffAtSharedStop(
                    $stops,
                    $requestedPick,
                    $requestedDrop,
                    $existingPick,
                    $existingDrop,
                    $shared[0]
                );
            }

            return false;
        }

        return self::segmentsOverlap($stops, $requestedPick, $requestedDrop, $existingPick, $existingDrop);
    }

    /**
     * Stand-level overlap on the ordered pickdrop route (boarding / alighting points).
     *
     * @param list<array{id: int, stand_id: int}> $route
     * @return bool|null true = conflict, false = allowed, null = cannot resolve stands
     */
    public static function segmentsOverlapOnPickdropRoute(
        array $route,
        $requestedPickStand,
        $requestedDropStand,
        $existingPickStand,
        $existingDropStand
    ): ?bool {
        $reqPickIdx = TripPickdropRoute::indexOnRoute($route, $requestedPickStand);
        $reqDropIdx = TripPickdropRoute::indexOnRoute($route, $requestedDropStand);
        $exPickIdx = TripPickdropRoute::indexOnRoute($route, $existingPickStand);
        $exDropIdx = TripPickdropRoute::indexOnRoute($route, $existingDropStand);

        if ($reqPickIdx === null || $reqDropIdx === null || $exPickIdx === null || $exDropIdx === null) {
            return null;
        }

        if ($reqPickIdx === $reqDropIdx || $exPickIdx === $exDropIdx) {
            return false;
        }

        [$exFrom, $exTo] = self::normalizeTravelSpan($exPickIdx, $exDropIdx);
        [$reqFrom, $reqTo] = self::normalizeTravelSpan($reqPickIdx, $reqDropIdx);

        // Handoff: new passenger boards exactly where the other alights.
        if ($reqFrom === $exTo || $exFrom === $reqTo) {
            return false;
        }

        return $exFrom < $reqTo && $exTo > $reqFrom;
    }

    /**
     * @return array{0: int, 1: int}
     */
    private static function normalizeTravelSpan(int $pickIdx, int $dropIdx): array
    {
        if ($pickIdx <= $dropIdx) {
            return [$pickIdx, $dropIdx];
        }

        return [$dropIdx, $pickIdx];
    }

    /**
     * Main-route indices visited between pick and drop (subtrip stoppage order when set).
     *
     * @return list<int>|null
     */
    public static function visitedIndicesForBooking(
        array $mainStops,
        ?string $subtripStoppage,
        $pick,
        $drop
    ): ?array {
        $pick = (string) $pick;
        $drop = (string) $drop;
        $path = self::parseStoppage($subtripStoppage);

        if ($path !== [] && in_array($pick, $path, true) && in_array($drop, $path, true)) {
            $pickPos = array_search($pick, $path, true);
            $dropPos = array_search($drop, $path, true);
            if ($pickPos !== false && $dropPos !== false && $pickPos !== $dropPos) {
                $from = min((int) $pickPos, (int) $dropPos);
                $to = max((int) $pickPos, (int) $dropPos);
                $slice = array_slice($path, $from, $to - $from + 1);
                $indices = [];
                foreach ($slice as $stopId) {
                    $idx = self::stopIndexOnRoute($mainStops, $stopId);
                    if ($idx !== null) {
                        $indices[] = $idx;
                    }
                }

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

        $pickIdx = self::stopIndexOnRoute($mainStops, $pick);
        $dropIdx = self::stopIndexOnRoute($mainStops, $drop);
        if ($pickIdx === null || $dropIdx === null || $pickIdx === $dropIdx) {
            return null;
        }

        if ($pickIdx < $dropIdx) {
            return range($pickIdx, $dropIdx);
        }

        return range($dropIdx, $pickIdx);
    }

    /**
     * @param array<int, string> $stops Main trip stoppage in route order.
     */
    public static function segmentsOverlap(
        array $stops,
        $requestedPick,
        $requestedDrop,
        $existingPick,
        $existingDrop
    ): bool {
        $reqPickIdx = self::stopIndexOnRoute($stops, $requestedPick);
        $reqDropIdx = self::stopIndexOnRoute($stops, $requestedDrop);
        $exPickIdx = self::stopIndexOnRoute($stops, $existingPick);
        $exDropIdx = self::stopIndexOnRoute($stops, $existingDrop);

        if ($reqPickIdx === null || $reqDropIdx === null || $exPickIdx === null || $exDropIdx === null) {
            return false;
        }

        if ($reqPickIdx === $reqDropIdx || $exPickIdx === $exDropIdx) {
            return false;
        }

        // Occupied only when intervals overlap on the route axis (handoff at the same stop is allowed).
        // Use directional indices — do not collapse with min/max or Ras Sudr→El Tor falsely overlaps Cairo→Ras Sudr.
        // Normalize existing bookings stored with pick/drop reversed on the main stop list.
        if ($exPickIdx > $exDropIdx) {
            [$exPickIdx, $exDropIdx] = [$exDropIdx, $exPickIdx];
        }

        return $exPickIdx < $reqDropIdx && $exDropIdx > $reqPickIdx;
    }

    public static function stopIndexOnRoute(array $stops, $locationId): ?int
    {
        foreach ($stops as $idx => $stopId) {
            if ((string) $stopId === (string) $locationId) {
                return (int) $idx;
            }
        }

        return null;
    }

    /**
     * @return list<int>
     */
    public static function traveledStopIndices(array $stops, $pick, $drop): array
    {
        $pickIdx = self::stopIndexOnRoute($stops, $pick);
        $dropIdx = self::stopIndexOnRoute($stops, $drop);
        if ($pickIdx === null || $dropIdx === null || $pickIdx === $dropIdx) {
            return [];
        }

        if ($pickIdx < $dropIdx) {
            return range($pickIdx, $dropIdx);
        }

        return range($dropIdx, $pickIdx);
    }

    /**
     * Location IDs strictly between pick and drop on the subtrip stoppage path.
     *
     * @return list<string>
     */
    public static function pathInteriorStopIds(?string $subtripStoppage, $pick, $drop): array
    {
        $path = self::parseStoppage($subtripStoppage);
        $pick = (string) $pick;
        $drop = (string) $drop;

        if ($path === [] || ! in_array($pick, $path, true) || ! in_array($drop, $path, true)) {
            return [];
        }

        $pickPos = array_search($pick, $path, true);
        $dropPos = array_search($drop, $path, true);
        if ($pickPos === false || $dropPos === false || $pickPos === $dropPos) {
            return [];
        }

        $from = min((int) $pickPos, (int) $dropPos) + 1;
        $to = max((int) $pickPos, (int) $dropPos) - 1;

        if ($from > $to) {
            return [];
        }

        return array_slice($path, $from, $to - $from + 1);
    }

    /**
     * Stops occupied between pick and drop (excludes endpoints on the main route).
     *
     * @param list<int> $visited
     * @return list<int>
     */
    private static function interiorVisitedIndices(
        array $stops,
        $pick,
        $drop,
        array $visited
    ): array {
        $pickIdx = self::stopIndexOnRoute($stops, $pick);
        $dropIdx = self::stopIndexOnRoute($stops, $drop);
        if ($pickIdx === null || $dropIdx === null) {
            return $visited;
        }

        $endpoints = [$pickIdx, $dropIdx];

        return array_values(array_filter(
            $visited,
            static fn (int $idx): bool => ! in_array($idx, $endpoints, true)
        ));
    }

    /**
     * Single shared stop where one journey ends and the other starts — seat may be sold again.
     */
    private static function isHandoffAtSharedStop(
        array $stops,
        $requestedPick,
        $requestedDrop,
        $existingPick,
        $existingDrop,
        int $sharedIndex
    ): bool {
        $reqPickIdx = self::stopIndexOnRoute($stops, $requestedPick);
        $reqDropIdx = self::stopIndexOnRoute($stops, $requestedDrop);
        $exPickIdx = self::stopIndexOnRoute($stops, $existingPick);
        $exDropIdx = self::stopIndexOnRoute($stops, $existingDrop);

        if ($reqPickIdx === null || $reqDropIdx === null || $exPickIdx === null || $exDropIdx === null) {
            return false;
        }

        return ($reqDropIdx === $sharedIndex && $exPickIdx === $sharedIndex)
            || ($exDropIdx === $sharedIndex && $reqPickIdx === $sharedIndex);
    }
}
