<?php
/********************************************************************************
 * (c)1337 aheadware.com - All rights reserved
 ********************************************************************************/


class TB_Server_Control_Appointment {

    const ATTENDANCE_STATUS_TIME_LIMIT = 0; //86400; // 24h in Sekunden

    /**
     * @param TB_Server_Core_RequestData $params
     *
     * @return TB_Server_Core_Response
     */
    public static function create( TB_Server_Core_RequestData $params )
    {
        $resp = new TB_Server_Core_Response();

        // start: Security check

        $forTeamId = $params->get( 'teamId' );
        $team = TB_Shared_Ent_TeamData_Team::get( $forTeamId );
        if ( is_null( $forTeamId ) || is_null( $team ) )
        {
            throw new \Exception( 'No team id specified.' );
        }

        $sessionProfile = TB_Server_Core_Session::get()->getProfile();
        if ( false === $sessionProfile->isAdminOfTeam( $forTeamId ) )
        {
            throw new \Exception( 'Profile is not allowed to create Appointment.' );
        }

        // stop: Security check

        // start: Validity check

        $startDt = DateTime::createFromFormat( 'U', (int)$params->get( 'startTime' ) );
        $endDt = DateTime::createFromFormat( 'U', (int)$params->get( 'endTime' ) );
        $deadlineDt = DateTime::createFromFormat( 'U', (int)$params->get( 'deadlineTime' ) );
        $deadlineRejectDt = DateTime::createFromFormat( 'U', (int)$params->get( 'deadlineRejectTime' ) );

        if ( $startDt > $endDt )
        {
            throw new \Exception( 'StartDt cannot be past EndDt' );
        }

        if ( $deadlineDt > $startDt )
        {
            throw new \Exception( 'DeadlintDt is past StartDt ... doesnt make sense.' );
        }

        $location = _xss( $params->get( 'location' ) );
        if ( 0 === mb_strlen( $location ) )
        {
            $location = NULL;
        }

        $minAttendees = ( (int)$params->get( 'minAttendee') === 0 ) ? NULL : (int)$params->get( 'minAttendee');
        $maxAttendees = ( (int)$params->get( 'maxAttendee') === 0 ) ? NULL : (int)$params->get( 'maxAttendee');

        $comment = _xss( $params->get( 'comment' ) );

        if ( 0 === mb_strlen( $comment ) )
        {
            $comment = NULL;
        }

        // Link replacements
        if ( 0 === mb_strlen( $comment ) )
        {
            $comment = NULL;
        } else {
            $comment = TB_Server_Utils_Helper::makeUrltoLink($comment);
        }

        $visibility = $params->get( 'visibility', TB_Shared_Ent_TeamData_Appointment::VISIBLE_ALL );

        if ( $visibility !== TB_Shared_Ent_TeamData_Appointment::VISIBLE_ALL && $visibility !== TB_Shared_Ent_TeamData_Appointment::VISIBLE_CATEGORIES )
        {
            throw new \Exception( 'Invalid value for bookable: ' . $visibility );
        }

        $categoryIds = _xss( $params->get( 'categoryIds' ) );

        if ( 0 === count( $categoryIds ) )
        {
            throw new \Exception( 'No category ids set.' );
        }

        foreach( $categoryIds as $cid )
        {
            if ( is_null( $team->getCourseCategoryDataById( $cid ) ) )
            {
                throw new \Exception( 'Invalid categoryId for team: ' . $cid );
            }

            if ( $visibility !== TB_Shared_Ent_TeamData_Appointment::VISIBLE_ALL && false === $sessionProfile->isOwnerOfTeam( $team->id ) )
            {
                // Check if admin has categories
                if ( false === $sessionProfile->hasCategoryId( $cid ) )
                {
                    throw new \Exception( 'Session profile not allowed to create appointment with given category id: ' . $cid );
                }
            }

        }
        // stop: Validity check

        $appointmentState = $params->get( 'appointmentState', TB_Shared_Ent_TeamData_Appointment::STATE_DRAFT );


        $priceFactor = $params->get( 'priceFactor' );
        if ( NULL === $priceFactor || $priceFactor < 0 )
        {
            throw new \Exception( 'Price factor invalid.' );
        }

        // start: Save appointment

        $appointment = new TB_Shared_Ent_TeamData_Appointment();
        $appointment->team_id = $forTeamId;
        $appointment->category_ids_js = $categoryIds;
        $appointment->visibility = $visibility;
        $appointment->subject = _xss( $params->get( 'subject' ) );
        $appointment->start_dt = $startDt;
        $appointment->end_dt = $endDt;
        $appointment->deadline_dt = $deadlineDt;
        $appointment->deadline_reject_dt = $deadlineRejectDt;
        $appointment->min_attendees = $minAttendees;
        $appointment->max_attendees = $maxAttendees;
        $appointment->location = $location;
        $appointment->comment = $comment;
        $appointment->state = $appointmentState;
        $appointment->price_factor = $priceFactor;
        $appointment->save();

        // stop: Save appointment

        // start: Add job
        if ( $appointmentState === TB_Shared_Ent_TeamData_Appointment::STATE_OPEN )
        {
            TB_Server_Core_JobManager::createForAppointment( $appointment );
        }
        // stop: Add Job

        // start : Create serial appointments
        if ( $params->get( 'frequency' ) === 'frequency-multiple' )
        {
            $freqCountNumRepetition = (int)$params->get( 'frequencyNumRepetition' );
            if ( $freqCountNumRepetition > 0 )
            {
                // Set serial id to root appointment
                // This is used to make later processing easier
                $appointment->serial_id = $appointment->id;
                $appointment->save();

                // Prepare appointment entity
                $appointment->serial_id = $appointment->id;
                $frequencyUnit = $params->get( 'frequencyUnit' );

                for ( $i = 1; $i <= $freqCountNumRepetition; $i++ )
                {
                    switch ( $frequencyUnit )
                    {
                        case 'daily':
                            $interval = new DateInterval( 'P1D' );
                        break;

                        case 'weekly':
                            $interval = new DateInterval( 'P7D' );
                        break;

                        case 'twoweek':
                            $interval = new DateInterval( 'P2W' );
                        break;

                        case 'monthly':
                            $interval = new DateInterval( 'P1M' );
                        break;

                        case 'yearly':
                            $interval = new DateInterval( 'P1Y' );
                        break;

                        default:
                            $interval = NULL;
                    }

                    if ( is_null( $interval ) )
                    {
                        break;
                    }

                    $systemDtz = new DateTimeZone( 'UTC' );
                    $defaultDtz = new DateTimeZone( 'Europe/Berlin' );

                    $startDt->setTimezone( $defaultDtz );
                    $endDt->setTimezone( $defaultDtz );
                    $deadlineDt->setTimezone( $defaultDtz );
                    $deadlineRejectDt->setTimezone( $defaultDtz );

                    $startDt->add( $interval );
                    $endDt->add( $interval );
                    $deadlineDt->add( $interval );
                    $deadlineRejectDt->add( $interval );

                    $startDt->setTimezone( $systemDtz );
                    $endDt->setTimezone( $systemDtz );
                    $deadlineDt->setTimezone( $systemDtz );
                    $deadlineRejectDt->setTimezone( $systemDtz );

                    $appointment->id = NULL;
                    $appointment->start_dt = $startDt;
                    $appointment->end_dt = $endDt;
                    $appointment->deadline_dt = $deadlineDt;
                    $appointment->deadline_reject_dt = $deadlineRejectDt;

                    $appointment->save();

                    // start: Add job
                    if ( $appointmentState === TB_Shared_Ent_TeamData_Appointment::STATE_OPEN )
                    {

                        TB_Server_Core_JobManager::createForAppointment( $appointment );
                    }
                    // stop: Add Job
                }
            }

        }
        // stop : Create serial appointments

        // Note:
        // Appointment needs to be reloaded at this time. Strangely working with timestamps does not work
        // otherwise (setting new DateTime).
        $appointment = TB_Shared_Ent_TeamData_Appointment::get( $appointment->id );

        if ( $appointmentState === TB_Shared_Ent_TeamData_Appointment::STATE_OPEN )
        {
            TB_Server_Core_Notification::notifyNewAppointment( $appointment, ( $params->get( 'frequency' ) === 'frequency-multiple' ) );
        }

        $resp->addData( 'appointment', $appointment );

        return $resp;
    }

