/*
DESCRIPTION:
Any functions that are used internally by the use-case
*/
package encounter

import (
	"errors"
	"fmt"
	un "simrs-vx/internal/use-case/main-use-case/nurse"
	"strings"
	"time"

	authhelper "simrs-vx/internal/lib/auth"

	dg "github.com/karincake/apem/db-gorm-pg"
	"gorm.io/gorm"

	pl "simrs-vx/pkg/logger"
	pu "simrs-vx/pkg/use-case-helper"

	ercl "simrs-vx/internal/domain/references/clinical"
	erc "simrs-vx/internal/domain/references/common"
	ere "simrs-vx/internal/domain/references/encounter"
	erg "simrs-vx/internal/domain/references/organization"

	eaeh "simrs-vx/internal/domain/main-entities/adm-employee-hist"
	ea "simrs-vx/internal/domain/main-entities/ambulatory"
	ec "simrs-vx/internal/domain/main-entities/chemo"
	edo "simrs-vx/internal/domain/main-entities/device-order"
	ed "simrs-vx/internal/domain/main-entities/doctor"
	ee "simrs-vx/internal/domain/main-entities/emergency"
	e "simrs-vx/internal/domain/main-entities/encounter"
	ei "simrs-vx/internal/domain/main-entities/inpatient"
	emo "simrs-vx/internal/domain/main-entities/material-order"
	emco "simrs-vx/internal/domain/main-entities/mcu-order"
	em "simrs-vx/internal/domain/main-entities/medication"
	emei "simrs-vx/internal/domain/main-entities/medication-item"
	emi "simrs-vx/internal/domain/main-entities/medicine-mix"
	emmi "simrs-vx/internal/domain/main-entities/medicine-mix-item"
	en "simrs-vx/internal/domain/main-entities/nurse"
	ep "simrs-vx/internal/domain/main-entities/prescription"
	epi "simrs-vx/internal/domain/main-entities/prescription-item"
	er "simrs-vx/internal/domain/main-entities/rehab"
	erdh "simrs-vx/internal/domain/main-entities/responsible-doctor-hist"
	es "simrs-vx/internal/domain/main-entities/soapi"
	eu "simrs-vx/internal/domain/main-entities/unit"

	// udo "simrs-vx/internal/use-case/main-use-case/device-order"
	uaeh "simrs-vx/internal/use-case/main-use-case/adm-employee-hist"
	ua "simrs-vx/internal/use-case/main-use-case/ambulatory"
	uc "simrs-vx/internal/use-case/main-use-case/chemo"
	ud "simrs-vx/internal/use-case/main-use-case/doctor"
	ue "simrs-vx/internal/use-case/main-use-case/emergency"
	ui "simrs-vx/internal/use-case/main-use-case/inpatient"
	um "simrs-vx/internal/use-case/main-use-case/medication"
	umei "simrs-vx/internal/use-case/main-use-case/medication-item"
	umi "simrs-vx/internal/use-case/main-use-case/medicine-mix"
	ummi "simrs-vx/internal/use-case/main-use-case/medicine-mix-item"
	_ "simrs-vx/internal/use-case/main-use-case/nurse"
	up "simrs-vx/internal/use-case/main-use-case/prescription"
	upi "simrs-vx/internal/use-case/main-use-case/prescription-item"
	ur "simrs-vx/internal/use-case/main-use-case/rehab"
	urdh "simrs-vx/internal/use-case/main-use-case/responsible-doctor-hist"
	us "simrs-vx/internal/use-case/main-use-case/soapi"
)

func setData[T *e.CreateDto | *e.UpdateDto](input T, data *e.Encounter) {
	var inputSrc *e.CreateDto
	if inputT, ok := any(input).(*e.CreateDto); ok {
		inputSrc = inputT
		data.Status_Code = erc.DSCNew
	} else {
		inputTemp := any(input).(*e.UpdateDto)
		inputSrc = &inputTemp.CreateDto
	}

	data.Patient_Id = inputSrc.Patient_Id
	data.RegisteredAt = inputSrc.RegisteredAt
	data.Class_Code = inputSrc.Class_Code
	data.Unit_Code = inputSrc.Unit_Code
	data.Specialist_Code = inputSrc.Specialist_Code
	data.Subspecialist_Code = inputSrc.Subspecialist_Code
	data.VisitDate = inputSrc.VisitDate
	data.PaymentMethod_Code = inputSrc.PaymentMethod_Code
	data.InsuranceCompany_Code = inputSrc.InsuranceCompany_Code
	data.Member_Number = inputSrc.Member_Number
	data.Ref_Number = inputSrc.Ref_Number
	data.Trx_Number = inputSrc.Trx_Number
	data.Appointment_Doctor_Code = inputSrc.Appointment_Doctor_Code
	data.Adm_Employee_Id = inputSrc.Adm_Employee_Id
	data.Responsible_Doctor_Code = inputSrc.Responsible_Doctor_Code
	data.RefSource_Name = inputSrc.RefSource_Name
	data.Appointment_Id = inputSrc.Appointment_Id
	data.RefType_Code = &inputSrc.RefTypeCode
	data.NewStatus = inputSrc.NewStatus
}

