package encounter

import (
	"errors"
	"fmt"
	"simrs-vx/internal/lib/auth"
	"strconv"
	"time"

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

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

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

	ev "simrs-vx/internal/domain/bpjs-entities/vclaim-reference"
	edc "simrs-vx/internal/domain/main-entities/death-cause"
	e "simrs-vx/internal/domain/main-entities/encounter"
	eir "simrs-vx/internal/domain/main-entities/internal-reference"
	es "simrs-vx/internal/domain/main-entities/soapi"
	esync "simrs-vx/internal/domain/sync-entities/log"

	uv "simrs-vx/internal/use-case/bpjs-use-case/vclaim-reference"
	udc "simrs-vx/internal/use-case/main-use-case/death-cause"
	uir "simrs-vx/internal/use-case/main-use-case/internal-reference"
	us "simrs-vx/internal/use-case/main-use-case/soapi"
)

const source = "encounter"

var now = time.Now()

func Create(input e.CreateDto) (*d.Data, error) {
	var (
		data                   e.Encounter
		recentSoapiDataforCopy []es.CreateDto
		err                    error
	)

	event := pl.Event{
		Feature: "Create",
		Source:  source,
	}

	// Start log
	pl.SetLogInfo(&event, input, "started", "create")

	roleAllowed := []string{string(erg.EPCReg)}
	err = validateAuth(input.AuthInfo, roleAllowed, "create-encounter", &event)
	if err != nil {
		return nil, err
	}

	// validate rehab by bpjs
	if input.RefTypeCode == ere.RTCBpjs &&
		input.Class_Code == ere.ECAmbulatory &&
		ere.AmbulatoryClassCode(*input.SubClass_Code) == ere.ACCRehab {
		// get latest rehab data
		recentRehabData, err := getLatestRehabData(input, &event)
		if err != nil {
			return nil, err
		}

		// If recentRehabData is nil, then visitMode_Code = "adm"
		if recentRehabData != nil {
			// If recentRehabData is not nil, determine the visitMode_Code:
			// If the mode is "series", verify whether the visit count still remains
			// and whether the series has not expired.
			// If visitMode is "series", then get encounterAdm
			input.VisitMode_Code, input.RecentEncounterAdm, err = determineVisitMode(recentRehabData, input, &event)
			if err != nil {
				return nil, err
			}
		} else {
			input.VisitMode_Code = ere.VMCAdm
		}

		// When visitMode_Code is "series", load the associated SOAPI record to copy its values.
		if input.VisitMode_Code == ere.VMCSeries {
			// get data soapi
			recentSoapiDataforCopy, err = getSoapiEncounterAdm(*input.RecentEncounterAdm, &event)
			if err != nil {
				return nil, err
			}
		}
	}

	// check if patient is new in the hospital
	input.NewStatus, err = identifyPatientStatus(input)
	input.Adm_Employee_Id = input.AuthInfo.Employee_Id

	mwRunner := newMiddlewareRunner(&event)

	err = dg.I.Transaction(func(tx *gorm.DB) error {
		// create encounter
		if resData, err := CreateData(input, &event, tx); err != nil {
			return err
		} else {
			data = *resData
			input.Id = data.Id
		}

		// insert ambulatory/emergency/inpatient
		err = insertdataClassCode(input, recentSoapiDataforCopy, &event, tx)
		if err != nil {
			return err
		}

		// insert vclaimReference
		if vr := input.VclaimReference; vr != nil {
			t, _ := time.Parse("2006-01-02", vr.TglRujukan)
			_, err = uv.CreateData(ev.CreateDto{
				Encounter_Id: &data.Id,
				Date:         &t,
				SrcCode:      input.Ref_Number,
				SrcName:      input.RefSource_Name,
				Number:       &vr.NoSep}, &event, tx)
		}

		dataEncounter, err := ReadDetailData(e.ReadDetailDto{
			Id: data.Id,
			Includes: "Adm_Employee.User,Patient.Person.Relatives," +
				"Patient.Person.VclaimMember,VclaimReference," +
				"Patient.Person.Contacts,Patient.Person.Addresses"},
			&event, tx)
		if err != nil {
			return err
		}

		mwRunner.setMwType(pu.MWTPre)
		// Run pre-middleware
		if err := mwRunner.RunCreateMiddleware(createPreMw, dataEncounter); err != nil {
			return err
		}

		return nil
	})

	if err = runLogMiddleware(err, input, mwRunner); err != nil {
		return nil, err
	}

	pl.SetLogInfo(&event, nil, "complete")

	return &d.Data{
		Meta: d.II{
			"source":    source,
			"structure": "single-data",
			"status":    "created",
		},
		Data: data.ToResponse(),
	}, nil
}