    public static function getAutocompleteData( TB_Server_Core_RequestData $params )
    {
        $resp = new TB_Server_Core_Response();

        $sessionProfile = TB_Server_Core_Session::get()->getProfile();
        foreach( $sessionProfile->teams_js as $td )
        {
            if ( true === $sessionProfile->isAdminOfTeam( $td[ 'team_id' ] ) )
            {
                $resp->addData( 'subjects_' . $td[ 'team_id' ], TB_Shared_Ent_TeamData_Appointment::getSubjectsForAutocompletion( $td [ 'team_id' ] ) );
                $resp->addData( 'locations_' . $td[ 'team_id' ], TB_Shared_Ent_TeamData_Appointment::getLocationsForAutocompletion( $td [ 'team_id' ] ) );
            }
        }

        return $resp;

    }

    /**
     * @param TB_Server_Core_RequestData $params
     * @return TB_Server_Core_Response
     */
    public static function publish( TB_Server_Core_RequestData $params )
    {
        $resp = new TB_Server_Core_Response();

        $db = TB_Shared_Db_TeamData::get();

        try
        {
            $db->beginTransaction();

            // start: Security check

            $appointment = TB_Shared_Ent_TeamData_Appointment::get( $params->get( 'appointmentId' ) );
            if ( is_null( $appointment ) ) {
                throw new Exception( ( 'Appointment to update not found.' ) );
            }

            $forTeamId = $appointment->team_id;
            if ( is_null( $forTeamId ) ) {
                throw new \Exception( 'No team id specified.' );
            }

            $sessionProfile = TB_Server_Core_Session::get()->getProfile();
            if ( false === $sessionProfile->isAdminOfTeam( $appointment->team_id ) ) {
                throw new \Exception( 'Profile is not allowed to update Appointment.' );
            }

            $serialPublishing = $params->get( 'serialPublishing', false );

            // stop: Security check

            if ( true === $serialPublishing )
            {
                $apps = TB_Shared_Ent_TeamData_Appointment::getSerials( $appointment->serial_id );
                /** @var TB_Shared_Ent_TeamData_Appointment $app */
                foreach( $apps as $app )
                {
                    if ( $app->isOver() || false === $app->isDraft() )
                    {
                        continue;
                    }

                    $app->state = TB_Shared_Ent_TeamData_Appointment::STATE_OPEN;
                    $app->save();

                    TB_Server_Core_JobManager::createForAppointment( $app );
                }
                TB_Server_Core_Notification::notifyNewAppointment( $appointment, true );
            }
            else
            {
                $appointment->state = TB_Shared_Ent_TeamData_Appointment::STATE_OPEN;
                $appointment->save();

                TB_Server_Core_JobManager::createForAppointment( $appointment );
                TB_Server_Core_Notification::notifyNewAppointment( $appointment, false );
            }

            // start: Add Job

            $db->commit();
        }
        catch ( Exception $e )
        {
            $db->rollBack();
        }

        $resp->addData( 'appointment', $appointment );

        return $resp;
    }