func setDataUpdate(src e.UpdateDto, dst *e.Encounter) {
	dst.Appointment_Doctor_Code = src.Appointment_Doctor_Code
	dst.Responsible_Doctor_Code = src.Responsible_Doctor_Code
	dst.Unit_Code = src.Unit_Code
	dst.Specialist_Code = src.Specialist_Code
	dst.Subspecialist_Code = src.Subspecialist_Code
	dst.VisitDate = src.VisitDate
}

func setDataDischarge(src e.DischargeDto, dst *e.Encounter) {
	dst.Discharge_Method_Code = src.Discharge_Method_Code
	dst.EarlyEducation = src.EarlyEducation
	dst.MedicalDischargeEducation = src.MedicalDischargeEducation
	dst.AdmDischargeEducation = src.AdmDischargeEducation
	dst.DischargeReason = src.DischargeReason
	dst.Status_Code = erc.DSCDone

	now := time.Now()
	dst.Discharge_Date = &now
	dst.FinishedAt = &now
}

func setDataCheckIn(src e.CheckinDto, dst *e.Encounter) {
	dst.Responsible_Nurse_Code = src.Responsible_Nurse_Code
	dst.Responsible_Doctor_Code = src.Responsible_Doctor_Code
	dst.StartedAt = src.StartedAt
	dst.Status_Code = erc.DSCProcess
}

func setDischargeMethodCode(code ere.PolySwitchCode) *ere.DischargeMethodCode {
	var dcm ere.DischargeMethodCode

	switch code {
	case ere.PSCConsulPoly:
		dcm = ere.DMCConsulPoly
	case ere.PSCConsulExecutive:
		dcm = ere.DMCConsulExecutive
	}

	return &dcm
}

func checkSoapiByDocExists(encounter_id uint, event *pl.Event, tx *gorm.DB) error {
	pl.SetLogInfo(event, nil, "started", "checkSoapiByDocExists")
	var soapies []es.Soapi
	err := tx.
		Preload("Employee").
		Preload("Employee.User").
		Where("\"Encounter_Id\" = ?", encounter_id).Find(&soapies).Error
	if err != nil {
		event.Status = "failed"
		event.ErrInfo = pl.ErrorInfo{
			Code:   "data-get-fail",
			Detail: "get soapi failed",
			Raw:    err,
		}
		return pl.SetLogError(event, nil)
	}

	if len(soapies) == 0 {
		event.Status = "failed"
		event.ErrInfo = pl.ErrorInfo{
			Code:   "data-notFound",
			Detail: "no soapi found for encounter",
			Raw:    errors.New("soapi not found"),
		}
		return pl.SetLogError(event, nil)
	}

	for _, s := range soapies {
		if s.Employee != nil && *s.Employee.Position_Code == erg.EPCDoc {
			return nil
		}
	}

	event.Status = "failed"
	event.ErrInfo = pl.ErrorInfo{
		Code:   "data-update-fail",
		Detail: "no soapi written by a doctor found",
		Raw:    errors.New("all soapi employees are not doctors"),
	}
	return pl.SetLogError(event, nil)
}

func createMedication(encounter_id uint, event *pl.Event, tx *gorm.DB) error {
	pl.SetLogInfo(event, nil, "started", "createMedication")

	prescription, err := up.ReadDetailData(ep.ReadDetailDto{Encounter_Id: &encounter_id}, event, tx)
	if err != nil {
		return err
	}

	prescriptionItem, _, err := upi.ReadListData(epi.ReadListDto{
		FilterDto: epi.FilterDto{Prescription_Id: &prescription.Id},
		Includes:  "medicineMix,medicineMix-MixItems"}, event, tx)
	if err != nil {
		return err
	}

	if len(prescriptionItem) == 0 {
		return nil
	}

	medicationCreate := em.CreateDto{
		Encounter_Id: &encounter_id,
		IssuedAt:     pu.GetTimeNow(),
		Status_Code:  erc.DSCNew,
	}
	medication, err := um.CreateData(medicationCreate, event, tx)
	if err != nil {
		return err
	}

	for _, prescriptionItem := range prescriptionItem {
		if prescriptionItem.IsMix {
			medMix_id, err := createMedicineMixAndItem(*prescriptionItem.MedicineMix, event, tx)
			if err != nil {
				return err
			}
			prescriptionItem.MedicineMix_Id = medMix_id
		}
		err := createMedicationItem(medication.Id, prescriptionItem, event, tx)
		if err != nil {
			return err
		}
	}
	return nil
}