func ReadList(input e.ReadListDto) (*d.Data, error) {
	var data *e.Encounter
	var dataList []e.Encounter
	var metaList *e.MetaDto
	var err error

	event := pl.Event{
		Feature: "ReadList",
		Source:  source,
	}

	// Start log
	pl.SetLogInfo(&event, input, "started", "readList")

	err = dg.I.Transaction(func(tx *gorm.DB) error {
		mwRunner := newMiddlewareRunner(&event)
		mwRunner.setMwType(pu.MWTPre)
		// Run pre-middleware
		if err := mwRunner.RunReadListMiddleware(readListPreMw, &input, data); err != nil {
			return err
		}

		if dataList, metaList, err = ReadListData(input, &event, tx); err != nil {
			return err
		}

		mwRunner.setMwType(pu.MWTPost)
		// Run post-middleware
		if err := mwRunner.RunReadListMiddleware(readListPostMw, &input, data); err != nil {
			return err
		}

		return nil
	})

	if err != nil {
		return nil, err
	}

	return &d.Data{
		Meta: d.IS{
			"source":              source,
			"structure":           "list-data",
			"status":              "fetched",
			"page_number":         strconv.Itoa(metaList.PageNumber),
			"page_size":           strconv.Itoa(metaList.PageSize),
			"record_totalCount":   strconv.Itoa(metaList.Count),
			"record_currentCount": strconv.Itoa(len(dataList)),
		},
		Data: e.ToResponseList(dataList),
	}, nil
}

func ReadDetail(input e.ReadDetailDto) (*d.Data, error) {
	var data *e.Encounter
	var err error

	event := pl.Event{
		Feature: "ReadDetail",
		Source:  source,
	}

	// Start log
	pl.SetLogInfo(&event, input, "started", "readDetail")

	err = dg.I.Transaction(func(tx *gorm.DB) error {
		mwRunner := newMiddlewareRunner(&event)
		mwRunner.setMwType(pu.MWTPre)
		// Run pre-middleware
		if err := mwRunner.RunReadDetailMiddleware(readDetailPreMw, &input, data); err != nil {
			return err
		}

		if data, err = ReadDetailData(input, &event, tx); err != nil {
			return err
		}

		mwRunner.setMwType(pu.MWTPost)
		// Run post-middleware
		if err := mwRunner.RunReadDetailMiddleware(readDetailPostMw, &input, data); err != nil {
			return err
		}

		return nil
	})

	if err != nil {
		return nil, err
	}

	return &d.Data{
		Meta: d.IS{
			"source":    source,
			"structure": "single-data",
			"status":    "fetched",
		},
		Data: data.ToResponse(),
	}, nil
}

