"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
    return function (target, key) { decorator(target, key, paramIndex); }
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
var TransferFacade_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.TransferFacade = void 0;
const DateTimeUtils_1 = require("../../../../helpers/DateTimeUtils");
const GtfsStopParser_1 = require("../../../../helpers/GtfsStopParser");
const RouteTypeEnums_1 = require("../../../../helpers/RouteTypeEnums");
const data_access_1 = require("../../data-access");
const DepartureFilter_1 = require("../../helpers/enums/DepartureFilter");
const DepartureMode_1 = require("../../helpers/enums/DepartureMode");
const DepartureOrder_1 = require("../../helpers/enums/DepartureOrder");
const DepartureSkip_1 = require("../../helpers/enums/DepartureSkip");
const OgPidToken_1 = require("../../ioc/OgPidToken");
const PIDDepartureModel_1 = __importDefault(require("../../models/helpers/PIDDepartureModel"));
const DelayComputationRepository_1 = require("../../../public/data-access/redis/DelayComputationRepository");
const PublicGtfsDepartureRepository_1 = require("../../../public/data-access/redis/PublicGtfsDepartureRepository");
const PublicStopTimeRepository_1 = require("../../../public/data-access/redis/PublicStopTimeRepository");
const PublicVehiclePositionsRepository_1 = require("../../../public/data-access/redis/PublicVehiclePositionsRepository");
const shared_1 = require("../../../shared");
const monitoring_1 = require("@golemio/core/dist/monitoring");
const golemio_errors_1 = require("@golemio/core/dist/shared/golemio-errors");
const tsyringe_1 = require("@golemio/core/dist/shared/tsyringe");
const const_1 = require("../../../../const");
const TransferDepartureTransformation_1 = require("../transformations/TransferDepartureTransformation");
const DUMMY_EMPTY_STRING_FOR_FILTERING = "";
let TransferFacade = TransferFacade_1 = class TransferFacade {
    constructor(publicDepartureRepository, departuresRepository, departureTransformation, tripRepository, stopTimeRepository, delayComputationRepository) {
        this.publicDepartureRepository = publicDepartureRepository;
        this.departuresRepository = departuresRepository;
        this.departureTransformation = departureTransformation;
        this.tripRepository = tripRepository;
        this.stopTimeRepository = stopTimeRepository;
        this.delayComputationRepository = delayComputationRepository;
    }
    async getTransferDepartures(stopIds, tripNumber, currentMoment, minutesOffset, timeZone = shared_1.RopidRouterUtils.TIMEZONE) {
        let departureEntities = [];
        try {
            departureEntities = await this.departuresRepository.getTransferDepartures({
                stopsIds: stopIds,
                currentMoment,
                minutesOffset,
            });
        }
        catch (error) {
            if (error instanceof golemio_errors_1.AbstractGolemioError) {
                throw error;
            }
            throw new golemio_errors_1.GeneralError("Failed to retrieve transfer departures", this.constructor.name, error, 500);
        }
        if (departureEntities.length === 0) {
            return [];
        }
        let transferDepartures = [];
        try {
            const processedDepartures = new PIDDepartureModel_1.default(departureEntities, {
                ...this.defaultOptions,
                timezone: timeZone,
                tripNumber,
            }).processAndReturnTransfers();
            transferDepartures = this.departureTransformation.transformArray(processedDepartures);
        }
        catch (error) {
            if (error instanceof golemio_errors_1.AbstractGolemioError) {
                throw error;
            }
            throw new golemio_errors_1.GeneralError("Failed to process transfer departures", this.constructor.name, error, 500);
        }
        return transferDepartures;
    }
    static sortByDepartureTime(a, b) {
        if (a.departure_datetime < b.departure_datetime) {
            return -1;
        }
        if (a.departure_datetime > b.departure_datetime) {
            return 1;
        }
        return 0;
    }
    static isDifferentLine(departure, position) {
        if (!departure.trip_id || departure.trip_id === position.gtfs_trip_id) {
            return false; // leftover data OR same trip = remove itself
        }
        // keep different means of transport + possibly keep same line (might have more future stops)
        return true;
    }
    static jsonPathDataToLineFilterStop(stopInfo) {
        return {
            stop_id: GtfsStopParser_1.GtfsStopParser.normalizedNodeId(stopInfo[0]),
            stop_name: stopInfo[1],
            platform_code: stopInfo[2] || DUMMY_EMPTY_STRING_FOR_FILTERING,
        };
    }
    calculateTimeFrom(currentArrival, currentPosition, delayedTimeFrom, plannedTimeFrom) {
        const arrivalDate = new Date(currentArrival.arrival_datetime);
        // arrival before expected time (negative number) is taken as 0 delay
        // arrival after expected time (positive number) is taken as is
        const delayOnArrivalMs = Math.max(currentPosition.delay ?? 0, 0) * 1000;
        delayedTimeFrom.setTime(arrivalDate.getTime() + delayOnArrivalMs);
        plannedTimeFrom.setTime(arrivalDate.getTime());
    }
    async getTransferCache(stopId, stopIds, limit, reqNumber, vehicleType, timeFrom) {
        const span = (0, monitoring_1.createChildSpan)(`Departures.getTransferCache`);
        span?.setAttributes({ stopIds, limit, reqNumber, vehicleType });
        let delayedTimeFrom = new Date(timeFrom.getTime());
        let plannedTimeFrom = new Date(timeFrom.getTime());
        let currentArrival = null;
        let currentStop = null;
        let currentTripId = null;
        try {
            // 1. find the vehicle
            const currentPosition = await this.tripRepository.getDetailedVehiclePosition(`service-${vehicleType}-${reqNumber}`);
            if (currentPosition) {
                // vehicle is on the map
                // 2. find its arrival to the stop-id
                currentArrival = await this.findVehicleArrival(currentPosition, stopIds, stopId);
            }
            if (currentPosition && currentArrival) {
                currentStop = {
                    stop_id: currentArrival.stop_id,
                    stop_name: currentArrival.stop_name,
                    platform_code: currentArrival.platform_code || DUMMY_EMPTY_STRING_FOR_FILTERING,
                };
                currentTripId = currentPosition.gtfs_trip_id;
                this.calculateTimeFrom(currentArrival, currentPosition, delayedTimeFrom, plannedTimeFrom);
            }
            else {
                // Vehicle does not ride into such stop-ids, or vehicle does not have Position
                return {
                    transfers: [],
                    delayedTimeFrom,
                    plannedTimeFrom,
                    currentStop,
                    currentTripId,
                };
            }
            // timeFrom in query can be too early, minimum is the real arrival of the vehicle
            if (timeFrom.getTime() > delayedTimeFrom.getTime()) {
                delayedTimeFrom = new Date(timeFrom);
                plannedTimeFrom = new Date(timeFrom);
            }
            const delayMinutes = Math.abs(Math.floor((delayedTimeFrom.getTime() - plannedTimeFrom.getTime()) / TransferFacade_1.MINUTE_AS_MS));
            let departureEntities = [];
            try {
                // 3. find departures from all stop_ids in Node
                // look forward 1 hour (add delay + open minutes), see JIS Transferboards Design Document
                departureEntities = await this.publicDepartureRepository.getPublicGtfsDepartureCache(stopIds, 60 + TransferFacade_1.OPEN_MINUTES_IN_PAST + delayMinutes, new Date(plannedTimeFrom.getTime() - TransferFacade_1.OPEN_MINUTES_IN_PAST_MS), Math.max(10, limit * 5) // should be (at least 10) 5x the limit from request
                );
            }
            catch (error) {
                span?.recordException(error);
                if (error instanceof golemio_errors_1.AbstractGolemioError) {
                    throw error;
                }
                throw new golemio_errors_1.GeneralError("Failed to publicGtfsDepartureCache", this.constructor.name, error, 500);
            }
            // Remove current line (and trip) from departureEntities
            departureEntities = departureEntities.filter((departure) => TransferFacade_1.isDifferentLine(departure, currentPosition));
            departureEntities.sort(TransferFacade_1.sortByDepartureTime);
            const uniqueDeparturesTripIds = Array.from(new Set(departureEntities.map((d) => d.trip_id)));
            // to be able to load future trips of Before_Track positioned vehicle
            const omitFutureTrips = false;
            const positionsByTrip = await this.tripRepository.getAllVehiclePositionsForMultipleTrips(uniqueDeparturesTripIds, omitFutureTrips);
            let transfers = [];
            const moreDepartures = await this.processMultiplePositionsForDeparturesAtOnce(departureEntities, positionsByTrip);
            transfers.push(...moreDepartures);
            return {
                transfers,
                delayedTimeFrom,
                plannedTimeFrom,
                currentStop,
                currentTripId,
            };
        }
        catch (error) {
            span?.recordException(error);
            if (error instanceof golemio_errors_1.AbstractGolemioError) {
                throw error;
            }
            throw new golemio_errors_1.GeneralError("Error in getTransferCache", this.constructor.name, error, 500);
        }
        finally {
            span?.end();
        }
    }
    static findStopTimeByStopId(stopTimes, stopIds) {
        return stopTimes.find((stopTime) => stopIds.includes(stopTime.stop_id)) || null;
    }
    async findVehicleArrival(currentPosition, stopIds, stopId) {
        const stopTimes = (await this.delayComputationRepository.getDelayComputationCacheProperty(currentPosition.gtfs_trip_id, "stop_times"));
        if (!stopTimes?.stop_times?.length) {
            return null;
        }
        // It is possible (based on incoming stop_ids), we won't find departure
        // for this vehicle directly on this stopId...
        let departureAtStopId = TransferFacade_1.findStopTimeByStopId(stopTimes.stop_times, [stopId]);
        // But it can be in the whole Node = iterate through all stop_ids then.
        departureAtStopId ??= TransferFacade_1.findStopTimeByStopId(stopTimes.stop_times, stopIds);
        if (departureAtStopId) {
            const arrivalDate = DateTimeUtils_1.DateTimeUtils.getStopDateTimeForTripOrigin(departureAtStopId.arrival_time_seconds, currentPosition.detailed_info.origin_timestamp);
            const departureDate = DateTimeUtils_1.DateTimeUtils.getStopDateTimeForTripOrigin(departureAtStopId.departure_time_seconds, currentPosition.detailed_info.origin_timestamp);
            return {
                stop_name: departureAtStopId.stop.stop_name,
                stop_id: departureAtStopId.stop_id,
                departure_datetime: departureDate.toISOString(),
                arrival_datetime: arrivalDate.toISOString(),
                route_short_name: currentPosition.gtfs_route_short_name,
                route_type: currentPosition.route_type,
                trip_id: currentPosition.gtfs_trip_id,
                stop_sequence: departureAtStopId.stop_sequence,
                platform_code: null,
                trip_headsign: currentPosition.detailed_info.trip_headsign,
            };
        }
        return null;
    }
    async processMultiplePositionsForDeparturesAtOnce(departures, positionsByTrip) {
        const outputGroup = [];
        const trainIdTripIdTuples = [];
        const trainDepartures = new Map();
        for (const departure of departures) {
            // for each departure, use its trip_id + multiple position
            const positions = positionsByTrip.get(departure.trip_id) || [];
            if (!positions.length) {
                outputGroup.push({
                    departure,
                    position: null,
                    stopTime: null,
                });
                continue;
            }
            const validPositions = positions.filter((pos) => pos.state_position !== const_1.StatePositionEnum.CANCELED);
            for (const pos of validPositions) {
                if (departure.platform_code == null || departure.route_type === RouteTypeEnums_1.GTFSRouteTypeEnum.TRAIN) {
                    trainDepartures.set(departure.trip_id, departure);
                    trainIdTripIdTuples.push([pos.vehicle_id, departure.trip_id]);
                    continue;
                }
                // we have platform_code, thus we do not need to load stopTime.cis_platform_code
                outputGroup.push({
                    departure, // no stop_name
                    position: pos, // no stop_name either
                    stopTime: null,
                });
            }
        }
        const stopTimeCachesByTripId = !trainIdTripIdTuples.length
            ? new Map()
            : await this.stopTimeRepository.getManyPublicStopTimeCaches(trainIdTripIdTuples);
        for (const [tripId, stopTimeCache] of Array.from(stopTimeCachesByTripId.entries())) {
            const departure = trainDepartures.get(tripId);
            const position = positionsByTrip.get(tripId)[0];
            const stopTime = stopTimeCache.find((stopTime) => departure.stop_id === stopTime.stop_id);
            outputGroup.push({
                departure,
                position: position || null,
                stopTime: stopTime || null,
            });
        }
        return outputGroup;
    }
    static findPreviousStopId(line) {
        if (line.currentStopIndex > 0) {
            return line.stops[line.currentStopIndex - 1].stop_id;
        }
        return line.stops[line.currentStopIndex].stop_id;
    }
    static isOtherEndNearCurrent(otherStops, currentStop) {
        const next = otherStops[0];
        const last = otherStops[otherStops.length - 1];
        if (next.stop_id !== last.stop_id) {
            return false;
        }
        if (next.stop_id === currentStop.stop_id) {
            return true;
        }
        return false;
    }
    static isOtherSubsetOfCurrent(otherStops, currentStops) {
        let oIndex = 0, oLen = otherStops.length, cLen = currentStops.length;
        if (oLen > cLen) {
            // other line is longer, thus is not a subset
            return false;
        }
        for (let cIndex = 0; cIndex < cLen; cIndex++) {
            if (currentStops[cIndex].stop_id === otherStops[oIndex].stop_id) {
                oIndex++;
                if (oIndex === oLen) {
                    return true;
                }
            }
        }
        return false;
    }
    static isOppositeDirection(previousStopId, otherFutureStops) {
        return otherFutureStops.length > 0 && previousStopId === otherFutureStops[0].stop_id;
    }
    static async filterOutForwardSubgroupOfLine(params) {
        const { currentLine } = params.data;
        if (!params.data.otherLinesMap.size)
            return params;
        const currentFutureStops = currentLine.stops.slice(currentLine.currentStopIndex + 1);
        const currentFutureStopIdsSet = new Set(currentFutureStops.map((stop) => stop.stop_id));
        const previousStopId = TransferFacade_1.findPreviousStopId(currentLine);
        for (const [tripId, otherLine] of params.data.otherLinesMap.entries()) {
            if (params.data.tripsToKeep.has(tripId)) {
                // already valid trip from different filter impl.
                continue;
            }
            const otherFutureStops = otherLine.stops.slice(otherLine.currentStopIndex + 1);
            if (!otherFutureStops.length) {
                // remove it, no future stops on other line
                params.data.otherLinesMap.delete(tripId);
                continue;
            }
            // Filter 1.0: Check if next other stop is same as current stop or the line ends there
            if (TransferFacade_1.isOtherEndNearCurrent(otherFutureStops, params.data.currentStop)) {
                // remove it, transfer for 1-2 stops is not worth it
                params.data.otherLinesMap.delete(tripId);
                continue;
            }
            // Filter 1.1: Opposite direction is handled elsewhere, will skip all other Filters 1.x below
            const isOppositeDirection = TransferFacade_1.isOppositeDirection(previousStopId, otherFutureStops);
            if (isOppositeDirection) {
                // leave it for other filter function, this one solves same direction only
                continue;
            }
            // Filter 1.2: Remove trip if other future stops are a subset of current route
            const isSubsetOfCurrentFuture = TransferFacade_1.isOtherSubsetOfCurrent(otherFutureStops, currentFutureStops);
            if (isSubsetOfCurrentFuture) {
                // remove from possible transfers, not relevant transfer at all
                params.data.otherLinesMap.delete(tripId);
                continue;
            }
            // Filter 1.3: If other line has something unique to my current trip, keep it
            const hasOtherFutureStopUniqueToCurrent = otherFutureStops.some(({ stop_id }) => !currentFutureStopIdsSet.has(stop_id));
            if (hasOtherFutureStopUniqueToCurrent) {
                // keep this other-stop transfer line
                params.data.tripsToKeep.add(tripId);
            }
        }
        return params;
    }
    static async filterOutLinesWithBacktrackingConflict(params) {
        const { currentLine } = params.data;
        const previousCountMax = params.options.previousCountMax;
        if (!params.data.otherLinesMap.size)
            return params;
        const previousCount = Math.min(previousCountMax, Math.max(previousCountMax, currentLine.currentStopIndex));
        // Get previous N stops before currentStop, most recent first
        const currentLinePastStops = currentLine.stops
            .slice(0, currentLine.currentStopIndex) // all stops before current
            .reverse()
            .slice(0, previousCount + 1); // take N+1 most recent (if one would be different, line should pass)
        for (const [tripId, otherLine] of params.data.otherLinesMap.entries()) {
            if (params.data.tripsToKeep.has(tripId)) {
                continue;
            }
            const otherStopIndex = otherLine.currentStopIndex + 1;
            // Get next N+1 stops after currentStop in the other line
            const otherFutureStopsToCheck = otherLine.stops.slice(otherStopIndex, otherStopIndex + previousCount + 1);
            // Filter 2.0: Future stops of other line are subset (or same) as past stop (limit to previousCount) of current line.
            // The order of the current previous stops and other future stops is checked
            const isSubsetOfCurrentPast = TransferFacade_1.isOtherSubsetOfCurrent(otherFutureStopsToCheck, currentLinePastStops);
            if (isSubsetOfCurrentPast) {
                // remove from possible transfers, not relevant transfer at all
                params.data.otherLinesMap.delete(tripId);
                params.data.tripsToKeep.delete(tripId);
                continue;
            }
            params.data.tripsToKeep.add(tripId);
        }
        return params;
    }
    static async applyFiltersSequentially(params, filters) {
        return filters.reduce((promiseChain, filterFn) => promiseChain.then(filterFn), Promise.resolve(params));
    }
    static rawTripLinesEntriesToTransfersMap(currentStopInput, currentTripId, stopTimesMap, rawTripIds, rawLinesMap) {
        const otherLinesMap = new Map();
        // apply same transformations as later on the other trips, so we have same format of stop_id / stop_name
        const currentStop = TransferFacade_1.jsonPathDataToLineFilterStop([
            currentStopInput.stop_id,
            currentStopInput.stop_name,
            currentStopInput.platform_code,
        ]);
        for (const [tripId, rawLine] of [...rawLinesMap.entries()].concat([...stopTimesMap.entries()])) {
            if (!rawLine?.length) {
                continue;
            }
            let index = null;
            let stops = [];
            if (tripId === currentTripId || rawTripIds.has(tripId)) {
                stops = rawLine.map(TransferFacade_1.jsonPathDataToLineFilterStop);
            }
            else {
                stops = rawLine.map((s) => TransferFacade_1.jsonPathDataToLineFilterStop([s.stop_id, s.stop_name]));
            }
            if (tripId === currentTripId) {
                index = rawLine.findIndex((rawStop) => {
                    return rawStop[0] === currentStopInput.stop_id; // gtfs stop id format
                });
            }
            otherLinesMap.set(tripId, {
                stops,
                currentStopIndex: index ??
                    stops.findIndex((stop) => stop.stop_id === currentStop.stop_id && stop.stop_name === currentStop.stop_name),
            });
        }
        return otherLinesMap;
    }
    async findRelevantTripIdsFromLines(otherLinesTripIdsFromDelay, currentStopInput, currentTripId, stopTimeIds, previousCountMax = 1) {
        if (!otherLinesTripIdsFromDelay.size && !stopTimeIds.length) {
            return [];
        }
        otherLinesTripIdsFromDelay.add(currentTripId);
        const jsonPath = "$.stop_times[*].stop['stop_id','stop_name']";
        const jsonPropertiesCount = 2;
        // JSON.MGET has some serious redis/valkey CPU implications, and we can load stop-times
        // for trips where there is position from stop-times-cache ... so let's combine these two flows in parallel
        const [tripsToStopTimes, rawLinesMap] = await Promise.all([
            this.stopTimeRepository.getManyPublicStopTimeCaches(stopTimeIds),
            this.delayComputationRepository.getJsonPathFieldsAsListOfValues(Array.from(otherLinesTripIdsFromDelay.values()), jsonPath, jsonPropertiesCount),
        ]);
        if (!rawLinesMap || !tripsToStopTimes) {
            throw new golemio_errors_1.GeneralError("Failed to find lines and stops for transfer trips", this.constructor.name, "Failed to read stop_id and stop_name properties", 500);
        }
        const otherLinesMap = TransferFacade_1.rawTripLinesEntriesToTransfersMap(currentStopInput, currentTripId, tripsToStopTimes, otherLinesTripIdsFromDelay, rawLinesMap);
        const currentLine = otherLinesMap.get(currentTripId) || null;
        if (!currentLine?.stops.length) {
            throw new golemio_errors_1.GeneralError(`Cannot find data in delay cache for current trip '${currentTripId}'`, this.constructor.name);
        }
        // remove current trip from the lines map
        otherLinesMap.delete(currentTripId);
        if (!otherLinesMap.size) {
            return [];
        }
        const filters = [
            TransferFacade_1.filterOutForwardSubgroupOfLine, // keep transfers with unique stops to my current stop
            TransferFacade_1.filterOutLinesWithBacktrackingConflict, // keep oncoming traffic going to my previous stop
        ];
        const data = {
            currentStop: currentLine.stops[currentLine.currentStopIndex],
            currentLine,
            otherLinesMap,
            tripsToKeep: new Set(),
        };
        const options = {
            previousCountMax,
        };
        const params = { data, options };
        const results = await TransferFacade_1.applyFiltersSequentially(params, filters);
        const tripIdsToKeep = results.data.tripsToKeep;
        return Array.from(tripIdsToKeep.values());
    }
    /**
     * Fallback options as defined by ROPID
     */
    get defaultOptions() {
        return {
            limit: 16,
            total: 16,
            offset: 0,
            mode: DepartureMode_1.DepartureMode.DEPARTURES,
            order: DepartureOrder_1.DepartureOrder.REAL,
            filter: DepartureFilter_1.DepartureFilter.ROUTE_HEADING_ONCE_NOGAP_FILL,
            skip: [DepartureSkip_1.DepartureSkip.CANCELED],
            departuresDirections: [],
            runScheduleMap: null, // not used in this context
            untrackedTrips: null, // not used in this context
        };
    }
};
exports.TransferFacade = TransferFacade;
TransferFacade.MINUTE_AS_MS = 60 * 1000;
TransferFacade.OPEN_MINUTES_IN_PAST = 15;
TransferFacade.OPEN_MINUTES_IN_PAST_MS = TransferFacade_1.OPEN_MINUTES_IN_PAST * TransferFacade_1.MINUTE_AS_MS;
exports.TransferFacade = TransferFacade = TransferFacade_1 = __decorate([
    (0, tsyringe_1.injectable)(),
    __param(0, (0, tsyringe_1.inject)(OgPidToken_1.OgPidToken.PublicGtfsDepartureRepository)),
    __param(1, (0, tsyringe_1.inject)(OgPidToken_1.OgPidToken.DeparturesRepository)),
    __param(2, (0, tsyringe_1.inject)(OgPidToken_1.OgPidToken.TransferDepartureTransformation)),
    __param(3, (0, tsyringe_1.inject)(OgPidToken_1.OgPidToken.PublicVehiclePositionsRepository)),
    __param(4, (0, tsyringe_1.inject)(OgPidToken_1.OgPidToken.PublicStopTimeRepository)),
    __param(5, (0, tsyringe_1.inject)(OgPidToken_1.OgPidToken.DelayComputationRepository)),
    __metadata("design:paramtypes", [PublicGtfsDepartureRepository_1.PublicGtfsDepartureRepository,
        data_access_1.DeparturesRepository,
        TransferDepartureTransformation_1.TransferDepartureTransformation,
        PublicVehiclePositionsRepository_1.PublicVehiclePositionsRepository,
        PublicStopTimeRepository_1.PublicStopTimeRepository,
        DelayComputationRepository_1.DelayComputationRepository])
], TransferFacade);
//# sourceMappingURL=TransferFacade.js.map