    /**
     * Updates existing appointment
     *
     * @param TB_Server_Core_RequestData $params
     * @return TB_Server_Core_Response
     * @throws Exception
     */
    public static function update( TB_Server_Core_RequestData $params )
    {
        $resp = new TB_Server_Core_Response();

        $db = TB_Shared_Db_TeamData::get();

        try
        {
            $db->beginTransaction();

            // start: Security check

            $appointment = TB_Shared_Ent_TeamData_Appointment::get( $params->get( 'appointmentId' ) );
            if ( is_null( $appointment ) )
            {
                throw new Exception(( 'Appointment to update not found.' ) );
            }

            $forTeamId = $appointment->team_id;
            $team = TB_Shared_Ent_TeamData_Team::get( $forTeamId );
            if ( is_null( $forTeamId ) || is_null( $team ) )
            {
                throw new \Exception( 'No team id specified.' );
            }

            $sessionProfile = TB_Server_Core_Session::get()->getProfile();
            if ( false === $sessionProfile->isAdminOfTeam( $appointment->team_id ) )
            {
                throw new \Exception( 'Profile is not allowed to update Appointment.' );
            }

            // stop: Security check

            // start: Validity check

            $startDt = DateTime::createFromFormat( 'U', (int)$params->get( 'startTime' ), new \DateTimeZone( 'UTC' ) );
            $endDt = DateTime::createFromFormat( 'U', (int)$params->get( 'endTime' ), new \DateTimeZone( 'UTC' ) );
            $deadlineDt = DateTime::createFromFormat( 'U', (int)$params->get( 'deadlineTime' ), new \DateTimeZone( 'UTC' ) );
            $deadlineRejectDt = DateTime::createFromFormat( 'U', (int)$params->get( 'deadlineRejectTime' ), new \DateTimeZone( 'UTC' ) );

            if ( $startDt > $endDt )
            {
                throw new \Exception( 'StartDt cannot be past EndDt' );
            }

            if ( $deadlineDt > $startDt )
            {
                throw new \Exception( 'DeadlintDt is past StartDt ... doesnt make sense.' );
            }

            $location = _xss( $params->get( 'location' ) );
            if ( 0 === mb_strlen( $location ) )
            {
                $location = NULL;
            }

            $minAttendees = ( (int)$params->get( 'minAttendee') === 0 ) ? NULL : (int)$params->get( 'minAttendee');
            $maxAttendees = ( (int)$params->get( 'maxAttendee') === 0 ) ? NULL : (int)$params->get( 'maxAttendee');

            $comment = _xss( $params->get( 'comment' ) );

            if ( 0 === mb_strlen( $comment ) )
            {
                $comment = NULL;
            }

            // Link replacements
            if ( 0 === mb_strlen( $comment ) )
            {
                $comment = NULL;
            } else {
                $comment = TB_Server_Utils_Helper::makeUrltoLink($comment);
            }

            $visibility = $params->get( 'visibility', TB_Shared_Ent_TeamData_Appointment::VISIBLE_ALL );

            if ( $visibility !== TB_Shared_Ent_TeamData_Appointment::VISIBLE_ALL && $visibility !== TB_Shared_Ent_TeamData_Appointment::VISIBLE_CATEGORIES )
            {
                throw new \Exception( 'Invalid value for visibility: ' . $visibility );
            }

            $categoryIds = _xss( $params->get( 'categoryIds' ) );

            if ( 0 === count( $categoryIds ) )
            {
                throw new \Exception( 'No category ids set.' );
            }

            foreach( $categoryIds as $cid )
            {
                if ( is_null( $team->getCourseCategoryDataById( $cid ) ) )
                {
                    throw new \Exception( 'Invalid categoryId for team: ' . $cid );
                }

                if ( $visibility !== TB_Shared_Ent_TeamData_Appointment::VISIBLE_ALL && false === $sessionProfile->isOwnerOfTeam( $team->id ) )
                {
                    // Check if admin has categories
                    if ( false === $sessionProfile->hasCategoryId( $cid ) )
                    {
                        throw new \Exception( 'Session profile not allowed to create appointment with given category id: ' . $cid );
                    }
                }

            }

            $priceFactor = $params->get( 'priceFactor' );
            if ( NULL === $priceFactor || $priceFactor < 0 )
            {
                throw new \Exception( 'Invalid price factor.' );
            }

            // stop: Validity check

            // start: Save appointment

            $processSerial = $params->get( 'processSerial', false );

            $diffStartDt = $appointment->start_dt->diff( $startDt );
            $diffEndDt = $appointment->end_dt->diff( $endDt );
            $diffDeadlineDt = $appointment->deadline_dt->diff( $deadlineDt );
            $diffDealineRejectDt = $appointment->deadline_reject_dt->diff( $deadlineRejectDt );

            $appointmentIdsToProcess = array();

            if ( $appointment->isSerial() && true === $processSerial )
            {
                $upApps = TB_Shared_Ent_TeamData_Appointment::getUpcomingBySerialId( $appointment->serial_id );
                /** @var TB_Shared_Ent_TeamData_Appointment $upApp */
                foreach( $upApps as $upApp )
                {
                    array_push( $appointmentIdsToProcess, $upApp->id );
                }
            }
            else
            {
                array_push( $appointmentIdsToProcess, $appointment->id );
            }

            $systemDtz = new DateTimeZone( 'UTC' );
            $defaultDtz = new DateTimeZone( 'Europe/Berlin' );

            foreach( $appointmentIdsToProcess as $appointmentId )
            {
                $appointment = TB_Shared_Ent_TeamData_Appointment::get( $appointmentId );

                $appointment->visibility = $visibility;
                $appointment->category_ids_js = $categoryIds;
                $appointment->subject = _xss( $params->get( 'subject' ) );

                $appointment->start_dt->setTimezone( $defaultDtz );
                $appointment->end_dt->setTimezone( $defaultDtz );
                $appointment->deadline_dt->setTimezone( $defaultDtz );
                $appointment->deadline_reject_dt->setTimezone( $defaultDtz );

                $appointment->start_dt->add( $diffStartDt );
                $appointment->end_dt->add( $diffEndDt );
                $appointment->deadline_dt->add( $diffDeadlineDt );
                $appointment->deadline_reject_dt->add( $diffDealineRejectDt );

                $appointment->start_dt->setTimezone( $systemDtz );
                $appointment->end_dt->setTimezone( $systemDtz );
                $appointment->deadline_dt->setTimezone( $systemDtz );
                $appointment->deadline_reject_dt->setTimezone( $systemDtz );

                // Note: Since we modify the dateobject directly, our DB abstraction layer does not know about those changes
                //       That's why we touch each datetime object explicitly to invoke updating those values
                $appointment->touch( 'start_dt' );
                $appointment->touch( 'end_dt' );
                $appointment->touch( 'deadline_dt' );
                $appointment->touch( 'deadline_reject_dt' );

                $appointment->min_attendees = $minAttendees;
                $appointment->max_attendees = $maxAttendees;
                $appointment->location = $location;
                $appointment->comment = $comment;
                $appointment->price_factor = $priceFactor;
                $appointment->save();

                // stop: Save appointment

                // Only process attendees when appointment is already open
                if ( TB_Shared_Ent_TeamData_Appointment::STATE_OPEN === $appointment->state )
                {

                    // @todo Think about notifying profiles if bookable status has been changed
                    // ALL to CATEGORIES
                    // CATEGORIES TO ALL

                    // Should we reset attendee list for this appointment
                    $resetAttendees = $params->get( 'resetAttendeeList');
                    if ( isset( $resetAttendees ) && 'reset' == $resetAttendees ) {

                        // Delete all attendees
                        TB_Shared_Ent_TeamData_Attendee::deleteAllForAppointmentId( $appointment->id );

                    }

                    // start: Inform all attendees about changes

                    $profileIds = array();
                    $attendees = TB_Shared_Ent_TeamData_Attendee::getAttendeesWithStatusForAppointmentId(
                        $appointment->id,
                        TB_Shared_Ent_TeamData_Attendee::STATUS_ACCEPTED
                    );
                    $attendeesWaiting = TB_Shared_Ent_TeamData_Attendee::getAttendeesWithStatusForAppointmentId(
                        $appointment->id,
                        TB_Shared_Ent_TeamData_Attendee::STATUS_WAITING
                    );

                    $attendees = array_merge( $attendees, $attendeesWaiting );

                    /** @var TB_Shared_Ent_TeamData_Attendee $attendee */
                    foreach( $attendees as $attendee )
                    {
                        array_push( $profileIds, $attendee->profile_id );
                    }

                    $notificationMessage = 'Der Termin ' . $appointment->subject . ' wurde aktualisiert.';
                    if ( 'reset' == $resetAttendees )
                    {
                        $notificationMessage .= ' Alle vorherigen Zu- und Absagen wurden gelöscht.';
                    }
                    TB_Server_Core_Notification::sendToProfiles(
                        'Terminänderung',
                        $notificationMessage,
                        $profileIds,
                        TB_Server_Core_Notification::createAdditionalData( '#/appointment/' . $appointment->id ),
                        'appointment-trainerupdate-' . $appointment->id
                    );

                    // stop: Inform all attendees about changes

                    self::processWaitingList( $appointment );

                    // start: Add job
                    TB_Server_Core_JobManager::createForAppointment( $appointment );
                    // stop: Add Job
                }

            }

            $db->commit();
        }
        catch ( Exception $e )
        {
            $db->rollBack();
        }

        $resp->addData( 'appointment', $appointment );

        return $resp;
    }

