"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.UpdateDelayTask = void 0;
const RouteTypeEnums_1 = require("../../../../../helpers/RouteTypeEnums");
const RopidGTFSStopTimesModel_1 = require("../../../../ropid-gtfs/RopidGTFSStopTimesModel");
const RopidGTFSTripsModel_1 = require("../../../../ropid-gtfs/RopidGTFSTripsModel");
const cache_1 = require("../../../../ropid-gtfs/data-access/cache");
const Di_1 = require("../../../../ropid-gtfs/ioc/Di");
const RopidGtfsContainerToken_1 = require("../../../../ropid-gtfs/ioc/RopidGtfsContainerToken");
const Di_2 = require("../../../ioc/Di");
const VPContainerToken_1 = require("../../../ioc/VPContainerToken");
const CoreToken_1 = require("@golemio/core/dist/helpers/ioc/CoreToken");
const helpers_1 = require("@golemio/core/dist/integration-engine/helpers");
const queueprocessors_1 = require("@golemio/core/dist/integration-engine/queueprocessors");
const workers_1 = require("@golemio/core/dist/integration-engine/workers");
const golemio_errors_1 = require("@golemio/core/dist/shared/golemio-errors");
const PositionsRepository_1 = require("../data-access/PositionsRepository");
const TripsRepository_1 = require("../data-access/TripsRepository");
const DelayComputationManager_1 = require("../helpers/DelayComputationManager");
const TripStopsManager_1 = require("../helpers/TripStopsManager");
const VPUtils_1 = require("../helpers/VPUtils");
const UpdateDelaySchema_1 = require("../schema/UpdateDelaySchema");
const UpdateDelayLimiter_1 = require("./helpers/UpdateDelayLimiter");
class UpdateDelayTask extends workers_1.AbstractTask {
    constructor(queuePrefix) {
        super(queuePrefix);
        this.queueName = "updateDelay";
        this.queueTtl = 50 * 1000; // 50 seconds
        this.schema = UpdateDelaySchema_1.UpdateDelayValidationSchema;
        this.enrichTripWithGtfsComputationData = async (trip) => {
            try {
                let gtfsComputationData = await this.delayComputationManager.getComputationObject(trip.gtfs_trip_id);
                if (!gtfsComputationData) {
                    return trip;
                }
                if (trip.gtfs_route_type === RouteTypeEnums_1.GTFSRouteTypeEnum.METRO) {
                    gtfsComputationData = await this.metroShapePointsFixer.fix(gtfsComputationData);
                }
                trip.gtfsData = gtfsComputationData;
            }
            catch (err) {
                if (err instanceof golemio_errors_1.GeneralError) {
                    this.logger.warn(err);
                }
                else {
                    this.logger.error(new golemio_errors_1.GeneralError("Error while getting delay computation data for trip " + trip.gtfs_trip_id, this.constructor.name, err));
                }
            }
            return trip;
        };
        /**
         * Propagate known position properties from trips positions to processed positions, when the processed position property is
         * `undefined`
         *
         * @param tripsPositions Trips positions to propagate properties from
         * @param processedPositions Processed positions to propagate properties to
         */
        this.propagateKnownPositionProperties = (tripPositions, processedPositions) => {
            for (let i = 0; i < processedPositions.positions.length; i++) {
                if (processedPositions.positions[i] === null)
                    continue;
                processedPositions.positions[i] = {
                    ...tripPositions.positions[i],
                    ...processedPositions.positions[i],
                };
            }
        };
        /**
         * Take all newly computed positions and save it, then save the last ones to the trips
         *
         * @param processedPositions - Arrays of all newly computed positions
         */
        this.updateComputedPositionsAndTrips = async (processedPositions) => {
            let positions = [];
            let trips = new Map();
            for (const processedPosition of processedPositions) {
                positions = positions.concat(processedPosition.positions.filter((position) => position !== null));
                trips.set(processedPosition.context.tripId, {
                    id: processedPosition.context.tripId,
                    last_position_id: processedPosition.context.lastPositionId ?? null,
                    last_position_context: processedPosition.context,
                    is_canceled: processedPosition.context.lastPositionCanceled,
                });
            }
            await this.positionsRepository.bulkSave(positions);
            await this.tripsRepository.bulkSave(Array.from(trips.values()));
        };
        this.logger = Di_2.VPContainer.resolve(CoreToken_1.CoreToken.Logger);
        this.tripsRepository = new TripsRepository_1.TripsRepository();
        this.positionsRepository = new PositionsRepository_1.PositionsRepository();
        this.delayComputationRedisRepository = new cache_1.DelayComputationRedisRepository();
        const gtfsStopTimesRepository = new RopidGTFSStopTimesModel_1.RopidGTFSStopTimesModel();
        const gtfsTripsRepository = new RopidGTFSTripsModel_1.RopidGTFSTripsModel();
        const blockStopsRedisRepository = Di_1.RopidGtfsContainer.resolve(RopidGtfsContainerToken_1.RopidGtfsContainerToken.BlockStopsRedisRepository);
        this.tripStopsManager = new TripStopsManager_1.TripStopsManager(gtfsStopTimesRepository, blockStopsRedisRepository);
        this.delayComputationManager = new DelayComputationManager_1.DelayComputationManager(gtfsTripsRepository);
        this.metroShapePointsFixer = Di_2.VPContainer.resolve(VPContainerToken_1.VPContainerToken.MetroShapePointsFixer);
        this.updateDelayLimiter = new UpdateDelayLimiter_1.UpdateDelayLimiter("UpdateDelayTask", this.queueTtl);
        this.positionsManager = Di_2.VPContainer.resolve(VPContainerToken_1.VPContainerToken.PositionsManager);
    }
    async execute(data, messageProperties) {
        const tripIds = data.updatedTrips.map((trip) => trip.id);
        if (tripIds.length === 0) {
            return;
        }
        if (data.positions.length > 0) {
            await this.savePositions(data);
        }
        if (!this.updateDelayLimiter.isActualMessage(messageProperties)) {
            return; // skip processing of old messages
        }
        let gtfsComputationTrips = [];
        let tripsPositionsWithGtfsData = [];
        try {
            const tripsPositionsToUpdate = await this.positionsRepository.getPositionsForUpdateDelay(tripIds);
            gtfsComputationTrips =
                await this.delayComputationRedisRepository.getTripPropertiesBatch(tripsPositionsToUpdate.map((trip) => trip.gtfs_trip_id), ["shapes_anchor_points", "stop_times"]);
            for (let i = 0; i < tripsPositionsToUpdate.length; i++) {
                const trip = tripsPositionsToUpdate[i];
                const gtfsData = gtfsComputationTrips[i];
                tripsPositionsWithGtfsData.push({
                    ...trip,
                    gtfsData,
                });
            }
            const tripsWithNewlyCreatedCache = [];
            const tripsWithExistingCache = [];
            const processedPositions = await Promise.all(tripsPositionsWithGtfsData.map(async (trip) => {
                if (trip.gtfsData === null) {
                    trip = await this.enrichTripWithGtfsComputationData(trip);
                    if (trip.gtfsData === null) {
                        return null;
                    }
                    tripsWithNewlyCreatedCache.push(trip);
                }
                else {
                    tripsWithExistingCache.push(trip);
                }
                const computedPositions = this.positionsManager.computePositions(trip, data.schedule);
                this.propagateKnownPositionProperties(trip, computedPositions);
                return computedPositions;
            }));
            const validProcessedPositions = processedPositions.filter((e) => !!e);
            await this.updateComputedPositionsAndTrips(validProcessedPositions);
            await Promise.all([
                this.invalidateBacktrackedPositions(validProcessedPositions),
                this.delayComputationRedisRepository.cacheTripDataBatch(tripsWithNewlyCreatedCache, validProcessedPositions),
                this.expireDelayComputationCache(tripsWithExistingCache, validProcessedPositions),
            ]);
            await queueprocessors_1.QueueManager.sendMessageToExchange(this.queuePrefix, "propagateDelay", {
                processedPositions: validProcessedPositions,
                trips: data.updatedTrips,
            });
        }
        catch (err) {
            if (err instanceof golemio_errors_1.AbstractGolemioError) {
                throw err;
            }
            throw new golemio_errors_1.GeneralError(`Error while updating delay. For trip ids:${tripIds.slice(0, 10).join(",")}`, this.constructor.name, err);
        }
    }
    /**
     * Expire delay computation cache for given trips
     */
    async expireDelayComputationCache(tripWithExistingCache, positions) {
        const tripIdsWithTtl = [];
        for (const trip of tripWithExistingCache) {
            const expirationTtl = this.delayComputationRedisRepository.calculateExpirationTtl(positions, trip.id);
            if (trip.gtfsData) {
                tripIdsWithTtl.push({
                    tripId: trip.gtfs_trip_id,
                    expirationTtl,
                });
            }
        }
        try {
            await this.delayComputationRedisRepository.expirePxat(tripIdsWithTtl);
        }
        catch (err) {
            this.logger.error(err);
        }
    }
    async savePositions(data) {
        // unique ids in input data in key-value (id-positions) structure
        let tripIdToPositionDict = {};
        for (const position of data.positions) {
            tripIdToPositionDict[position.trips_id] = position;
        }
        // unique block_ids and its parent input trips_id in key-value structure
        let tripsIdsByBlockIds = {};
        // unique block_ids and its parent gtfs_trip_id in key-value structure
        let blockIdToTripIdsDict = {};
        for (const trip of data.updatedTrips) {
            if (!trip.gtfs_block_id) {
                continue;
            }
            if (!tripsIdsByBlockIds[trip.gtfs_block_id]) {
                tripsIdsByBlockIds[trip.gtfs_block_id] = trip.id;
            }
            else {
                // save only the shorter trip_id (it is the raw input id with no suffixes)
                tripsIdsByBlockIds[trip.gtfs_block_id] =
                    tripsIdsByBlockIds[trip.gtfs_block_id] > trip.id ? trip.id : tripsIdsByBlockIds[trip.gtfs_block_id];
            }
            if (!blockIdToTripIdsDict[trip.gtfs_block_id]) {
                blockIdToTripIdsDict[trip.gtfs_block_id] = new Set();
            }
            if (trip.gtfs_trip_id) {
                blockIdToTripIdsDict[trip.gtfs_block_id].add(trip.gtfs_trip_id);
            }
        }
        // Get cached schedule for each unique block_id
        const blockIdToTripStopScheduleDict = await Object.entries(blockIdToTripIdsDict).reduce(async (acc, [blockId, gtfsTripIds]) => {
            const tripStops = await acc;
            try {
                const schedule = await this.tripStopsManager.setAndReturnTripStopsSchedule(blockId, [...gtfsTripIds]);
                tripStops[blockId] = schedule;
            }
            catch (err) {
                helpers_1.IntegrationErrorHandler.handle(err);
            }
            return acc;
        }, Promise.resolve({}));
        // duplicate positions for trips with block_ids
        for (const trip of data.updatedTrips) {
            if (tripIdToPositionDict[trip.id]) {
                if (trip.gtfs_block_id) {
                    // Determine current position tracking status
                    tripIdToPositionDict[trip.id].is_tracked = VPUtils_1.VPUtils.determineTrackingStatus(trip.gtfs_trip_id, blockIdToTripStopScheduleDict[trip.gtfs_block_id], tripIdToPositionDict[trip.id]);
                }
                continue;
            }
            const currentPosition = data.positions.find((position) => {
                return position.trips_id === tripsIdsByBlockIds[trip.gtfs_block_id];
            });
            if (!currentPosition) {
                continue;
            }
            // Determine position duplication and new position tracking status
            const newPositionTrackingStatus = VPUtils_1.VPUtils.determineNewPositionTracking(blockIdToTripStopScheduleDict[trip.gtfs_block_id], trip, currentPosition);
            // duplicate position with new created trip for second trip
            const newPosition = { ...currentPosition };
            newPosition.trips_id = trip.id;
            newPosition.is_tracked = newPositionTrackingStatus;
            data.positions.push(newPosition);
        }
        // save all new positions
        await this.positionsRepository.bulkSave(data.positions);
    }
    /**
     * Invalidate positions after the last stop sequence of backtracked positions
     * We do this to improve historical data quality
     * as well as to prevent serving of incorrect stop times to API clients
     * (vehicle is expected to repeat some part of the trip)
     */
    async invalidateBacktrackedPositions(processedPositions) {
        const tripWithStopSequenceTuples = processedPositions
            .filter(({ isBacktrackingDetected }) => !!isBacktrackingDetected)
            .map(({ context, positions }) => {
            const tripId = context.tripId;
            const maxStopSequence = this.getMaxStopSequence(positions);
            return [tripId, maxStopSequence];
        });
        if (tripWithStopSequenceTuples.length === 0) {
            return;
        }
        const count = await this.positionsRepository.invalidatePositionsAfterStopSequence(tripWithStopSequenceTuples);
        this.logger.info(`Invalidated ${count} backtracked positions`);
    }
    getMaxStopSequence(positions) {
        const maxStopSequence = Math.max(...positions.map((position) => position?.last_stop_sequence ?? 0));
        if (maxStopSequence === 0) {
            throw new golemio_errors_1.GeneralError("Max stop sequence must be greater than 0", this.constructor.name);
        }
        return maxStopSequence;
    }
}
exports.UpdateDelayTask = UpdateDelayTask;
//# sourceMappingURL=UpdateDelayTask.js.map