func Update(input e.UpdateDto) (*d.Data, error) {
	rdDto := e.ReadDetailDto{Id: input.Id}
	var data *e.Encounter
	var err error

	event := pl.Event{
		Feature: "Update",
		Source:  source,
	}

	// Start log
	pl.SetLogInfo(&event, input, "started", "update")

	roleAllowed := []string{string(erg.EPCReg)}
	err = validateAuth(input.AuthInfo, roleAllowed, "update-encounter", &event)
	if err != nil {
		return nil, err
	}
	input.Adm_Employee_Id = input.AuthInfo.Employee_Id

	mwRunner := newMiddlewareRunner(&event)

	err = dg.I.Transaction(func(tx *gorm.DB) error {
		pl.SetLogInfo(&event, rdDto, "started", "DBReadDetail")
		if data, err = ReadDetailData(rdDto, &event, tx); err != nil {
			return err
		}

		if data.IsDone() {
			event.Status = "failed"
			event.ErrInfo = pl.ErrorInfo{
				Code:   "data-state-mismatch",
				Detail: "encounter is done",
				Raw:    errors.New("encounter is done"),
			}
			return pl.SetLogError(&event, input)
		}

		if err := UpdateData(input, data, &event, tx); err != nil {
			return err
		}

		dataEncounter, err := ReadDetailData(e.ReadDetailDto{
			Id:       data.Id,
			Includes: "Adm_Employee.User,Patient.Person.Relatives,Patient.Person.VclaimMember"},
			&event, tx)
		if err != nil {
			return err
		}

		mwRunner.setMwType(pu.MWTPre)
		// Run post-middleware
		if err := mwRunner.RunUpdateMiddleware(updatePreMw, dataEncounter); err != nil {
			return err
		}

		return nil
	})

	if err = runLogMiddleware(err, input, mwRunner); err != nil {
		return nil, err
	}

	pl.SetLogInfo(&event, nil, "complete")

	return &d.Data{
		Meta: d.IS{
			"source":    source,
			"structure": "single-data",
			"status":    "updated",
		},
		Data: data.ToResponse(),
	}, nil

}

func Delete(input e.DeleteDto) (*d.Data, error) {
	rdDto := e.ReadDetailDto{Id: input.Id}
	var data *e.Encounter
	var err error

	event := pl.Event{
		Feature: "Delete",
		Source:  source,
	}

	// Start log
	pl.SetLogInfo(&event, input, "started", "delete")

	roleAllowed := []string{string(erg.EPCReg)}
	err = validateAuth(input.AuthInfo, roleAllowed, "delete-encounter", &event)
	if err != nil {
		return nil, err
	}

	mwRunner := newMiddlewareRunner(&event)

	err = dg.I.Transaction(func(tx *gorm.DB) error {
		pl.SetLogInfo(&event, rdDto, "started", "DBReadDetail")
		if data, err = ReadDetailData(rdDto, &event, tx); err != nil {
			return err
		}

		if data.IsDone() {
			event.Status = "failed"
			event.ErrInfo = pl.ErrorInfo{
				Code:   "data-state-mismatch",
				Detail: "encounter is done",
				Raw:    errors.New("encounter is done"),
			}
			return pl.SetLogError(&event, input)
		}

		// Prevent delete if soapi exist
		dataSoapi, _, err := us.ReadListData(es.ReadListDto{
			FilterDto: es.FilterDto{Encounter_Id: &data.Id}}, &event, tx)
		if err != nil {
			return err
		}

		if len(dataSoapi) > 0 {
			event.Status = "failed"
			event.ErrInfo = pl.ErrorInfo{
				Code:   "cancel-not-allowed",
				Detail: "encounter can't be deleted because SOAPI already exists",
				Raw:    errors.New("encounter has SOAPI records"),
			}
			return pl.SetLogError(&event, input)
		}

		if err := DeleteData(data, &event, tx); err != nil {
			return err
		}

		mwRunner.setMwType(pu.MWTPre)
		// Run pre-middleware
		if err := mwRunner.RunDeleteMiddleware(deletePreMw, &input); err != nil {
			return err
		}

		return nil
	})

	if err = runLogMiddleware(err, input, mwRunner); err != nil {
		return nil, err
	}

	pl.SetLogInfo(&event, nil, "complete")

	return &d.Data{
		Meta: d.IS{
			"source":    source,
			"structure": "single-data",
			"status":    "deleted",
		},
		Data: data.ToResponse(),
	}, nil

}