func createMedicineMixAndItem(input emi.MedicineMix, event *pl.Event, tx *gorm.DB) (*uint, error) {
	pl.SetLogInfo(event, nil, "started", "createMedicineMix")

	medicineMixCreate := emi.CreateDto{
		Name:     input.Name,
		Uom_Code: input.Uom_Code,
	}
	medicineMix, err := umi.CreateData(medicineMixCreate, event, tx)
	if err != nil {
		return nil, err
	}

	// recreate medicineMixItem with new medicineMix_id to keep medMixItem remain the same for prescriptionItem that is created
	for _, medicineMixItem := range input.MixItems {
		medicineMixItemCreate := emmi.CreateDto{
			MedicineMix_Id: &medicineMix.Id,
			Medicine_Code:  medicineMixItem.Medicine_Code,
			Dose:           medicineMixItem.Dose,
		}
		_, err := ummi.CreateData(medicineMixItemCreate, event, tx)
		if err != nil {
			return nil, err
		}

	}
	return &medicineMix.Id, nil
}

func createMedicationItem(medication_id uint, input epi.PrescriptionItem, event *pl.Event, tx *gorm.DB) error {
	pl.SetLogInfo(event, nil, "started", "createMedicationItem")

	medicationItemCreate := emei.CreateDto{
		Medication_Id:     &medication_id,
		IsMix:             input.IsMix,
		Medicine_Code:     input.Medicine_Code,
		MedicineMix_Id:    input.MedicineMix_Id,
		Usage:             input.Usage,
		Interval:          input.Interval,
		IntervalUnit_Code: input.IntervalUnit_Code,
		Quantity:          input.Quantity,
	}

	_, err := umei.CreateData(medicationItemCreate, event, tx)
	if err != nil {
		return err
	}

	return nil
}

func checkNewOrdersExist(encounter_id uint, event *pl.Event, tx *gorm.DB) error {
	pl.SetLogInfo(event, nil, "started", "CheckNewOrdersExist")
	var errs []string
	if err := getDeviceOrders(encounter_id, event, tx); err != nil {
		errs = append(errs, err.Error())
	}

	if err := getMaterialOrders(encounter_id, event, tx); err != nil {
		errs = append(errs, err.Error())
	}

	if err := getMcuOrders(encounter_id, event, tx); err != nil {
		errs = append(errs, err.Error())
	}

	if len(errs) > 0 {
		return fmt.Errorf("encounter has open orders: %s", strings.Join(errs, "; "))
	}

	return nil
}

func getDeviceOrders(encounter_id uint, event *pl.Event, tx *gorm.DB) error {
	pl.SetLogInfo(event, nil, "started", "getDeviceOrders")
	var orders []edo.DeviceOrder
	err := tx.Where("\"Encounter_Id\" = ? AND \"Status_Code\" = ?", encounter_id, erc.DSCNew).Find(&orders).Error
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			return nil
		}
		event.Status = "failed"
		event.ErrInfo = pl.ErrorInfo{
			Code:   "data-get-fail",
			Detail: "get device order failed",
			Raw:    err,
		}
		return pl.SetLogError(event, nil)
	}

	if len(orders) > 0 {
		return fmt.Errorf("encounter has %d device orders", len(orders))
	}

	return nil
}

func getMaterialOrders(encounter_id uint, event *pl.Event, tx *gorm.DB) error {
	pl.SetLogInfo(event, nil, "started", "getMaterialOrders")
	var orders []emo.MaterialOrder
	err := tx.Where("\"Encounter_Id\" = ? AND \"Status_Code\" = ?", encounter_id, erc.DSCNew).Find(&orders).Error
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			return nil
		}
		event.Status = "failed"
		event.ErrInfo = pl.ErrorInfo{
			Code:   "data-get-fail",
			Detail: "get material order failed",
			Raw:    err,
		}
		return pl.SetLogError(event, nil)
	}

	if len(orders) > 0 {
		return fmt.Errorf("encounter has %d material orders", len(orders))
	}

	return nil
}

