import { DataTypes, Op } from 'sequelize';
import { ADMINISTRATION_FREQUENCIES, ENCOUNTER_TYPES, SYNC_DIRECTIONS, SYSTEM_USER_UUID, TASK_STATUSES, TASK_TYPES } from '@tamanu/constants';
import { addDays, addHours, addMonths, endOfDay, getDate, isValid, setDate, startOfDay } from 'date-fns';
import config from 'config';
import { getFirstAdministrationDate, areDatesInSameTimeSlot } from '@tamanu/shared/utils/medication';
import { Model } from '../Model';
import { dateTimeType } from '../../types/model';
import { getCurrentDateTimeString } from '@tamanu/utils/dateTime';
import { Task } from '../Task';
import { buildEncounterLinkedLookupSelect } from '../../sync/buildEncounterLinkedLookupFilter';
import { afterCreateHook, afterUpdateHook } from './hooks';
export class MedicationAdministrationRecord extends Model {
    static initModel({ primaryKey, ...options }) {
        super.init({
            id: primaryKey,
            status: DataTypes.STRING,
            dueAt: dateTimeType('dueAt', {
                allowNull: false
            }),
            recordedAt: dateTimeType('recordedAt', {
                allowNull: true
            }),
            isAutoGenerated: DataTypes.BOOLEAN,
            changingStatusReason: DataTypes.TEXT,
            changingNotGivenInfoReason: DataTypes.TEXT,
            isEdited: {
                type: DataTypes.BOOLEAN,
                allowNull: false,
                defaultValue: false
            },
            isError: DataTypes.BOOLEAN,
            errorNotes: DataTypes.TEXT
        }, {
            ...options,
            syncDirection: SYNC_DIRECTIONS.BIDIRECTIONAL,
            hooks: {
                afterCreate: afterCreateHook,
                afterUpdate: afterUpdateHook
            }
        });
    }
    /**
   * Generates upcoming Medication Administration Records (MARs) for a given prescription.
   *
   * This function calculates and creates MAR entries based on the prescription's
   * frequency, start date, end date, ideal administration times, and discontinuation date.
   * It ensures that MARs are generated up to a configurable timeframe in the future
   * (defaulting to 72 hours) or until the prescription's end date, whichever is earlier.
   *
   * Special handling is included for 'IMMEDIATELY' and 'AS_DIRECTED' frequencies,
   * as well as for calculating next due dates for various frequencies like
   * 'EVERY_SECOND_DAY', 'ONCE_A_WEEK', and 'ONCE_A_MONTH'.
   *
   * It avoids creating duplicate records by checking the last existing MAR and
   * skips generation if the calculated due date falls outside the valid prescription period
   * or before the last generated MAR.
   *
   * IMPORTANT: keep it in sync with the mobile app's MedicationAdministrationRecord model
   *
   * @param prescription The prescription object for which to generate MARs.
   */ static async generateMedicationAdministrationRecords(prescription) {
        if (!prescription.frequency || !prescription.startDate || prescription.frequency === ADMINISTRATION_FREQUENCIES.AS_DIRECTED) {
            return;
        }
        // Get the last MAR of the prescription (for cron job)
        const lastMedicationAdministrationRecord = await this.findOne({
            where: {
                prescriptionId: prescription.id
            },
            order: [
                [
                    'dueAt',
                    'DESC'
                ]
            ]
        });
        // If the prescription is immediately, create a MAR for the start date
        if (prescription.frequency === ADMINISTRATION_FREQUENCIES.IMMEDIATELY) {
            if (!lastMedicationAdministrationRecord) {
                await this.create({
                    prescriptionId: prescription.id,
                    dueAt: prescription.startDate,
                    isAutoGenerated: true
                });
            }
            return;
        }
        // Get the first administration date for the prescription
        let firstAdministrationDate;
        if (prescription.idealTimes && prescription.idealTimes.length > 0) {
            firstAdministrationDate = getFirstAdministrationDate(new Date(prescription.startDate), prescription.idealTimes);
        }
        // Get the upcoming records should be generated time frame
        const upcomingRecordsShouldBeGeneratedTimeFrame = config?.medicationAdministrationRecord?.upcomingRecordsShouldBeGeneratedTimeFrame || 72;
        let endDate = endOfDay(addHours(new Date(), upcomingRecordsShouldBeGeneratedTimeFrame));
        // Override with prescription end date if it's earlier
        if (prescription.endDate && new Date(prescription.endDate) < endDate) {
            endDate = new Date(prescription.endDate);
        }
        // Get the last due date for the prescription
        let lastDueDate;
        if (lastMedicationAdministrationRecord) {
            lastDueDate = new Date(lastMedicationAdministrationRecord.dueAt);
        } else if (firstAdministrationDate) {
            lastDueDate = firstAdministrationDate;
        } else {
            lastDueDate = new Date(prescription.startDate);
        }
        // Generate the upcoming MARs
        while(lastDueDate < endDate){
            for (const idealTime of prescription.idealTimes || []){
                const [hours, minutes] = idealTime.split(':').map(Number);
                const nextDueDate = new Date(lastDueDate.getFullYear(), lastDueDate.getMonth(), lastDueDate.getDate(), hours, minutes, 0);
                const prescriptionStartDate = new Date(prescription.startDate);
                // Skip if the next due date is after the end date, or after the prescription was discontinued
                // For cron job, skip if the next due date is before the last due date (to avoid creating duplicate records)
                if (nextDueDate < prescriptionStartDate && !areDatesInSameTimeSlot(prescriptionStartDate, nextDueDate) || nextDueDate > endDate || lastMedicationAdministrationRecord && nextDueDate <= new Date(lastMedicationAdministrationRecord.dueAt) || prescription.discontinuedDate && nextDueDate >= new Date(prescription.discontinuedDate)) {
                    continue;
                }
                // Skip if administration date is not valid (required to pass unit tests)
                if (isValid(nextDueDate)) {
                    await this.create({
                        prescriptionId: prescription.id,
                        dueAt: nextDueDate,
                        isAutoGenerated: true
                    });
                }
            }
            // Get next administration date based on frequency
            switch(prescription.frequency){
                case ADMINISTRATION_FREQUENCIES.EVERY_SECOND_DAY:
                    lastDueDate = startOfDay(addDays(lastDueDate, 2));
                    break;
                case ADMINISTRATION_FREQUENCIES.ONCE_A_WEEK:
                    lastDueDate = startOfDay(addDays(lastDueDate, 7));
                    break;
                case ADMINISTRATION_FREQUENCIES.ONCE_A_MONTH:
                    {
                        const lastDueDay = getDate(lastDueDate);
                        // If the due date of the first administration is the 29th or 30th or 31st, then set the next due date to the 1st of the second next month
                        if (lastDueDay >= 29) {
                            lastDueDate = startOfDay(setDate(addMonths(lastDueDate, 2), 1));
                        } else {
                            lastDueDate = startOfDay(setDate(addMonths(lastDueDate, 1), lastDueDay));
                        }
                        break;
                    }
                default:
                    lastDueDate = startOfDay(addDays(lastDueDate, 1));
                    break;
            }
        }
    }
    static async createMedicationDueTaskForMar(mar) {
        const { models } = this.sequelize;
        const { Task, Encounter, EncounterPrescription, Prescription } = models;
        const prescription = await Prescription.findByPk(mar.prescriptionId);
        if (!prescription) {
            throw new Error('Prescription not found');
        }
        // Skip if this is a PRN medication
        if (prescription.isPrn) return;
        const encounterPrescription = await EncounterPrescription.findOne({
            where: {
                prescriptionId: prescription.id
            },
            include: [
                {
                    model: Encounter,
                    as: 'encounter',
                    attributes: [
                        'id',
                        'encounterType',
                        'endDate'
                    ]
                }
            ]
        });
        const encounter = encounterPrescription?.encounter;
        if (!encounter) {
            throw new Error('Encounter not found');
        }
        // Skip if this is not an inpatient encounter or is discharged
        if (encounter.encounterType !== ENCOUNTER_TYPES.ADMISSION || encounter.endDate) {
            return;
        }
        const existingTask = await Task.findOne({
            where: {
                encounterId: encounter.id,
                dueTime: mar.dueAt,
                status: TASK_STATUSES.TODO,
                taskType: TASK_TYPES.MEDICATION_DUE_TASK
            },
            attributes: [
                'id'
            ]
        });
        // Skip if the task at the same ideal time already exists
        if (existingTask) return;
        await Task.create({
            taskType: TASK_TYPES.MEDICATION_DUE_TASK,
            encounterId: encounter.id,
            name: 'Medication Due',
            dueTime: mar.dueAt,
            status: TASK_STATUSES.TODO,
            requestTime: getCurrentDateTimeString(),
            requestedByUserId: SYSTEM_USER_UUID
        });
    }
    /**
   * If all MARs at the same ideal time as the task are recorded (GIVEN or NOT_GIVEN), mark the task as completed by the system user
   */ static async checkAndCompleteMedicationDueTask(mar) {
        const { models } = this.sequelize;
        const { Task, EncounterPrescription, MedicationAdministrationRecord, Prescription } = models;
        const encounterPrescription = await EncounterPrescription.findOne({
            where: {
                prescriptionId: mar.prescriptionId
            },
            attributes: [
                'encounterId'
            ]
        });
        if (!encounterPrescription) return;
        const encounterId = encounterPrescription.encounterId;
        const task = await Task.findOne({
            where: {
                encounterId,
                dueTime: mar.dueAt,
                status: TASK_STATUSES.TODO,
                taskType: TASK_TYPES.MEDICATION_DUE_TASK
            },
            attributes: [
                'id',
                'dueTime'
            ]
        });
        if (!task) return;
        // Check if there is another unrecorded MAR at the same ideal time as the task
        const unrecordedMar = await MedicationAdministrationRecord.findOne({
            where: {
                id: {
                    [Op.ne]: mar.id
                },
                dueAt: mar.dueAt,
                status: null
            },
            include: [
                {
                    model: Prescription,
                    as: 'prescription',
                    attributes: [
                        'id'
                    ],
                    required: true,
                    include: [
                        {
                            model: EncounterPrescription,
                            as: 'encounterPrescription',
                            attributes: [
                                'encounterId'
                            ],
                            required: true,
                            where: {
                                encounterId
                            }
                        }
                    ]
                }
            ],
            attributes: [
                'id'
            ]
        });
        // Skip if there is still at least one unrecorded MAR
        if (unrecordedMar) return;
        await Task.update({
            status: TASK_STATUSES.COMPLETED,
            completedTime: getCurrentDateTimeString(),
            completedByUserId: SYSTEM_USER_UUID
        }, {
            where: {
                id: task.id
            }
        });
    }
    /**
   * Removes medication administration records that are no longer valid due to:
   * 1. The encounter being discharged (MARs with due dates after the encounter end date)
   * 2. The prescription being discontinued (MARs with due dates after the prescription discontinued date)
   *
   * @param {Transaction} [transaction] - Optional transaction to use for the database operations
   */ static async removeInvalidMedicationAdministrationRecords(transaction) {
        const { models } = this.sequelize;
        const { Prescription, EncounterPrescription } = models;
        const marsToRemove = await this.findAll({
            where: {
                [Op.or]: [
                    // Case 1: Discontinued prescriptions
                    {
                        '$prescription.discontinued$': true,
                        dueAt: {
                            [Op.gt]: {
                                [Op.col]: 'prescription.discontinued_date'
                            }
                        }
                    },
                    // Case 2: Prescriptions from discharged encounters
                    {
                        '$prescription.encounterPrescription.encounter.end_date$': {
                            [Op.not]: null
                        },
                        dueAt: {
                            [Op.gt]: {
                                [Op.col]: 'prescription.encounterPrescription.encounter.end_date'
                            }
                        }
                    }
                ],
                status: null
            },
            include: [
                {
                    model: Prescription,
                    as: 'prescription',
                    required: true,
                    attributes: [
                        'id'
                    ],
                    include: [
                        {
                            model: EncounterPrescription,
                            as: 'encounterPrescription',
                            required: true,
                            include: [
                                'encounter'
                            ],
                            attributes: [
                                'id'
                            ]
                        }
                    ]
                }
            ],
            attributes: [
                'id',
                'dueAt'
            ],
            transaction
        });
        const marIdsToRemove = marsToRemove.map((mar)=>mar.id);
        // Remove tasks that are no longer valid
        for (const mar of marsToRemove){
            const encounter = mar?.prescription?.encounterPrescription?.encounter;
            if (!encounter) continue;
            const existingTask = await Task.findOne({
                where: {
                    encounterId: encounter.id,
                    dueTime: mar.dueAt,
                    status: TASK_STATUSES.TODO,
                    taskType: TASK_TYPES.MEDICATION_DUE_TASK
                },
                attributes: [
                    'id'
                ]
            });
            if (!existingTask) continue;
            const unrecordedMar = await MedicationAdministrationRecord.findOne({
                where: {
                    id: {
                        [Op.notIn]: marIdsToRemove
                    },
                    dueAt: mar.dueAt,
                    status: null
                },
                include: [
                    {
                        model: Prescription,
                        as: 'prescription',
                        attributes: [
                            'id'
                        ],
                        required: true,
                        include: [
                            {
                                model: EncounterPrescription,
                                as: 'encounterPrescription',
                                attributes: [
                                    'encounterId'
                                ],
                                required: true,
                                where: {
                                    encounterId: encounter.id
                                }
                            }
                        ]
                    }
                ],
                attributes: [
                    'id'
                ]
            });
            if (unrecordedMar) continue;
            await existingTask.destroy({
                transaction
            });
        }
        await this.destroy({
            where: {
                id: {
                    [Op.in]: marIdsToRemove
                }
            },
            transaction
        });
    }
    static getListReferenceAssociations() {
        return [
            'prescription'
        ];
    }
    static initRelations(models) {
        this.belongsTo(models.Prescription, {
            foreignKey: 'prescriptionId',
            as: 'prescription'
        });
        this.belongsTo(models.ReferenceData, {
            foreignKey: 'reasonNotGivenId',
            as: 'reasonNotGiven'
        });
        this.hasMany(models.MedicationAdministrationRecordDose, {
            foreignKey: 'marId',
            as: 'doses'
        });
        this.belongsTo(models.User, {
            foreignKey: 'recordedByUserId',
            as: 'recordedByUser'
        });
    }
    static buildPatientSyncFilter(patientCount, markedForSyncPatientsTable) {
        if (patientCount === 0) {
            return null;
        }
        return `
      LEFT JOIN encounter_prescriptions ON medication_administration_records.prescription_id = encounter_prescriptions.prescription_id
      LEFT JOIN encounters ON encounter_prescriptions.encounter_id = encounters.id
      LEFT JOIN patient_ongoing_prescriptions ON medication_administration_records.prescription_id = patient_ongoing_prescriptions.prescription_id
      WHERE (
        (encounters.patient_id IS NOT NULL AND encounters.patient_id IN (SELECT patient_id FROM ${markedForSyncPatientsTable}))
        OR
        (patient_ongoing_prescriptions.patient_id IS NOT NULL AND patient_ongoing_prescriptions.patient_id IN (SELECT patient_id FROM ${markedForSyncPatientsTable}))
      )
      AND medication_administration_records.updated_at_sync_tick > :since
    `;
    }
    static async buildSyncLookupQueryDetails() {
        return {
            select: await buildEncounterLinkedLookupSelect(this, {
                patientId: 'COALESCE(encounters.patient_id, patient_ongoing_prescriptions.patient_id)'
            }),
            joins: `
        LEFT JOIN encounter_prescriptions ON medication_administration_records.prescription_id = encounter_prescriptions.prescription_id
        LEFT JOIN encounters ON encounter_prescriptions.encounter_id = encounters.id
        LEFT JOIN patient_ongoing_prescriptions ON medication_administration_records.prescription_id = patient_ongoing_prescriptions.prescription_id
        LEFT JOIN locations ON encounters.location_id = locations.id
        LEFT JOIN facilities ON locations.facility_id = facilities.id
      `
        };
    }
}

//# sourceMappingURL=MedicationAdministrationRecord.js.map