func CheckOut(input e.DischargeDto) (*d.Data, error) {
	rdDto := e.ReadDetailDto{Id: input.Id, Includes: "Ambulatory,Rehab,InternalReferences"}
	var data *e.Encounter
	var err error

	event := pl.Event{
		Feature: "CheckOut",
		Source:  source,
	}

	// Start log
	pl.SetLogInfo(&event, input, "started", "checkOut")

	roleAllowed := []string{string(erg.EPCNur)}
	err = validateAuth(input.AuthInfo, roleAllowed, "checkOut-encounter", &event)
	if err != nil {
		return nil, err
	}

	mwRunner := newMiddlewareRunner(&event)

	err = dg.I.Transaction(func(tx *gorm.DB) error {
		pl.SetLogInfo(&event, rdDto, "started", "DBReadDetail")
		if data, err = ReadDetailData(rdDto, &event, tx); err != nil {
			return err
		}

		if data.IsDone() {
			event.Status = "failed"
			event.ErrInfo = pl.ErrorInfo{
				Code:   "data-state-mismatch",
				Detail: "encounter is done",
				Raw:    errors.New("encounter is done"),
			}
			return pl.SetLogError(&event, input)
		}

		if data.Ambulatory != nil && (data.Ambulatory.Class_Code == ere.ACCReg || data.Ambulatory.Class_Code == ere.ACCRehab) {
			// Validate SOAPI completeness: abort checkout if any required SOAPI is missing.
			if err = getSoapiByTypeCode(data, &event, "check-out"); err != nil {
				return err
			}

			if data.Ambulatory.Class_Code == ere.ACCRehab {
				// verify and update rehabData to DONE if visit count has reached the allowed limit
				if err = verifyRehabLimit(data, &event, tx); err != nil {
					return err
				}
			}
		} else {
			// TODO: chemo TBC (to be confirm)
			if err := checkSoapiByDocExists(data.Id, &event, tx); err != nil {
				return err
			}
		}

		// update encounter
		if err := updateDischargeData(input, data, &event, tx); err != nil {
			return err
		}

		if err := checkNewOrdersExist(data.Id, &event, tx); err != nil {
			return err
		}

		if err := createMedication(data.Id, &event, tx); err != nil {
			if !pu.IsDataNotFoundError(err) {
				return err
			}
		}

		// insert data death-cause
		if *input.Discharge_Method_Code == ere.DMCDeath {
			if _, err = udc.CreateData(edc.CreateDto{Encounter_Id: &input.Id, Value: input.DeathCause}, &event, tx); err != nil {
				return err
			}
		}

		// perform additional validation when discharge_method_code is "consul-poly" or "consul-executive"
		if *data.Discharge_Method_Code == ere.DMCConsulPoly || *data.Discharge_Method_Code == ere.DMCConsulExecutive {
			for _, v := range *data.InternalReferences {
				if *v.Status_Code == erc.DACNew {
					event.Status = "failed"
					event.ErrInfo = pl.ErrorInfo{
						Code:   "data-state-mismatch",
						Detail: "checkout is not allowed because the internal-reference status is 'new'",
						Raw:    errors.New("internal-referral status not done yet"),
					}
					return pl.SetLogError(&event, input)
				}
			}
		}

		// read the newest data encounter for sync
		dataEncounter, err := ReadDetailData(e.ReadDetailDto{Id: data.Id, Includes: "DeathCause"}, &event, tx)
		if err != nil {
			return err
		}

		mwRunner.setMwType(pu.MWTPre)
		// Run pre-middleware
		if err := mwRunner.RunCheckoutMiddleware(checkoutEncounter, dataEncounter); err != nil {
			return err
		}

		return nil
	})

	if err = runLogMiddleware(err, input, mwRunner); err != nil {
		return nil, err
	}

	pl.SetLogInfo(&event, nil, "complete")

	return &d.Data{
		Meta: d.IS{
			"source":    source,
			"structure": "single-data",
			"status":    "checkOut",
		},
		Data: data.ToResponse(),
	}, nil
}