func getMcuOrders(encounter_id uint, event *pl.Event, tx *gorm.DB) error {
	pl.SetLogInfo(event, nil, "started", "getMcuOrders")
	var orders []emco.McuOrder
	err := tx.Where("\"Encounter_Id\" = ? AND \"Status_Code\" = ?", encounter_id, erc.DSCNew).Find(&orders).Error
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			return nil
		}
		event.Status = "failed"
		event.ErrInfo = pl.ErrorInfo{
			Code:   "data-get-fail",
			Detail: "get mcu order failed",
			Raw:    err,
		}
		return pl.SetLogError(event, nil)
	}

	if len(orders) > 0 {
		return fmt.Errorf("encounter has %d mcu orders", len(orders))
	}

	return nil
}

func upsertResponsibleDoctorHist(input erdh.CreateDto, event *pl.Event, dbx ...*gorm.DB) error {
	pl.SetLogInfo(event, nil, "started", "DBCreate")

	var tx *gorm.DB
	if len(dbx) > 0 {
		tx = dbx[0]
	} else {
		tx = dg.I
	}

	var latest erdh.ResponsibleDoctorHist
	err := tx.
		Where("\"Encounter_Id\" = ?", input.Encounter_Id).
		Order("\"CreatedAt\" DESC").
		Limit(1).
		First(&latest).Error

	switch {
	case errors.Is(err, gorm.ErrRecordNotFound):
		// Insert
		if _, err = urdh.CreateData(input, event, tx); err != nil {
			return err
		}
	case err != nil:
		event.Status = "failed"
		event.ErrInfo = pl.ErrorInfo{
			Code:   "read-fail",
			Detail: "Failed to read responsible doctor history",
			Raw:    err,
		}
		return pl.SetLogError(event, input)
	default:
		// Update
		if err := tx.Model(&latest).Updates(map[string]interface{}{
			"Doctor_Code": input.Doctor_Code,
			"StartedAt":   input.StartedAt,
			"UpdatedAt":   time.Now(),
		}).Error; err != nil {
			event.Status = "failed"
			event.ErrInfo = pl.ErrorInfo{
				Code:   "update-fail",
				Detail: "Failed to update responsible doctor history",
				Raw:    err,
			}
			return pl.SetLogError(event, input)
		}
	}
	pl.SetLogInfo(event, input, "complete")
	return nil
}

func upsertAdmEmployeeHist(input eaeh.CreateDto, event *pl.Event, dbx ...*gorm.DB) error {
	pl.SetLogInfo(event, nil, "started", "DBCreate")

	var tx *gorm.DB
	if len(dbx) > 0 {
		tx = dbx[0]
	} else {
		tx = dg.I
	}

	var latest eaeh.AdmEmployeeHist
	err := tx.
		Where("\"Encounter_Id\" = ?", input.Encounter_Id).
		Order("\"CreatedAt\" DESC").
		Limit(1).
		First(&latest).Error

	switch {
	case errors.Is(err, gorm.ErrRecordNotFound):
		// Insert
		if _, err = uaeh.CreateData(input, event, tx); err != nil {
			return err
		}
	case err != nil:
		event.Status = "failed"
		event.ErrInfo = pl.ErrorInfo{
			Code:   "read-fail",
			Detail: "Failed to read responsible doctor history",
			Raw:    err,
		}
		return pl.SetLogError(event, input)

	default:
		// Update
		if err := tx.Model(&latest).Updates(map[string]interface{}{
			"Employee_Id": input.Employee_Id,
			"StartedAt":   input.StartedAt,
			"UpdatedAt":   time.Now(),
		}).Error; err != nil {
			event.Status = "failed"
			event.ErrInfo = pl.ErrorInfo{
				Code:   "update-fail",
				Detail: "Failed to update responsible doctor history",
				Raw:    err,
			}
			return pl.SetLogError(event, input)
		}
	}
	pl.SetLogInfo(event, input, "complete")

	return nil
}

