"use strict";
Object.defineProperty(exports, "__esModule", {
    value: true
});
Object.defineProperty(exports, "AppointmentSchedule", {
    enumerable: true,
    get: function() {
        return AppointmentSchedule;
    }
});
const _lodash = require("lodash");
const _sequelize = require("sequelize");
const _datefns = require("date-fns");
const _constants = require("@tamanu/constants");
const _errors = require("@tamanu/errors");
const _dateTime = require("@tamanu/utils/dateTime");
const _appointmentScheduling = require("@tamanu/utils/appointmentScheduling");
const _Model = require("./Model");
const _buildSyncLookupSelect = require("../sync/buildSyncLookupSelect");
const _model = require("./../types/model");
const _resolveAppointmentSchedules = require("../sync/resolveAppointmentSchedules");
let AppointmentSchedule = class AppointmentSchedule extends _Model.Model {
    static initModel({ primaryKey, ...options }) {
        super.init({
            id: primaryKey,
            untilDate: (0, _model.dateType)('untilDate', {
                allowNull: true
            }),
            generatedUntilDate: (0, _model.dateType)('generatedUntilDate', {
                allowNull: true
            }),
            cancelledAtDate: (0, _model.dateType)('cancelledAtDate', {
                allowNull: true
            }),
            interval: {
                type: _sequelize.DataTypes.INTEGER,
                allowNull: false
            },
            frequency: {
                type: _sequelize.DataTypes.ENUM(..._constants.REPEAT_FREQUENCY_VALUES),
                allowNull: false
            },
            daysOfWeek: {
                type: _sequelize.DataTypes.ARRAY(_sequelize.DataTypes.STRING),
                allowNull: true
            },
            nthWeekday: {
                type: _sequelize.DataTypes.INTEGER,
                allowNull: true
            },
            occurrenceCount: {
                type: _sequelize.DataTypes.INTEGER,
                allowNull: true
            },
            isFullyGenerated: {
                type: _sequelize.DataTypes.BOOLEAN,
                allowNull: false,
                defaultValue: false
            }
        }, {
            ...options,
            syncDirection: _constants.SYNC_DIRECTIONS.BIDIRECTIONAL,
            validate: {
                mustHaveEitherUntilDateOrOccurrenceCount () {
                    if (!this.untilDate && !this.occurrenceCount) {
                        throw new _errors.InvalidOperationError('AppointmentSchedule must have either untilDate or occurrenceCount');
                    }
                },
                // Currently all implemented frequencies require a single weekday, multiple weekdays are currently not supported
                mustHaveOneWeekday () {
                    const daysOfWeek = this.daysOfWeek;
                    if (daysOfWeek.length !== 1 || daysOfWeek[0] && !_constants.DAYS_OF_WEEK.includes(daysOfWeek[0])) {
                        throw new _errors.InvalidOperationError('AppointmentSchedule must have exactly one weekday');
                    }
                },
                mustHaveNthWeekdayForMonthly () {
                    if (this.frequency === _constants.REPEAT_FREQUENCY.MONTHLY && !(0, _lodash.isNumber)(this.nthWeekday)) {
                        throw new _errors.InvalidOperationError('AppointmentSchedule must have nthWeekday for MONTHLY frequency');
                    }
                }
            }
        });
    }
    static initRelations(models) {
        this.hasMany(models.Appointment, {
            as: 'appointments',
            foreignKey: 'scheduleId'
        });
    }
    static buildPatientSyncFilter(patientCount, markedForSyncPatientsTable) {
        if (patientCount === 0) {
            return null;
        }
        return `
      JOIN
        appointments
      ON
        appointments.schedule_id = appointment_schedules.id
      LEFT JOIN
        location_groups
      ON
        appointments.location_group_id = location_groups.id
      LEFT JOIN
        locations
      ON
        appointments.location_id = locations.id
      WHERE
        appointments.patient_id IN (SELECT patient_id FROM ${markedForSyncPatientsTable})
      AND
        COALESCE(location_groups.facility_id, locations.facility_id) in (:facilityIds)
      AND
        appointment_schedules.updated_at_sync_tick > :since
    `;
    }
    static async buildSyncLookupQueryDetails() {
        return {
            select: await (0, _buildSyncLookupSelect.buildSyncLookupSelect)(this, {
                patientId: 'appointments.patient_id',
                facilityId: 'COALESCE(location_groups.facility_id, locations.facility_id)'
            }),
            joins: `
        JOIN (
          SELECT DISTINCT ON (schedule_id) *
          FROM appointments
        ) appointments ON appointments.schedule_id = ${this.tableName}.id
        LEFT JOIN location_groups ON appointments.location_group_id = location_groups.id
        LEFT JOIN locations ON appointments.location_id = locations.id
      `
        };
    }
    static async incomingSyncHook(changes) {
        return (0, _resolveAppointmentSchedules.resolveAppointmentSchedules)(this, changes);
    }
    isDifferentFromSchedule(scheduleData) {
        const toComparable = (schedule)=>(0, _lodash.omit)(schedule, [
                'createdAt',
                'updatedAt',
                'updatedAtSyncTick',
                'id'
            ]);
        return !(0, _lodash.isMatch)(toComparable(this.get({
            plain: true
        })), toComparable(scheduleData));
    }
    /**
   * End the schedule at the given appointment.
   * This will cancel all appointments starting from the given appointment.
   * The schedule will then be marked as fully generated and the untilDate will be set to the
   * startTime of the latest non-cancelled appointment.
   * @param appointment All appointments starting from this appointment will be cancelled
   */ async endAtAppointment(appointment) {
        const { models } = this.sequelize;
        if (!this.sequelize.isInsideTransaction()) {
            throw new Error('AppointmentSchedule.endAtAppointment must always run inside a transaction');
        }
        await models.Appointment.update({
            status: _constants.APPOINTMENT_STATUSES.CANCELLED
        }, {
            where: {
                startTime: {
                    [_sequelize.Op.gte]: appointment.startTime
                },
                scheduleId: this.id
            }
        });
        const [previousAppointment] = await this.getAppointments({
            order: [
                [
                    'startTime',
                    'DESC'
                ]
            ],
            limit: 1,
            where: {
                status: {
                    [_sequelize.Op.not]: _constants.APPOINTMENT_STATUSES.CANCELLED
                }
            }
        });
        const updatedSchedule = await this.update({
            isFullyGenerated: true,
            cancelledAtDate: previousAppointment ? previousAppointment.startTime : appointment.startTime
        });
        return updatedSchedule;
    }
    /**
   * Modify all appointments starting from the given appointment.
   * @param appointment The appointment to start modifying from
   * @param appointmentData The data to update the appointments with
   */ async modifyFromAppointment(appointment, appointmentData) {
        const { models } = this.sequelize;
        return models.Appointment.update(appointmentData, {
            where: {
                startTime: {
                    [_sequelize.Op.gte]: appointment.startTime
                },
                scheduleId: this.id
            }
        });
    }
    /**
   * Generate repeating appointments based on the schedule parameters and the initial appointment data.
   * When the generation is complete, the schedule is marked as fully generated.
   * Otherwise the schedule continues to generate via the scheduled task GenerateRepeatingAppointments
   * @param settings Settings reader
   * @param initialAppointmentData Optional initial appointment data to start the generation
   */ async generateRepeatingAppointment(settings, initialAppointmentData) {
        const { models } = this.sequelize;
        if (!this.sequelize.isInsideTransaction()) {
            throw new Error('AppointmentSchedule.generateRepeatingAppointment must always run inside a transaction');
        }
        const maxRepeatingAppointmentsPerGeneration = await settings.get('appointments.maxRepeatingAppointmentsPerGeneration');
        const existingAppointments = await this.getAppointments({
            order: [
                [
                    'startTime',
                    'DESC'
                ]
            ],
            where: {
                status: {
                    [_sequelize.Op.not]: _constants.APPOINTMENT_STATUSES.CANCELLED
                }
            }
        });
        const latestExistingAppointment = existingAppointments[0];
        if (!(initialAppointmentData || latestExistingAppointment)) {
            throw new Error('Cannot generate repeating appointments without initial appointment data or existing appointments within the schedule');
        }
        const { interval, frequency, untilDate, occurrenceCount, daysOfWeek, nthWeekday } = this;
        const parsedUntilDate = untilDate && (0, _datefns.endOfDay)((0, _datefns.parseISO)(untilDate));
        const appointmentsToCreate = [];
        if (initialAppointmentData) {
            // Add the initial appointment data to the list of appointments to generate and to act
            // as a reference for incremented appointments
            appointmentsToCreate.push({
                ...initialAppointmentData,
                scheduleId: this.id
            });
        }
        const adjustDateForFrequency = (date)=>{
            if (frequency === _constants.REPEAT_FREQUENCY.MONTHLY) {
                // Set the date to the nth weekday of the month i.e 3rd Monday
                return (0, _datefns.set)(date, {
                    date: (0, _appointmentScheduling.weekdayAtOrdinalPosition)(date, daysOfWeek[0], nthWeekday).getDate()
                });
            }
            return date;
        };
        const incrementDateString = (date)=>{
            const incrementedDate = (0, _datefns.add)((0, _datefns.parseISO)(date), {
                [_constants.REPEAT_FREQUENCY_UNIT_PLURAL_LABELS[frequency]]: interval
            });
            return (0, _dateTime.toDateTimeString)(adjustDateForFrequency(incrementedDate));
        };
        const pushNextAppointment = ()=>{
            // Get the most recent appointment or start off where the last generation left off
            const referenceAppointment = appointmentsToCreate.at(-1) || latestExistingAppointment.toCreateData();
            appointmentsToCreate.push({
                ...referenceAppointment,
                startTime: incrementDateString(referenceAppointment.startTime),
                endTime: referenceAppointment.endTime && incrementDateString(referenceAppointment.endTime)
            });
        };
        const checkFullyGenerated = ()=>{
            // Generation is considered complete if the next appointments startTime falls after the untilDate
            const nextAppointmentAfterUntilDate = parsedUntilDate && (0, _datefns.isAfter)((0, _datefns.parseISO)(incrementDateString(appointmentsToCreate.at(-1).startTime)), parsedUntilDate);
            // Or if the occurrenceCount is reached
            const hasReachedOccurrenceCount = occurrenceCount && appointmentsToCreate.length + existingAppointments.length === occurrenceCount;
            return nextAppointmentAfterUntilDate || hasReachedOccurrenceCount;
        };
        let isFullyGenerated = false;
        // If initial appointment data has been preloaded in appointment array start generating from i = 1
        for(let i = appointmentsToCreate.length; i < maxRepeatingAppointmentsPerGeneration; i++){
            pushNextAppointment();
            if (checkFullyGenerated()) {
                isFullyGenerated = true;
                break;
            }
        }
        const appointments = await models.Appointment.bulkCreate(appointmentsToCreate);
        await this.update({
            isFullyGenerated,
            generatedUntilDate: appointments.at(-1).startTime
        });
        return appointments;
    }
};

//# sourceMappingURL=AppointmentSchedule.js.map