func UpdateStatusCode(input e.UpdateStatusDto) (*d.Data, error) {
	rdDto := e.ReadDetailDto{Id: input.Id}
	var data *e.Encounter
	var err error

	event := pl.Event{
		Feature: "Update Status Code",
		Source:  source,
	}

	// Start log
	pl.SetLogInfo(&event, input, "started", "update")

	// TODO: Only "supervisi pendaftaran" could cancel encounter
	roleAllowed := []string{string(erg.EPCReg), string(erg.EPCNur), string(erg.EPCDoc)}
	err = validateAuth(input.AuthInfo, roleAllowed, "update-status-encounter", &event)
	if err != nil {
		return nil, err
	}

	mwRunner := newMiddlewareRunner(&event)

	err = dg.I.Transaction(func(tx *gorm.DB) error {
		pl.SetLogInfo(&event, rdDto, "started", "DBReadDetail")
		if data, err = ReadDetailData(rdDto, &event, tx); err != nil {
			return err
		}

		if data.IsDone() {
			event.Status = "failed"
			event.ErrInfo = pl.ErrorInfo{
				Code:   "data-state-mismatch",
				Detail: "encounter is done",
				Raw:    errors.New("encounter is done"),
			}
			return pl.SetLogError(&event, input)
		}

		if input.StatusCode == erc.DSCCancel {
			// TODO: Prevent cancellation if the billing has been verified

			// Prevent cancellation if soapi exist
			dataSoapi, _, err := us.ReadListData(es.ReadListDto{
				FilterDto: es.FilterDto{Encounter_Id: &data.Id}}, &event, tx)
			if err != nil {
				return err
			}

			if len(dataSoapi) > 0 {
				event.Status = "failed"
				event.ErrInfo = pl.ErrorInfo{
					Code:   "cancel-not-allowed",
					Detail: "encounter can't be cancelled because SOAPI already exists",
					Raw:    errors.New("encounter has SOAPI records"),
				}
				return pl.SetLogError(&event, input)
			}
		}

		if err := UpdateStatusData(input, &event, tx); err != nil {
			return err
		}

		if input.StatusCode == erc.DSCProcess || input.StatusCode == erc.DSCCancel {
			data.Status_Code = input.StatusCode

			mwRunner.setMwType(pu.MWTPre)
			// Run pre-middleware
			if err := mwRunner.RunUpdateStatusMiddleware(updatestatusEncounter, data); err != nil {
				return err
			}
		}

		return nil
	})

	if err = runLogMiddleware(err, input, mwRunner); err != nil {
		return nil, err
	}

	pl.SetLogInfo(&event, nil, "complete")

	return &d.Data{
		Meta: d.IS{
			"source":    source,
			"structure": "single-data",
			"status":    "updated",
		},
		Data: data.ToResponse(),
	}, nil

}

func CheckIn(input e.CheckinDto) (*d.Data, error) {
	rdDto := e.ReadDetailDto{Id: input.Id, Includes: "Rehab,Ambulatory"}
	var data *e.Encounter
	var err error

	event := pl.Event{
		Feature: "CheckIn",
		Source:  source,
	}

	// Start log
	pl.SetLogInfo(&event, input, "started", "checkIn")

	roleAllowed := []string{string(erg.EPCNur), string(erg.EPCDoc)}
	err = validateAuth(input.AuthInfo, roleAllowed, "checkIn-encounter", &event)
	if err != nil {
		return nil, err
	}
	input.Responsible_Nurse_Code = input.AuthInfo.Nurse_Code

	// validate foreign key
	if err := validateForeignKey(input, &event); err != nil {
		return nil, err
	}

	// set startedAt
	if input.StartedAt == nil {
		input.StartedAt = &now
	}

	mwRunner := newMiddlewareRunner(&event)

	err = dg.I.Transaction(func(tx *gorm.DB) error {
		pl.SetLogInfo(&event, rdDto, "started", "DBReadDetail")
		if data, err = ReadDetailData(rdDto, &event, tx); err != nil {
			return err
		}

		if data.IsDone() {
			event.Status = "failed"
			event.ErrInfo = pl.ErrorInfo{
				Code:   "data-state-mismatch",
				Detail: "encounter is done",
				Raw:    errors.New("encounter is done"),
			}
			return pl.SetLogError(&event, input)
		}

		// prevent repeated check-in. If SOAPI already exists, block the check-in process.
		if data.Ambulatory != nil && (data.Ambulatory.Class_Code == ere.ACCReg || data.Ambulatory.Class_Code == ere.ACCRehab) {
			err = getSoapiByTypeCode(data, &event, "check-in")
			if err != nil {
				return err
			}
		}

		// update encounter data
		if err := updateCheckInData(input, data, &event, tx); err != nil {
			return err
		}

		// read the newest data encounter for sync
		dataEncounter, err := ReadDetailData(e.ReadDetailDto{Id: data.Id, Includes: "Responsible_Nurse.Employee.User"}, &event, tx)
		if err != nil {
			return err
		}

		mwRunner.setMwType(pu.MWTPre)
		// Run pre-middleware
		if err := mwRunner.RunCheckinMiddleware(checkinEncounterMw, dataEncounter); err != nil {
			return err
		}

		return nil
	})

	if err = runLogMiddleware(err, input, mwRunner); err != nil {
		return nil, err
	}

	pl.SetLogInfo(&event, nil, "complete")

	return &d.Data{
		Meta: d.IS{
			"source":    source,
			"structure": "single-data",
			"status":    "checkIn",
		},
		Data: data.ToResponse(),
	}, nil
}