func updateLatestResponsibleDoctorHist(input e.CheckinDto, event *pl.Event, dbx ...*gorm.DB) error {
	pl.SetLogInfo(event, "started", "DBUpdate")

	var tx *gorm.DB
	if len(dbx) > 0 {
		tx = dbx[0]
	} else {
		tx = dg.I
	}

	subQuery := tx.
		Select("\"Id\"").
		Model(&erdh.ResponsibleDoctorHist{}).
		Where("\"Encounter_Id\" = ?", input.Id).
		Order("\"CreatedAt\" DESC").
		Limit(1)

	result := tx.
		Model(&erdh.ResponsibleDoctorHist{}).
		Where("\"Id\" = (?)", subQuery).
		Update("\"FinishedAt\"", input.FinishedAt)

	if result.Error != nil {
		event.Status = "failed"
		event.ErrInfo = pl.ErrorInfo{
			Code:   "data-update-fail",
			Detail: "Database update failed",
			Raw:    result.Error,
		}
		return pl.SetLogError(event, input)
	}

	if result.RowsAffected == 0 {
		pl.SetLogInfo(event, input, "no previous data found to update")
		return nil
	}

	pl.SetLogInfo(event, nil, "complete")
	return nil
}

func updateLatestAdmEmployeeHist(input e.CheckinDto, event *pl.Event, dbx ...*gorm.DB) error {
	pl.SetLogInfo(event, "started", "DBUpdate")

	var tx *gorm.DB
	if len(dbx) > 0 {
		tx = dbx[0]
	} else {
		tx = dg.I
	}

	subQuery := tx.
		Select("\"Id\"").
		Model(&eaeh.AdmEmployeeHist{}).
		Where("\"Encounter_Id\" = ?", input.Id).
		Order("\"CreatedAt\" DESC").
		Limit(1)

	result := tx.
		Model(&eaeh.AdmEmployeeHist{}).
		Where("\"Id\" = (?)", subQuery).
		Update("\"FinishedAt\"", input.FinishedAt)

	if result.Error != nil {
		event.Status = "failed"
		event.ErrInfo = pl.ErrorInfo{
			Code:   "data-update-fail",
			Detail: "Database update failed",
			Raw:    result.Error,
		}
		return pl.SetLogError(event, input)
	}

	if result.RowsAffected == 0 {
		pl.SetLogInfo(event, input, "no previous data found to update")
		return nil
	}

	pl.SetLogInfo(event, nil, "complete")
	return nil
}

func getSoapiByResponsibleDoctor(enc e.Encounter, event *pl.Event) (data []es.Soapi, err error) {
	pl.SetLogInfo(event, enc, "started", "DBReadList")

	if enc.Responsible_Doctor == nil {
		event.Status = "failed"
		event.ErrInfo = pl.ErrorInfo{
			Code:   "no responsible-doctor found",
			Detail: "Encounter does not have responsible-doctor",
		}
		return nil, pl.SetLogError(event, enc)
	}

	err = dg.I.
		Model(&es.Soapi{}).
		Joins("JOIN \"Employee\" ON \"Employee\".\"Id\" = \"Soapi\".\"Employee_Id\"").
		Where("\"Encounter_Id\" = ?", enc.Id).
		Where("\"Employee\".\"Position_Code\" = ?", erg.EPCDoc).
		Where("\"Soapi\".\"TypeCode\" IN ?", []ercl.SoapiTypeCode{ercl.STCEEarlyMedic, ercl.STCFunc}).
		Where("\"Soapi\".\"Employee_Id\" = ?", *enc.Responsible_Doctor.Employee_Id).
		Find(&data).Error

	if err != nil {
		event.Status = "failed"
		event.ErrInfo = pl.ErrorInfo{
			Raw:    err,
			Code:   "read-fail",
			Detail: "Database read failed",
		}
		return nil, pl.SetLogError(event, enc)
	}

	pl.SetLogInfo(event, nil, "complete")
	return
}

func getSoapiEncounterAdm(enc e.Encounter, event *pl.Event) (dataSoapi []es.CreateDto, err error) {
	data, err := getSoapiByResponsibleDoctor(enc, event)
	if err != nil {
		return nil, err
	}

	for _, s := range data {
		// set data soapi for copy
		dataSoapi = append(dataSoapi, es.CreateDto{
			Employee_Id: s.Employee_Id,
			Time:        s.Time,
			TypeCode:    s.TypeCode,
			Value:       s.Value,
		})
	}

	if len(dataSoapi) < 2 {
		event.Status = "failed"
		event.ErrInfo = pl.ErrorInfo{
			Code:   "missing-soapi",
			Detail: fmt.Sprintf("Missing required Soapi types"),
		}
		return nil, pl.SetLogError(event, enc)
	}
	return
}