    public static function batchUpdatePriceFactor( TB_Server_Core_RequestData $params )
    {
        $resp = new TB_Server_Core_Response();
        $updateResult = [];

        $sessionProfile = TB_Server_Core_Session::get()->getProfile();
        $teamId = $params->get( 'teamId' );
        if ( false === $sessionProfile->isAdminOfTeam( $teamId ) )
        {
            throw new \Exception( 'Profile is not allowed to batch update appointsments' );
        }

        $appIds = $params->get( 'appointmentIds' );
        $newPriceFactor = +$params->get( 'priceFactor' );
        if ( $newPriceFactor < 0 )
        {
            throw new \Exception( 'Price factor must be higher than 0' );
        }

        foreach( $appIds as $appId )
        {
            $app = TB_Shared_Ent_TeamData_Appointment::get( $appId );
            if ( null === $app || $app->team_id != $teamId )
            {
                continue;
            }

            $app->price_factor = +$newPriceFactor;
            $app->save();
            array_push( $updateResult, array( 'appointmentId' => $app->id, 'price_factor' => $newPriceFactor ) );
        }

        $resp->addData( 'update_result', $updateResult );
        return $resp;
    }

    /**
     * @param TB_Server_Core_RequestData $params
     * @return TB_Server_Core_Response
     */
    public static function subscribe( TB_Server_Core_RequestData $params ) {
        $resp = new TB_Server_Core_Response();

        $resp->addData( 'team', TB_Shared_Ent_TeamData_Team::get( TB_Server_Core_Session::get()->getTeam()->id ) );

        return $resp;
    }

    /**
     * @param TB_Server_Core_RequestData $params
     * @return TB_Server_Core_Response
     * @throws Exception
     */
    public static function cancel( TB_Server_Core_RequestData $params )
    {
        $resp = new TB_Server_Core_Response();

        $appointment = TB_Shared_Ent_TeamData_Appointment::get( $params->get( 'appointmentId' ) );
        if ( is_null( $appointment ) )
        {
            throw new Exception(( 'Appointment to cancel not found.' ) );
        }
        if ( $appointment->hasStarted() )
        {
            throw new Exception( 'Appointment cannot be cancelled because it has already started.' );
        }
        if ( $appointment->state !== TB_Shared_Ent_TeamData_Appointment::STATE_OPEN )
        {
            return $resp;
        }

        $forTeamId = $appointment->team_id;
        if ( is_null( $forTeamId ) )
        {
            throw new \Exception( 'No team id specified.' );
        }

        $sessionProfile = TB_Server_Core_Session::get()->getProfile();
        if ( false === $sessionProfile->isAdminOfTeam( $appointment->team_id ) )
        {
            throw new \Exception( 'Profile is not allowed to cancel Appointment.' );
        }

        $appointment->state = TB_Shared_Ent_TeamData_Appointment::STATE_CANCELLED;
        $appointment->save();

        // start: Inform all attendees about changes

        $profileIds = array();
        $attendees = TB_Shared_Ent_TeamData_Attendee::getAttendeesWithStatusForAppointmentId(
            $appointment->id,
            TB_Shared_Ent_TeamData_Attendee::STATUS_ACCEPTED
        );
        /** @var TB_Shared_Ent_TeamData_Attendee $attendee */
        foreach( $attendees as $attendee )
        {
            array_push( $profileIds, $attendee->profile_id );
        }

        $attendees = TB_Shared_Ent_TeamData_Attendee::getAttendeesWithStatusForAppointmentId(
            $appointment->id,
            TB_Shared_Ent_TeamData_Attendee::STATUS_WAITING
        );
        /** @var TB_Shared_Ent_TeamData_Attendee $attendee */
        foreach( $attendees as $attendee )
        {
            array_push( $profileIds, $attendee->profile_id );
        }

        TB_Server_Core_Notification::notifyAppointmentCancellation(
            $profileIds,
            $appointment
        );

        // stop: Inform all attendees about changes

        return $resp;
    }