func RequestSwitchUnit(input e.SwitchUnitDto) (*d.Data, error) {
	rdDto := e.ReadDetailDto{Id: input.Id, Includes: "Responsible_Nurse.Employee.User,Responsible_Doctor.Employee"}
	var data *e.Encounter
	var err error

	event := pl.Event{
		Feature: "RequestSwitchUnit",
		Source:  source,
	}

	// Start log
	pl.SetLogInfo(&event, input, "started", "checkOut")

	unitCodes := make(map[string]struct{})
	doctorCodes := make(map[string]struct{})
	for _, ref := range *input.InternalReferences {
		if ref.Unit_Code != nil {
			unitCodes[*ref.Unit_Code] = struct{}{}
		}
		if ref.Doctor_Code != nil {
			doctorCodes[*ref.Doctor_Code] = struct{}{}
		}
	}

	// validate unit
	if err = validateUnitCodes(unitCodes, &event); err != nil {
		return nil, err
	}

	// validate doctor
	if err = validateDoctorCodes(doctorCodes, &event); err != nil {
		return nil, err
	}

	roleAllowed := []string{string(erg.EPCNur)}
	err = validateAuth(input.AuthInfo, roleAllowed, "request-switch-poly", &event)
	if err != nil {
		return nil, err
	}
	input.Src_Nurse_Code = input.AuthInfo.Nurse_Code

	mwRunner := newMiddlewareRunner(&event)

	err = dg.I.Transaction(func(tx *gorm.DB) error {
		pl.SetLogInfo(&event, rdDto, "started", "DBReadDetail")
		if data, err = ReadDetailData(rdDto, &event, tx); err != nil {
			return err
		}
		input.Src_Doctor_Code = data.Responsible_Doctor_Code

		if data.IsDone() {
			event.Status = "failed"
			event.ErrInfo = pl.ErrorInfo{
				Code:   "data-state-mismatch",
				Detail: "encounter is done",
				Raw:    errors.New("encounter is done"),
			}
			return pl.SetLogError(&event, input)
		}

		// Poly-Switch-Requests can only be processed if the previous responsible doctor has completed their SOAPI.
		dataSoapi, err := getSoapiByResponsibleDoctor(*data, &event)
		if err != nil {
			return err
		}
		if len(dataSoapi) < 1 {
			event.Status = "failed"
			event.ErrInfo = pl.ErrorInfo{
				Code:   "missing-soapi",
				Detail: fmt.Sprintf("Missing soapi from latest responsible doctor"),
			}
			return pl.SetLogError(&event, input)
		}

		// update encounter discharge_method_code
		if err = UpdateDischargeMethod(input, &event, tx); err != nil {
			return err
		}

		// bulk internal references
		if err := uir.CreateBulkData(&input, &event, tx); err != nil {
			return err
		}

		// read the newest data encounter for sync
		dataEncounter, err := ReadDetailData(e.ReadDetailDto{Id: data.Id, Includes: "InternalReferences,Responsible_Nurse.Employee.User"}, &event, tx)
		if err != nil {
			return err
		}

		mwRunner.setMwType(pu.MWTPre)
		// Run pre-middleware
		if err := mwRunner.RunRequestSwitchUnitMiddleware(requestSwitchEncounter, dataEncounter); err != nil {
			return err
		}
		return nil
	})

	if err = runLogMiddleware(err, input, mwRunner); err != nil {
		return nil, err
	}

	pl.SetLogInfo(&event, nil, "complete")

	return &d.Data{
		Meta: d.IS{
			"source":    source,
			"structure": "single-data",
			"status":    "requestSwitchUnit",
		},
		Data: data.ToResponse(),
	}, nil
}