func getSoapiByTypeCode(encounter *e.Encounter, event *pl.Event, mode string) (err error) {
	pl.SetLogInfo(event, encounter, "started", "DBReadList")

	var (
		dataSoapi []es.Soapi
		amb       = encounter.Ambulatory
		rehab     = encounter.Rehab
	)

	// Set Query for get data Soapi by doc
	tx := dg.I.
		Model(&es.Soapi{}).
		Joins("JOIN \"Employee\" ON \"Employee\".\"Id\" = \"Soapi\".\"Employee_Id\"").
		Where("\"Encounter_Id\" = ?", encounter.Id).
		Where("\"Employee\".\"Position_Code\" = ?", erg.EPCDoc)

	// Set Case
	switch {
	case amb.Class_Code == ere.ACCReg:
		tx = tx.Where("\"Soapi\".\"TypeCode\" = ?", ercl.STCEEarlyMedic)
	case amb.Class_Code == ere.ACCRehab && *rehab.VisitMode_Code == ere.VMCAdm:
		tx = tx.Where("\"Soapi\".\"TypeCode\" IN ?", []ercl.SoapiTypeCode{ercl.STCEEarlyMedic, ercl.STCFunc, ercl.STCEarlyRehab})
	case amb.Class_Code == ere.ACCRehab && *rehab.VisitMode_Code == ere.VMCSeries:
		tx = tx.Where("\"Soapi\".\"TypeCode\" = ?", ercl.STCEarlyRehab)
	}

	if err = tx.Find(&dataSoapi).Error; err != nil {
		return setDBError(event, err, encounter)
	}

	pl.SetLogInfo(event, nil, "complete")
	return validateExistedSoapi(dataSoapi, encounter, event, mode)
}

func validateExistedSoapi(dataSoapi []es.Soapi, dataEncounter *e.Encounter, event *pl.Event, mode string) error {
	var (
		amb   = dataEncounter.Ambulatory
		rehab = dataEncounter.Rehab
	)

	typeExist := make(map[ercl.SoapiTypeCode]bool)
	for _, s := range dataSoapi {
		typeExist[s.TypeCode] = true
	}

	required := []ercl.SoapiTypeCode{}

	switch {
	case amb.Class_Code == ere.ACCReg:
		required = []ercl.SoapiTypeCode{ercl.STCEEarlyMedic}
	case amb.Class_Code == ere.ACCRehab && *rehab.VisitMode_Code == ere.VMCAdm:
		required = []ercl.SoapiTypeCode{ercl.STCEEarlyMedic, ercl.STCFunc, ercl.STCEarlyRehab}
	case amb.Class_Code == ere.ACCRehab && *rehab.VisitMode_Code == ere.VMCSeries:
		required = []ercl.SoapiTypeCode{ercl.STCEarlyRehab}
	}

	var missing, existing []string
	for _, code := range required {
		if typeExist[code] {
			existing = append(existing, string(code))
		} else {
			missing = append(missing, string(code))
		}
	}

	switch mode {
	case "check-in":
		if len(existing) > 0 {
			return setSoapiError(event, fmt.Sprintf("Soapi type(s) %s exist, can't check-in", strings.Join(existing, ", ")))
		}
	case "check-out":
		if len(missing) > 0 {
			return setSoapiError(event, fmt.Sprintf("Soapi type(s) %s not found, can't check-out", strings.Join(missing, ", ")))
		}
	}

	return nil
}

func identifyPatientStatus(input e.CreateDto) (isNewPatient bool, err error) {
	dataPatient, err := ReadList(e.ReadListDto{
		FilterDto: e.FilterDto{Patient_Id: input.Patient_Id},
		AuthInfo:  authhelper.AuthInfo{User_Id: input.User_Id}})
	if err != nil {
		return
	}

	if list, ok := dataPatient.Data.([]e.ResponseDto); ok {
		if len(list) < 1 {
			isNewPatient = true
		}
	}

	return
}