    /**
     * @param TB_Server_Core_RequestData $params
     *
     * @return TB_Server_Core_Response
     */
    public static function delete( TB_Server_Core_RequestData $params )
    {
        $resp = new TB_Server_Core_Response();

        $appointment = TB_Shared_Ent_TeamData_Appointment::get( $params->get( 'appointmentId', 0 ) );
        if ( !isset( $appointment ) )
        {
            throw new Exception( 'Invalid appointment to update.' );
        }

        $sessionProfile = TB_Server_Core_Session::get()->getProfile();
        if ( false === $sessionProfile->isAdminOfTeam( $appointment->team_id ) )
        {
            throw new \Exception( 'Profile is not allowed to cancel Appointment.' );
        }

        $serialDeletion = $params->get( 'serialDeletion', false );

        if ( true === $serialDeletion && $appointment->serial_id )
        {
            $apps = TB_Shared_Ent_TeamData_Appointment::getSerials( $appointment->serial_id );
            /** @var TB_Shared_Ent_TeamData_Appointment $app */
            foreach( $apps as $app )
            {
                if ( $app->isOver() )
                {
                    continue;
                }

                $app->delete();
            }
        }
        else
        {
            // Delete appointment and all its relations
            $appointment->delete();
        }


        return $resp;
    }

    /**
     * @param TB_Server_Core_RequestData $params
     *
     * @return TB_Server_Core_Response
     */
    public static function getReminder( TB_Server_Core_RequestData $params ) {
        $resp = new TB_Server_Core_Response();

        $profile = TB_Server_Core_Session::get()->getProfile();

        /** @var TB_Shared_Ent_TeamData_Appointment $appointment */
        $appointment = TB_Shared_Ent_TeamData_Appointment::get( $params->get( 'appointment_id' ) );
        $attendees = TB_Shared_Ent_TeamData_Attendee::getByAppointmentId( $appointment->id );
        $tpl = new TB_Server_Utils_Template( 'mail/appointmentreminder' );
        $mailTxt = $tpl->get( array(
            'appointment_name' => $appointment->getName(),
            'appointment_date' => TB_Server_Utils_Helper::getDate( $appointment->starttime ),
            'appointment_time' => TB_Server_Utils_Helper::getTime( $appointment->starttime )
        ));
        $resp->addData( 'mailtext', $mailTxt );
        $resp->addData( 'appointment', $appointment );
        $resp->addData( 'attendees', $attendees );

        return $resp;
    }

    /**
     * @param TB_Server_Core_RequestData $params
     */
    public static function sendReminder( TB_Server_Core_RequestData $params ) {
        $resp = new TB_Server_Core_Response();

        $profile = TB_Server_Core_Session::get()->getProfile();
        if ( !$profile->isTrainer() && !$profile->isCoTrainer() ) {
            throw new Exception( 'No rights' );
        }

        /** @var TB_Shared_Ent_TeamData_Appointment $appointment */
        $appointment = TB_Shared_Ent_TeamData_Appointment::get( $params->get( 'appointment_id' ) );
        if ( !isset( $appointment ) || $profile->team_id != $appointment->team_id ) {
            throw new Exception( 'Invalid appointment' );
        }

        $team = TB_Server_Core_Session::get()->getTeam();
        if ( !isset( $team ) ) {
            throw new Exception( 'No team found.' );
        }

        $tpl = new TB_Server_Utils_Template( 'mail/appointmentreminder2' );
        $content = $tpl->get( array(
            'appointment_name' => $appointment->getName(),
            'appointment_date' => TB_Server_Utils_Helper::getDate( $appointment->starttime ),
            'appointment_time' => TB_Server_Utils_Helper::getTime( $appointment->starttime ),
            'team_display_name' => $team->display_name,
            'appointment_url' => 'http://client.tbuddy.de/#/c=Appointment&a=index'
        ));


        $attendees = TB_Shared_Ent_TeamData_Attendee::getByAppointmentId( $appointment->id );

        $resp->addData( 'res', 'ok' );

        return $resp;
    }

