import StatusCodes from 'http-status-codes'
import { Request, Response, Router } from 'express'
import { ApiError, notFoundError, paramMissingError } from '@shared/responseTypes'
import BikePointDao from '@daos/BikePoint.ts/BikePointDao'
import { IBikePointActivityMap, IBikePointDetails } from '@entities/BikePointDetails'
import { bikeTripsCollectionName, dbClient, dbName } from '@server'

const router = Router()
const bikePointDao = new BikePointDao()
const {NOT_FOUND, OK} = StatusCodes

interface QueryParams {
    from: string
    to: string
    day: string
}

interface PathParam {
    bikePointId: string
}

interface IBikePointDetailsResponse {
    bikePointDetails: IBikePointDetails
}


/******************************************************************************
 *         Get Details about single Bike Point by id - "GET /api/bike-point-details/:bikePointId" /api/bike-point-details/311?from=1420329660&to=1421538420&selectedDay=0
 ******************************************************************************/


router.get('/:bikePointId', async (req: Request<PathParam, any, any, QueryParams>, res: Response<IBikePointDetailsResponse | ApiError>) => {
    const bikePointId = req.params.bikePointId
    const from = Number(req.query.from)
    const to = Number(req.query.to)
    /*0=Monday, 1=Tuesday, etc...*/
    const selectedDay = Number(req.query.day)

    if (!bikePointId || (selectedDay === undefined) || !from || !to) {
        return res.status(StatusCodes.BAD_REQUEST).json({error: paramMissingError})
    }

    const bikePoint = await bikePointDao.getById(bikePointId)

    if (!bikePoint) {
        return res.status(NOT_FOUND).json({error: notFoundError})
    }

    const [rentalsAtHoursOfDay, returnsAtHoursOfDay] = await Promise.all([
        dbClient.db(dbName).collection(bikeTripsCollectionName).aggregate<{ _id: number, count: number }>([
            // check if trip was started at the desired bikepoint
            {$match: {startStationId: {$eq: bikePointId}}},
            // check if trip was started within the time range
            {$match: {startDate: {$gte: from, $lt: to}}},
            // convert unixtimestaamp to mongoDB-Date
            {$set: {startDate: {$toDate: {$multiply: ['$startDate', 1000]}}}},
            {$set: {dayofWeek: {$dayOfWeek: '$startDate'}}},
            //check if trip was started at desired day (e.g. Monday)
            {$match: {dayofWeek: {$eq: selectedDay + 1}}},
            //group rentals by hour of day (0-23)
            {
                $group: {
                    _id: {$hour: {date: '$startDate'}},
                    count: {$sum: 1}
                }
            },
            {$sort: {_id: 1}},
        ]).toArray(),
        dbClient.db(dbName).collection(bikeTripsCollectionName).aggregate<{ _id: number, count: number }>([
            // check if trip was ended at the desired bikepoint
            {$match: {endStationId: {$eq: bikePointId}}},
            // check if trip was started within the time range
            {$match: {endDate: {$gte: from, $lt: to}}},
            // convert unixtimestaamp to mongoDB-Date
            {$set: {endDate: {$toDate: {$multiply: ['$endDate', 1000]}}}},
            {$set: {dayofWeek: {$dayOfWeek: '$endDate'}}},
            //check if trip was ended at desired day (e.g. Monday)
            {$match: {dayofWeek: {$eq: selectedDay + 1}}},
            //group returns by hour of day (0-23)
            {
                $group: {
                    _id: {$hour: {date: '$endDate'}},
                    count: {$sum: 1}
                }
            },
            {$sort: {_id: 1}},
        ]).toArray()
    ])

    const countOfSelectedDay = countCertainDay(selectedDay, new Date(from * 1000), new Date(to * 1000))

    const combinedData: IBikePointActivityMap = {}
    for (let i = 0; i < 24; i++) {
        const nbRentals = (rentalsAtHoursOfDay.find(entry => entry._id === i)?.count ?? 0)
        const nbReturns = (returnsAtHoursOfDay.find(entry => entry._id === i)?.count ?? 0)
        combinedData[i] = {
            avgNbRentals: nbRentals / countOfSelectedDay,
            avgNbReturns: nbReturns / countOfSelectedDay,
            avgNbTotal: (nbRentals + nbReturns) / countOfSelectedDay
        }
    }

    const bikePointDetails: IBikePointDetails = {
        commonName: bikePoint.commonName,
        id: bikePoint.id,
        installDate: Number(bikePoint.additionalProperties.find(additionalProperty => additionalProperty.key === 'InstallDate')?.value) / 1000,
        nbDocks: Number(bikePoint.additionalProperties.find(additionalProperty => additionalProperty.key === 'NbDocks')?.value),
        diagrammData: combinedData
    }

    return res.status(OK).json({bikePointDetails})
})

// count how often a certain day of week appears in given time range, where day can be 0 (Monday) up to 6 (Sunday)
function countCertainDay(day: number, startDate: Date, endDate: Date) {
    const numberOfDays = 1 + Math.round((endDate.getTime() - startDate.getTime()) / (24 * 3600 * 1000))
    return Math.floor((numberOfDays + (startDate.getDay() + 6 - day) % 7) / 7)
}


/******************************************************************************
 *                                 Export
 ******************************************************************************/

export default router