func determineVisitMode(recentRehabData *er.Rehab, input e.CreateDto, event *pl.Event) (ere.VisitModeCode, *e.Encounter, error) {
	var (
		visitModeCode          ere.VisitModeCode
		recentAdmEncounterData e.Encounter
		isQuotaValid           bool
		err                    error
	)

	switch *recentRehabData.Status_Code {
	case erc.DSCProcess:
		visitModeCode = ere.VMCSeries

		// verify whether the allocated visit count has not exceeded the limit
		recentAdmEncounterData, isQuotaValid, err = verifyAllocatedVisitCount(input, event)
		if err != nil {
			return "", nil, err
		}

		if !isQuotaValid {
			event.Status = "failed"
			event.ErrInfo = pl.ErrorInfo{
				Code:   "visit-limit-exceeded",
				Detail: "Encounter has exceeded the allowed number of visits",
				Raw:    errors.New("visit count exceeds allowed limit"),
			}
			return "", nil, pl.SetLogError(event, input)
		}

		if recentRehabData.ExpiredAt != nil && recentRehabData.ExpiredAt.Before(*pu.GetTimeNow()) {
			event.Status = "failed"
			event.ErrInfo = pl.ErrorInfo{
				Code:   "visit-limit-exceeded",
				Detail: "Encounter period has expired",
				Raw:    errors.New("encounter expired"),
			}
			return "", nil, pl.SetLogError(event, input)
		}

	case erc.DSCDone:
		visitModeCode = ere.VMCAdm

	default:
		event.Status = "failed"
		event.ErrInfo = pl.ErrorInfo{
			Code:   "invalid-status",
			Detail: fmt.Sprintf("Unknown rehab status: %v", *recentRehabData.Status_Code),
			Raw:    errors.New("unsupported rehab status"),
		}
		return "", nil, pl.SetLogError(event, input)
	}

	return visitModeCode, &recentAdmEncounterData, nil
}

func insertdataClassCode(input e.CreateDto, soapiData []es.CreateDto, event *pl.Event, tx *gorm.DB) (err error) {
	switch input.Class_Code {
	case ere.ECAmbulatory:
		subCode := ere.AmbulatoryClassCode(*input.SubClass_Code)
		ambCreate := ea.CreateDto{
			Encounter_Id: &input.Id,
			Class_Code:   subCode,
		}

		// create data Ambulatory
		_, err = ua.CreateData(ambCreate, event, tx)
		if err != nil {
			return err
		}

		// insert chemo/rehab
		err = insertDataSubClassAmbulatory(input, soapiData, event, tx)
		if err != nil {
			return err
		}

	case ere.ECEmergency:
		subCode := ere.EmergencyClassCode(*input.SubClass_Code)
		emerCreate := ee.CreateDto{
			Encounter_Id: &input.Id,
			Class_Code:   subCode,
		}

		// create data emergency
		_, err = ue.CreateData(emerCreate, event, tx)
		if err != nil {
			return err
		}
	case ere.ECInpatient:
		subCode := ere.InpatientClassCode(*input.SubClass_Code)
		inpCreate := ei.CreateDto{
			Encounter_Id: &input.Id,
			Class_Code:   subCode,
			Infra_Code:   input.Infra_Code,
		}

		// create data inpatient
		_, err = ui.CreateData(inpCreate, event, tx)
		if err != nil {
			return err
		}
	default:
		return errors.New("invalid encounter class code")
	}

	return
}

func insertDataSubClassAmbulatory(input e.CreateDto, soapiData []es.CreateDto, event *pl.Event, tx *gorm.DB) (err error) {
	subCode := ere.AmbulatoryClassCode(*input.SubClass_Code)

	switch {
	case subCode == ere.ACCChemo:
		chemoCreate := ec.CreateDto{
			Encounter_Id: &input.Id,
			Status_Code:  erc.DVCNew,
			SrcUnit_Code: input.Unit_Code,
		}

		// create data chemo
		_, err = uc.CreateData(chemoCreate, event, tx)
		if err != nil {
			return err
		}

	case subCode == ere.ACCRehab:
		rehabData := er.CreateDto{
			Encounter_Id:   &input.Id,
			VisitMode_Code: input.VisitMode_Code,
			Status_Code:    erc.DSCProcess,
		}

		// if visitMode_code is series, then bulk insert soapi
		if input.VisitMode_Code == ere.VMCSeries {
			rehabData.Parent_Encounter_Id = &input.RecentEncounterAdm.Id

			// Insert Soapi
			if err = us.CreateBulkData(soapiData, input.Id, event, tx); err != nil {
				return err
			}
		}

		// create rehab
		if _, err = ur.CreateData(rehabData, event, tx); err != nil {
			return err
		}
	}

	return
}

func verifyRehabLimit(data *e.Encounter, event *pl.Event, tx *gorm.DB) error {
	// get data encounter adm
	encounterAdmData, _, err := verifyAllocatedVisitCount(e.CreateDto{Patient_Id: data.Patient_Id}, event)
	if err != nil {
		return err
	}

	// Check if the visit count has reached the allowed limit
	// Mark latest rehab status as 'done' if exceeded.
	if len(*encounterAdmData.RehabChildren) >= *encounterAdmData.Rehab.AllocatedVisitCount {
		err = updateRehabStatus(er.UpdateDto{
			Id:        uint16(data.Rehab.Id),
			CreateDto: er.CreateDto{Status_Code: erc.DSCDone}}, event, tx)
		if err != nil {
			return err
		}
	}

	return nil
}