    /**
     * @param TB_Server_Core_RequestData $params
     * @return TB_Server_Core_Response
     */
    public static function getDetail( TB_Server_Core_RequestData $params )
    {
        $resp = new TB_Server_Core_Response();

        $sessionProfile = TB_Server_Core_Session::get()->getProfile();

        /** @var TB_Shared_Ent_TeamData_Appointment $appointment */
        $appointment = TB_Shared_Ent_TeamData_Appointment::get( $params->get( 'appointmentId' ) );
        if ( !isset( $appointment ) )
        {
            $resp->addData( 'appointment', null );
            return $resp;
            //throw new Exception( 'Appointment not found: ' . $params->get( 'appointmentId' ) );
        }

        if ( false === $sessionProfile->isInTeam( $appointment->team_id ) )
        {
            throw new \Exception( 'Profile cannot access appointement.' );
        }

        if ( false === $appointment->isVisibleByProfile( $sessionProfile ) )
        {
            throw new \Exception( 'Not visible for current session profile.' );
        }

        //$attendeeData = TB_Shared_Ent_TeamData_Attendee::getWithProfileDataByAppointmentId( $appointment->id );
        $attendeeData = TB_Shared_Ent_TeamData_Attendee::getByAppointmentId( $appointment->id );
        $attendeeProfiles = array();
        /** @var TB_Shared_Ent_TeamData_Attendee $ad */
        foreach( $attendeeData as $ad )
        {
            array_push( $attendeeProfiles, TB_Shared_Ent_TeamData_Profile::get( $ad->profile_id ) );
        }

        $resp->addData( 'appointment', $appointment );
        $resp->addData( 'attendee_data', $attendeeData );
        $resp->addData( 'attendee_profiles', $attendeeProfiles );

        return $resp;
    }

    /**
     * @param TB_Server_Core_RequestData $params
     *
     * @return TB_Server_Core_Response
     */
    public static function getList( TB_Server_Core_RequestData $params )
    {
        $resp = new TB_Server_Core_Response();

        $profile = TB_Server_Core_Session::get()->getProfile();
        $teamIds = $profile->getTeamIds();

        $filter = $params->get( 'filter' );
        $groupId = $params->get( 'groupId' );

        $accessibleAppointments = array();

        if ( isset( $groupId ) )
        {
            if ( !in_array( $groupId, $teamIds ) )
            {
                throw new \Exception( 'GroupId is not in profiles team ids to get list of appointments.' );
            }

            $teamIds = array( $groupId );
        }

        if ( $filter === 'all' )
        {
            $filter = array();
            $filter[ 'previous' ] = true;
            $filter[ 'appointmentCategoryIds' ] = array();
            foreach( $teamIds as $teamId )
            {
                /** @var TB_Shared_Ent_TeamData_Team $filterTeam */
                $filterTeam = TB_Shared_Ent_TeamData_Team::get( $teamId );
                foreach( $filterTeam->course_categories_js as $cc )
                {
                    array_push( $filter[ 'appointmentCategoryIds' ], $cc[ 'id' ] );
                }
            }
        }

        $appointments = TB_Shared_Ent_TeamData_Appointment::getAllForTeamIds( $teamIds, $filter );
        /** @var TB_Shared_Ent_TeamData_Appointment $appointment */
        foreach( $appointments as $appointment )
        {
            if ( $appointment->state === TB_Shared_Ent_TeamData_Appointment::STATE_DRAFT &&
                 !$profile->isAdminOfTeam( $appointment->team_id ) )
            {
                continue;
            }

            if ( false === $appointment->isVisibleByProfile( $profile ) )
            {
                continue;
            }

            array_push( $accessibleAppointments, $appointment );

            // Add attendee information
            $attendees = TB_Shared_Ent_TeamData_Attendee::getByAppointmentId( $appointment->id );
            $resp->addRowsToData( 'attendees_' . $appointment->id, $attendees );
        }

        $resp->addRowsToData( 'appointments', $accessibleAppointments );

        return $resp;
    }

    /**
     * @param TB_Server_Core_RequestData $params
     * @return TB_Server_Core_Response
     */
    public static function getChat( TB_Server_Core_RequestData $params )
    {
        $resp = new TB_Server_Core_Response();

        /** @var TB_Shared_Ent_TeamData_Profile $sessionProfile */
        $sessionProfile = TB_Server_Core_Session::get()->getProfile();

        /** @var TB_Shared_Ent_TeamData_Appointment $appointment */
        $appointment = TB_Shared_Ent_TeamData_Appointment::get( $params->get( 'appointmentId' ) );
        if ( !isset( $appointment ) )
        {
            throw new Exception( 'Appointment not found: ' . $params->get( 'appointmentId' ) );
        }

        if ( false === $sessionProfile->isInTeam( $appointment->team_id ) )
        {
            throw new \Exception( 'Profile cannot access appointement.' );
        }

        if ( false === $appointment->isVisibleByProfile( $sessionProfile ) )
        {
            throw new \Exception( 'Not bookable for current session profile.' );
        }

        $chats = TB_Shared_Ent_TeamData_Chat::getForRefId( 'appointment-' . $appointment->id, $params->get( 'lastId' ) );

        /*
         * DISABLE FOR NOW - THIS CAUSES IRRITATION ON HOW NOTIFICATIONS ARE SEND
         *
        TB_Shared_Ent_TeamData_Notification::markAllAsReadForProfileIdAndRefId(
            TB_Server_Core_Session::get()->getProfileId(),
            'appointment-' . $appointment->id
        );
        */

        $resp->addData( 'chats', $chats );

        return $resp;
    }

    /**
     * @param TB_Server_Core_RequestData $params
     * @return TB_Server_Core_Response
     */
    public static function chat( TB_Server_Core_RequestData $params )
    {
        $resp = new TB_Server_Core_Response();

        $message = $params->get( 'message' );
        if ( 0 === mb_strlen( $message ) )
        {
            throw new \Exception( 'Invalid message length for chat.' );
        }

        /** @var TB_Shared_Ent_TeamData_Appointment $appointment */
        $appointment = TB_Shared_Ent_TeamData_Appointment::get( $params->get( 'appointmentId' ) );
        if ( !isset( $appointment ) )
        {
            throw new Exception( 'Appointment not found: ' . $params->get( 'appointmentId' ) );
        }

        $currentProfile = TB_Server_Core_Session::get()->getProfile();
        if ( $currentProfile->isAnonymousInTeam( $appointment->team_id ) )
        {
            throw new \Exception( 'Invisible profile not allowed to chat.' );
        }

        if ( false === $currentProfile->isInTeam( $appointment->team_id ) )
        {
            throw new \Exception( 'Profile cannot access appointement.' );
        }

        if ( false === $appointment->isVisibleByProfile( $currentProfile ) )
        {
            throw new \Exception( 'Not visible for current session profile.' );
        }

        // Create chat
        $dtNow = TB_Server_Utils_Helper::getUTCNowDateTime();
        $chat = new TB_Shared_Ent_TeamData_Chat();
        $chat->ref_id = 'appointment-' . $appointment->id;
        $chat->creator_profile_id = $currentProfile->id;
        $chat->message = $message;
        $chat->last_updated_dt = $dtNow;
        $chat->created_dt = $dtNow;
        $chat->save();

        TB_Server_Core_Notification::notifyNewAppointmentChat( $appointment );

        $resp->addData( 'chat', $chat );

        return $resp;
    }