func ApproveSwitchUnit(input e.ApproveCancelUnitDto) (*d.Data, error) {
	rdDto := e.ReadDetailDto{Id: input.Id, Includes: "Responsible_Doctor.Employee"}
	var (
		data *e.Encounter
		err  error
	)

	event := pl.Event{
		Feature: "ApproveSwitchUnit",
		Source:  source,
	}

	// Start log
	pl.SetLogInfo(&event, input, "started", "approveSwitchUnit")

	roleAllowed := []string{string(erg.EPCNur)}
	err = validateAuth(input.AuthInfo, roleAllowed, "request-switch-poly", &event)
	if err != nil {
		return nil, err
	}

	mwRunner := newMiddlewareRunner(&event)

	err = dg.I.Transaction(func(tx *gorm.DB) error {
		pl.SetLogInfo(&event, rdDto, "started", "DBReadDetail")
		if data, err = ReadDetailData(rdDto, &event, tx); err != nil {
			return err
		}

		if data.IsDone() {
			event.Status = "failed"
			event.ErrInfo = pl.ErrorInfo{
				Code:   "data-state-mismatch",
				Detail: "encounter is done",
				Raw:    errors.New("encounter is done"),
			}
			return pl.SetLogError(&event, input)
		}

		// get internalReference based on internalReferences_Id
		irData, err := uir.ReadDetailData(eir.ReadDetailDto{
			Id: input.InternalReferences_Id, Includes: "Doctor"}, &event, tx)
		if err != nil {
			return err
		}

		if *irData.Status_Code != erc.DACNew {
			event.Status = "failed"
			event.ErrInfo = pl.ErrorInfo{
				Code:   "data-state-mismatch",
				Detail: "internal references is approve",
				Raw:    errors.New("internal references is approve"),
			}
			return pl.SetLogError(&event, input)
		}

		// Approve-Switch-Requests can only be processed if the previous responsible doctor has completed their SOAPI.
		dataSoapi, err := getSoapiByResponsibleDoctor(*data, &event)
		if err != nil {
			return err
		}
		if len(dataSoapi) < 1 {
			event.Status = "failed"
			event.ErrInfo = pl.ErrorInfo{
				Code:   "missing-soapi",
				Detail: fmt.Sprintf("Missing soapi from latest responsible doctor"),
			}
			return pl.SetLogError(&event, input)
		}

		// Set doctor_code; nil indicates no change.
		if input.Dst_Doctor_Code == nil {
			input.Dst_Doctor_Code = irData.Doctor_Code
		}

		// update internal reference
		if err = uir.UpdateData(eir.UpdateDto{
			Id: input.InternalReferences_Id,
			CreateDto: eir.CreateDto{
				Status_Code: erc.DACApproved,
				Doctor_Code: input.Dst_Doctor_Code,
				Nurse_Code:  input.Nurse_Code}}, irData, &event, tx); err != nil {
			return err
		}

		// update encounter
		if err = updateEncounterApproveSwitchUnit(input, &event, tx); err != nil {
			return err
		}

		mwRunner.setMwType(pu.MWTPre)
		// Run pre-middleware
		if err := mwRunner.RunApproveSwitchUnitMiddleware(approveSwitchEncounter, &input); err != nil {
			return err
		}
		return nil
	})

	if err = runLogMiddleware(err, input, mwRunner); err != nil {
		return nil, err
	}

	pl.SetLogInfo(&event, nil, "complete")

	return &d.Data{
		Meta: d.IS{
			"source":    source,
			"structure": "single-data",
			"status":    "updated",
		},
		Data: data.ToResponse(),
	}, nil

}