func updateRehabStatus(input er.UpdateDto, event *pl.Event, dbx ...*gorm.DB) error {
	pl.SetLogInfo(event, "started", "DBUpdate")

	var tx *gorm.DB
	if len(dbx) > 0 {
		tx = dbx[0]
	} else {
		tx = dg.I
	}

	result := tx.
		Model(&er.Rehab{}).
		Where("\"Id\" = (?)", input.Id).
		Update("\"Status_Code\"", input.Status_Code)

	if result.Error != nil {
		event.Status = "failed"
		event.ErrInfo = pl.ErrorInfo{
			Code:   "data-update-fail",
			Detail: "Database update failed",
			Raw:    result.Error,
		}
		return pl.SetLogError(event, input)
	}

	pl.SetLogInfo(event, nil, "complete")
	return nil
}

func validateForeignKey(input e.CheckinDto, event *pl.Event) error {
	// validate employee_Id
	if input.Responsible_Nurse_Code != nil {
		if _, err := un.ReadDetailData(en.ReadDetailDto{Code: input.Responsible_Nurse_Code}, event); err != nil {
			return err
		}
	}

	// validate doctor_Code
	if input.Responsible_Doctor_Code != nil {
		if _, err := ud.ReadDetailData(ed.ReadDetailDto{Code: input.Responsible_Doctor_Code}, event); err != nil {
			return err
		}
	}

	return nil
}

func setSoapiError(event *pl.Event, detail string) error {
	event.Status = "failed"
	event.ErrInfo = pl.ErrorInfo{
		Code:   "invalid-soapi-state",
		Detail: detail,
	}
	return pl.SetLogError(event, detail)
}

func setDBError(event *pl.Event, err error, ctx any) error {
	event.Status = "failed"
	event.ErrInfo = pl.ErrorInfo{
		Raw:    err,
		Code:   "read-fail",
		Detail: "Database read failed",
	}
	return pl.SetLogError(event, ctx)
}

func getUnits(unitIds []string, event *pl.Event) ([]eu.Unit, error) {
	pl.SetLogInfo(event, nil, "started", "getUnits")
	var units []eu.Unit
	err := dg.I.Where("\"Code\" IN ?", unitIds).Find(&units).Error
	if err != nil {
		event.Status = "failed"
		event.ErrInfo = pl.ErrorInfo{
			Code:   "data-get-fail",
			Detail: "get units",
			Raw:    err,
		}
		return nil, pl.SetLogError(event, nil)
	}
	return units, nil
}

func getDoctors(doctorIds []string, event *pl.Event) ([]ed.Doctor, error) {
	pl.SetLogInfo(event, nil, "started", "getDoctors")
	var doctors []ed.Doctor
	err := dg.I.Where("\"Code\" IN ?", doctorIds).Find(&doctors).Error
	if err != nil {
		event.Status = "failed"
		event.ErrInfo = pl.ErrorInfo{
			Code:   "data-get-fail",
			Detail: "get doctors",
			Raw:    err,
		}
		return nil, pl.SetLogError(event, nil)
	}
	return doctors, nil
}

func validateUnitCodes(unitCodes map[string]struct{}, event *pl.Event) error {
	if len(unitCodes) > 0 {
		var codes []string
		for code := range unitCodes {
			codes = append(codes, code)
		}

		units, err := getUnits(codes, event)
		if err != nil {
			return fmt.Errorf("failed to fetch units: %w", err)
		}
		if len(units) != len(codes) {
			event.Status = "failed"
			event.ErrInfo = pl.ErrorInfo{
				Code:   "data-validation-fail",
				Detail: "unit_code not found",
			}
			return pl.SetLogError(event, nil)
		}
	}

	return nil
}

func validateDoctorCodes(doctorCodes map[string]struct{}, event *pl.Event) error {
	if len(doctorCodes) > 0 {
		var codes []string
		for code := range doctorCodes {
			codes = append(codes, code)
		}

		doctors, err := getDoctors(codes, event)
		if err != nil {
			return fmt.Errorf("failed to fetch doctors: %w", err)
		}
		if len(doctors) != len(codes) {
			event.Status = "failed"
			event.ErrInfo = pl.ErrorInfo{
				Code:   "data-validation-fail",
				Detail: "doctor_code not found",
			}
			return pl.SetLogError(event, nil)
		}
	}

	return nil
}