    /**
     * Special Trainer action to set attendee state for foreign profile
     *
     * @param TB_Server_Core_RequestData $params
     * @return TB_Server_Core_Response
     * @throws Exception
     */
    public static function setAttendanceStatusByTrainer( TB_Server_Core_RequestData $params )
    {
        $resp = new TB_Server_Core_Response();

        /** @var TB_Shared_Ent_TeamData_Appointment $appointment */
        $appointment = TB_Shared_Ent_TeamData_Appointment::get( $params->get( 'appointmentId' ) );
        if ( is_null( $appointment) )
        {
            throw new Exception( 'Invalid appointment id' );
        }

        $sessionProfile = TB_Server_Core_Session::get()->getProfile();
        if ( false === $sessionProfile->isAdminOfTeam( $appointment->team_id ) )
        {
            throw new \Exception( 'No trainer - no rights to set attendee status.' );
        }

        /** @var TB_Shared_Ent_TeamData_Profile $profileToUpdate */
        $profileToUpdate = TB_Shared_Ent_TeamData_Profile::get( $params->get( 'profileId' ) );
        /*
        if ( false === $profileToUpdate->isInTeam( $appointment->team_id ) )
        {
            throw new Exception( 'Invalid profile id' );
        }
        */

        $processSerial = $params->get( 'processSerial', false );

        // Status
        $status = $params->get( 'status' );
        if ( !in_array( $status, TB_Shared_Ent_TeamData_Attendee::getValidStatus() ) )
        {
            throw new Exception( 'Invalid attendee state.' );
        }

        $appointmentIdsToProcess = array();

        if (  $appointment->isSerial() && true === $processSerial )
        {
            $upApps = TB_Shared_Ent_TeamData_Appointment::getUpcomingBySerialId( $appointment->serial_id );
            /** @var TB_Shared_Ent_TeamData_Appointment $upApp */
            foreach( $upApps as $upApp )
            {
                array_push( $appointmentIdsToProcess, $upApp->id );
            }
        }
        else
        {
            array_push( $appointmentIdsToProcess, $appointment->id );
        }

        foreach( $appointmentIdsToProcess as $appointmentId )
        {
            $appointment = TB_Shared_Ent_TeamData_Appointment::get( $appointmentId );

            // Update / Create attendee entry
            $attendee = TB_Shared_Ent_TeamData_Attendee::getByAppointmentIdAndProfileId(
                $appointment->id,
                $profileToUpdate->id
            );

            if ( is_null( $attendee ) )
            {
                $attendee = new TB_Shared_Ent_TeamData_Attendee();
                $attendee->appointment_id = $appointment->id;
                $attendee->profile_id = $profileToUpdate->id;
            }

            $attendee->set_by_profile_id = $sessionProfile->id;

            // If appointment has not started yet
            if ( $appointment->isDraft() )
            {
                $attendee->status = $status;
                $attendee->has_attended = ( $status === TB_Shared_Ent_TeamData_Attendee::STATUS_ACCEPTED ) ? TB_Shared_Ent_TeamData_Attendee::HAS_ATTENDED : TB_Shared_Ent_TeamData_Attendee::HAS_NOT_ATTENDED;
            }
            else if ( false === $appointment->hasStarted() )
            {
                $oldStatus = $attendee->status;
                $attendee->status = $status;
                $attendee->has_attended = ( $status === TB_Shared_Ent_TeamData_Attendee::STATUS_ACCEPTED ) ? TB_Shared_Ent_TeamData_Attendee::HAS_ATTENDED : TB_Shared_Ent_TeamData_Attendee::HAS_NOT_ATTENDED;

                TB_Server_Core_Notification::notifyAttenddeeStatusChangeByAdmin( $attendee, $appointment );
            }
            else
            {
                $attendee->has_attended = ( $status === TB_Shared_Ent_TeamData_Attendee::STATUS_ACCEPTED ) ? TB_Shared_Ent_TeamData_Attendee::HAS_ATTENDED : TB_Shared_Ent_TeamData_Attendee::HAS_NOT_ATTENDED;
            }

            $attendee->save();

            if ( false === $appointment->isDraft() && false === $appointment->hasStarted() )
            {
                $isLastInBatch  = $params->get( 'isLastInBatch' );
                if ( true == $isLastInBatch )
                {
                    self::processWaitingList( $appointment );
                }
            }
        }

        return $resp;
    }