func CancelSwitchUnit(input e.ApproveCancelUnitDto) (*d.Data, error) {
	rdDto := e.ReadDetailDto{Id: input.Id}
	var (
		data *e.Encounter
		err  error
	)

	event := pl.Event{
		Feature: "CancelSwitchUnit",
		Source:  source,
	}

	// Start log
	pl.SetLogInfo(&event, input, "started", "cancelSwitchUnit")

	roleAllowed := []string{string(erg.EPCNur)}
	err = validateAuth(input.AuthInfo, roleAllowed, "request-switch-poly", &event)
	if err != nil {
		return nil, err
	}

	mwRunner := newMiddlewareRunner(&event)

	err = dg.I.Transaction(func(tx *gorm.DB) error {
		pl.SetLogInfo(&event, rdDto, "started", "DBReadDetail")
		if data, err = ReadDetailData(rdDto, &event, tx); err != nil {
			return err
		}

		if data.IsDone() {
			event.Status = "failed"
			event.ErrInfo = pl.ErrorInfo{
				Code:   "data-state-mismatch",
				Detail: "encounter is done",
				Raw:    errors.New("encounter is done"),
			}
			return pl.SetLogError(&event, input)
		}

		// get internalReference based on internalReferences_Id
		irData, err := uir.ReadDetailData(eir.ReadDetailDto{
			Id: input.InternalReferences_Id}, &event, tx)
		if err != nil {
			return err
		}

		if *irData.Status_Code != erc.DACNew {
			event.Status = "failed"
			event.ErrInfo = pl.ErrorInfo{
				Code:   "data-state-mismatch",
				Detail: "internal references is approve",
				Raw:    errors.New("internal references is approve"),
			}
			return pl.SetLogError(&event, input)
		}

		// update internal reference
		if err = uir.UpdateData(eir.UpdateDto{
			Id: input.InternalReferences_Id,
			CreateDto: eir.CreateDto{
				Status_Code: erc.DACCanceled,
				Nurse_Code:  input.Nurse_Code}}, irData, &event, tx); err != nil {
			return err
		}

		mwRunner.setMwType(pu.MWTPre)
		// Run pre-middleware
		if err := mwRunner.RunCancelSwitchUnitMiddleware(cancelSwitchEncounter, &input); err != nil {
			return err
		}
		return nil
	})

	if err = runLogMiddleware(err, input, mwRunner); err != nil {
		return nil, err
	}

	pl.SetLogInfo(&event, nil, "complete")

	return &d.Data{
		Meta: d.IS{
			"source":    source,
			"structure": "single-data",
			"status":    "updated",
		},
		Data: data.ToResponse(),
	}, nil

}

func runLogMiddleware(err error, input any, mwRunner *middlewareRunner) error {
	var errMsg string
	inputLog := esync.SimxLogDto{
		Payload: input,
		Method:  erc.CCCreate,
	}

	if err != nil {
		// Run log-middleware
		errMsg = err.Error()
		inputLog.ErrMessage = &errMsg
		inputLog.IsSuccess = false

		// create log failed
		if errMiddleware := mwRunner.RunCreateLogMiddleware(createSimxLogMw, &inputLog); errMiddleware != nil {
			return errMiddleware
		}
		return err
	}

	// create log success
	inputLog.IsSuccess = true
	if err = mwRunner.RunCreateLogMiddleware(createSimxLogMw, &inputLog); err != nil {
		return err
	}

	return nil
}

func validateAuth(a auth.AuthInfo, roleAllowed []string, action string, event *pl.Event) error {
	// check if user has employee position
	if !a.HasEmployeePosition() {
		event.Status = "failed"
		event.ErrInfo = pl.ErrorInfo{
			Code:   "auth-forbidden",
			Detail: "missing employee position",
			Raw:    errors.New("authentication failed"),
		}
		return pl.SetLogError(event, a)
	}

	// check only roleAllowed position is allowed to create encounter
	if a.Employee_Position_Code != nil && !pu.Contains(roleAllowed, *a.Employee_Position_Code) {
		event.Status = "failed"
		event.ErrInfo = pl.ErrorInfo{
			Code:   "auth-forbidden",
			Detail: fmt.Sprintf("position '%s' is not allowed to %s", *a.Employee_Position_Code, action),
			Raw:    errors.New("authentication failed"),
		}
		return pl.SetLogError(event, a)
	}

	return nil
}