    /**
     * @param TB_Server_Core_RequestData $params
     * @return TB_Server_Core_Response
     * @throws Exception
     */
    public static function setAttendanceStatus( TB_Server_Core_RequestData $params )
    {
        $resp = new TB_Server_Core_Response();

        $oldStatus = NULL;
        $acceptedAttendees = 0;

        /** @var TB_Shared_Ent_TeamData_Appointment $appointment */
        $appointment = TB_Shared_Ent_TeamData_Appointment::get( $params->get( 'appointmentId' ) );

        if ( $appointment->isDraft() )
        {
            throw new Exception( 'Appointment is in draft mode. Attandance status cannot be set.' );
        }

        $sessionProfile = TB_Server_Core_Session::get()->getProfile();
        if ( false === $sessionProfile->isInTeam( $appointment->team_id ))
        {
            throw new Exception( 'No profile in this team means no right to set attendee status.' );
        }

        if ( false === $appointment->isVisibleByProfile( $sessionProfile ) )
        {
            throw new \Exception( 'Not visible by current profile.' );
        }

        $status = $params->get( 'status' );
        if ( !in_array( $status, TB_Shared_Ent_TeamData_Attendee::getValidStatus() ) )
        {
            throw new Exception( 'Invalid attendee state.' );
        }

        if ( $appointment->isDealineOver() )
        {
            $resp->addData( 'err', 'deadline_over' );
        }
        else if ( $appointment->isDeadlineRejectOver() && $status === TB_Shared_Ent_TeamData_Attendee::STATUS_DECLINED )
        {
            $resp->addData( 'err', 'deadline_reject_over' );
        }
        else if ( $appointment->isCancelled() )
        {
            $resp->addData( 'err', 'appointment_is_cancelled' );
        }
        else
        {
            $isMaxReached = false;
            if ( (int)$appointment->max_attendees > 0 )
            {
                $acceptedAttendees = TB_Shared_Ent_TeamData_Attendee::getAttendeesWithStatusForAppointmentId(
                    $appointment->id,
                    TB_Shared_Ent_TeamData_Attendee::STATUS_ACCEPTED
                );

                if ( count( $acceptedAttendees ) >= $appointment->max_attendees )
                {
                    $isMaxReached = true;
                }
            }

            if ( $status === TB_Shared_Ent_TeamData_Attendee::STATUS_ACCEPTED &&
                 true === $isMaxReached )
            {
                $resp->addData( 'err', 'max_reached' );
            }
            else
            {
                // Update / Create attendee entry
                $attendee = TB_Shared_Ent_TeamData_Attendee::getByAppointmentIdAndProfileId(
                    $appointment->id,
                    $sessionProfile->id
                );
                if ( is_null( $attendee ) )
                {
                    $attendee = new TB_Shared_Ent_TeamData_Attendee();
                    $attendee->appointment_id = $appointment->id;
                    $attendee->profile_id = $sessionProfile->id;
                }
                else
                {
                    $oldStatus = $attendee->status;
                }

                $attendee->set_by_profile_id = $sessionProfile->id;

                $attendee->status = $status;
                $attendee->has_attended = ( $status === TB_Shared_Ent_TeamData_Attendee::STATUS_ACCEPTED ) ? TB_Shared_Ent_TeamData_Attendee::HAS_ATTENDED : TB_Shared_Ent_TeamData_Attendee::HAS_NOT_ATTENDED;
                $attendee->save();

                if ( $isMaxReached &&
                    $oldStatus === TB_Shared_Ent_TeamData_Attendee::STATUS_ACCEPTED &&
                    $status === TB_Shared_Ent_TeamData_Attendee::STATUS_DECLINED &&
                    ( (int)count( $acceptedAttendees ) - 1 ) < (int)$appointment->max_attendees )
                {
                    self::processWaitingList( $appointment );
                }

                // If attendee status changed, return updated appointment
                try
                {
                    $resp = self::getDetail( $params );
                }
                catch( \Exception $e )
                {
                    $resp->addData( 'err', 'appointment_not_accessible' );
                }

            }
        }

        return $resp;
    }

    /**
     * Process appointments waiting list after one attendee has declined
     *
     * @param TB_Shared_Ent_TeamData_Appointment $appointment
     * @return TB_Shared_Ent_TeamData_Appointment
     * @throws Exception
     */
    public static function processWaitingList( TB_Shared_Ent_TeamData_Appointment $appointment )
    {
        $isMaxReached = false;
        $numFreePlaces = 0;
        if ( (int)$appointment->max_attendees > 0 )
        {
            $acceptedAttendees = TB_Shared_Ent_TeamData_Attendee::getAttendeesWithStatusForAppointmentId(
                $appointment->id,
                TB_Shared_Ent_TeamData_Attendee::STATUS_ACCEPTED
            );

            if ( count( $acceptedAttendees ) >= $appointment->max_attendees )
            {
                $isMaxReached = true;
            }
            else
            {
                $numFreePlaces = ( $appointment->max_attendees - count( $acceptedAttendees ));
            }
        }

        // If there are no more places for waiting members
        if( true === $isMaxReached )
        {
            // Nothing to do
            return $appointment;
        }

        //
        $waitingAttendees = TB_Shared_Ent_TeamData_Attendee::getAttendeesWithStatusForAppointmentId(
            $appointment->id,
            TB_Shared_Ent_TeamData_Attendee::STATUS_WAITING
        );

        /** @var TB_Shared_Ent_TeamData_Attendee $waitingAttendee */
        foreach ( $waitingAttendees as $waitingAttendee )
        {
            $waitingAttendee->status = TB_Shared_Ent_TeamData_Attendee::STATUS_ACCEPTED;
            $waitingAttendee->has_attended = TB_Shared_Ent_TeamData_Attendee::HAS_ATTENDED;
            $waitingAttendee->save();

            TB_Server_Core_Notification::sendToProfiles(
                'Nachgerutscht',
                'Du bist nun beim Termin ' . $appointment->subject . ' angemeldet.',
                array( $waitingAttendee->profile_id ),
                TB_Server_Core_Notification::createAdditionalData( '#/appointment/' . $appointment->id )
            );

            $numFreePlaces--;
            if ( 0 === $numFreePlaces )
            {
                break;
            }
        }
    }

    public static function getListForAttendedProfileId( TB_Server_Core_RequestData $params )
    {
        $resp = new TB_Server_Core_Response();

        $teamId = $params->get( 'teamId' );
        $profileId = $params->get( 'profileId' );
        $profile = TB_Shared_Ent_TeamData_Profile::get( $profileId );
        if ( NULL === $profile )
        {
            throw new \Exception( 'Invalid profile.' );
        }

        $resp->addData( 'profile', $profile );
        $resp->addData( 'attended_appointments', TB_Shared_Ent_TeamData_Appointment::getTeamAppointmentsForAttendedProfileId( $teamId, $profileId ) );

        return $resp;
    }

} 