package mysql

import (
	"context"
	"errors"
	"strings"
	"time"

	"gorm.io/gorm"
	"gorm.io/gorm/clause"

	basedomain "wucher/internal/domain/base"
	dutyroster "wucher/internal/domain/duty_roster"
)

type DutyRosterRepository struct {
	db *gorm.DB
}

func NewDutyRosterRepository(db *gorm.DB) *DutyRosterRepository {
	return &DutyRosterRepository{db: db}
}

func (r *DutyRosterRepository) InTx(ctx context.Context, fn func(txRepo dutyroster.TxRepository) error) error {
	return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		return fn(&DutyRosterRepository{db: tx})
	})
}

func (r *DutyRosterRepository) GetRosterByID(ctx context.Context, id []byte) (*dutyroster.DutyRoster, error) {
	row := &dutyroster.DutyRoster{}
	if err := r.db.WithContext(ctx).
		Where("id = ? AND deleted_at IS NULL", id).
		Take(row).Error; err != nil {
		return nil, err
	}
	return row, nil
}

func (r *DutyRosterRepository) LockRosterByID(ctx context.Context, id []byte) (*dutyroster.DutyRoster, error) {
	row := &dutyroster.DutyRoster{}
	if err := r.db.WithContext(ctx).
		Clauses(clause.Locking{Strength: "UPDATE"}).
		Where("id = ? AND deleted_at IS NULL", id).
		Take(row).Error; err != nil {
		return nil, err
	}
	return row, nil
}

func (r *DutyRosterRepository) CreateHeader(ctx context.Context, row *dutyroster.DutyRoster) error {
	return r.db.WithContext(ctx).Create(row).Error
}

func (r *DutyRosterRepository) UpdateHeader(ctx context.Context, row *dutyroster.DutyRoster) error {
	updates := map[string]any{
		"base_id":    row.BaseID,
		"duty_date":  row.DutyDate,
		"updated_by": row.UpdatedBy,
	}
	res := r.db.WithContext(ctx).Model(&dutyroster.DutyRoster{}).
		Where("id = ? AND deleted_at IS NULL", row.ID).
		Updates(updates)
	if res.Error != nil {
		return res.Error
	}
	if res.RowsAffected == 0 {
		return gorm.ErrRecordNotFound
	}
	return nil
}

func (r *DutyRosterRepository) SoftDeleteHeader(ctx context.Context, id []byte, deletedBy []byte) error {
	now := time.Now().UTC()
	res := r.db.WithContext(ctx).Model(&dutyroster.DutyRoster{}).
		Where("id = ? AND deleted_at IS NULL", id).
		Updates(map[string]any{"deleted_at": now, "deleted_by": deletedBy, "updated_by": deletedBy})
	if res.Error != nil {
		return res.Error
	}
	if res.RowsAffected == 0 {
		return gorm.ErrRecordNotFound
	}
	return nil
}

func (r *DutyRosterRepository) SoftDeleteCrewsByRosterID(ctx context.Context, rosterID []byte, deletedBy []byte) error {
	now := time.Now().UTC()
	return r.db.WithContext(ctx).Model(&dutyroster.DutyRosterCrew{}).
		Where("roster_id = ? AND deleted_at IS NULL", rosterID).
		Updates(map[string]any{"deleted_at": now, "deleted_by": deletedBy, "updated_by": deletedBy}).Error
}

func (r *DutyRosterRepository) HardDeleteHeader(ctx context.Context, id []byte) error {
	return r.db.WithContext(ctx).
		Where("id = ?", id).
		Delete(&dutyroster.DutyRoster{}).Error
}

func (r *DutyRosterRepository) HardDeleteCrewsByRosterID(ctx context.Context, rosterID []byte) error {
	return r.db.WithContext(ctx).
		Where("roster_id = ?", rosterID).
		Delete(&dutyroster.DutyRosterCrew{}).Error
}

func (r *DutyRosterRepository) ListActiveCrewsByRosterID(ctx context.Context, rosterID []byte) ([]dutyroster.DutyRosterCrew, error) {
	rows := make([]dutyroster.DutyRosterCrew, 0)
	if err := r.db.WithContext(ctx).
		Where("roster_id = ? AND deleted_at IS NULL", rosterID).
		Find(&rows).Error; err != nil {
		return nil, err
	}
	return rows, nil
}

func (r *DutyRosterRepository) InsertCrew(ctx context.Context, row *dutyroster.DutyRosterCrew) error {
	return r.db.WithContext(ctx).Create(row).Error
}

func (r *DutyRosterRepository) CrewExists(ctx context.Context, row dutyroster.DutyRosterCrew) (bool, error) {
	return crewExists(r.db.WithContext(ctx), row)
}

func (r *DutyRosterRepository) SoftDeleteCrewByUser(ctx context.Context, rosterID []byte, roleCode, crewType string, userID, updatedBy []byte) error {
	return softReplaceCrewByUser(r.db.WithContext(ctx), rosterID, roleCode, crewType, userID, updatedBy)
}

func (r *DutyRosterRepository) FindRosterIDByBaseDate(ctx context.Context, baseID []byte, dutyDate time.Time) ([]byte, error) {
	date := dateOnlyUTC(dutyDate)
	var row dutyroster.DutyRoster
	err := r.db.WithContext(ctx).
		Where("base_id = ? AND duty_date = ? AND deleted_at IS NULL", baseID, date).
		Take(&row).Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}
	return row.ID, nil
}

func (r *DutyRosterRepository) FindRosterIDsByBaseDate(ctx context.Context, baseID []byte, dutyDate time.Time) ([][]byte, error) {
	return findRosterIDsByBaseAndDate(r.db.WithContext(ctx), baseID, dutyDate)
}

func (r *DutyRosterRepository) MatchCrewByFilter(ctx context.Context, rosterIDs [][]byte, crew dutyroster.DutyRosterCrew) ([]dutyroster.DeletedCrewResult, error) {
	return matchCrewByFilter(r.db.WithContext(ctx), rosterIDs, crew)
}

func (r *DutyRosterRepository) SoftDeleteCrewRows(ctx context.Context, rows []dutyroster.DeletedCrewResult, deletedBy []byte) error {
	return softDeleteCrewByIDs(r.db.WithContext(ctx), rows, time.Now().UTC(), deletedBy)
}

func (r *DutyRosterRepository) SoftDeleteCrewForDate(ctx context.Context, baseID []byte, dutyDate time.Time, crew dutyroster.DutyRosterCrew, updatedBy []byte) error {
	return softDeleteCrewForDate(r.db.WithContext(ctx), baseID, dutyDate, crew, time.Now().UTC(), updatedBy)
}

func (r *DutyRosterRepository) GetBaseDefaultShift(ctx context.Context, baseID []byte, baseType string) (string, error) {
	hasDefaultShiftTypes := r.hasBaseDefaultShiftTypeColumns()
	q := r.db.WithContext(ctx).
		Table("bases b").
		Where("b.id = ?", baseID)
	if hasDefaultShiftTypes {
		q = q.Select("b.default_start_time_type, b.default_end_time_type, b.default_shift_start, b.default_shift_end")
	} else {
		q = q.Select("b.default_shift_start, b.default_shift_end")
	}
	if strings.TrimSpace(baseType) != "" {
		q = q.Joins("JOIN base_categories bc ON bc.id = b.base_category_id").
			Where("bc.key = ?", rosterBaseCategoryKey(baseType))
	}
	var out struct {
		DefaultStartTimeType string
		DefaultEndTimeType   string
		DefaultShiftStart    string
		DefaultShiftEnd      string
	}
	err := q.Take(&out).Error
	if err != nil {
		return "", err
	}
	start := ""
	end := ""
	if !hasDefaultShiftTypes || normalizeBaseShiftTimeType(out.DefaultStartTimeType) == basedomain.ShiftTimeTypeFixed {
		start = strings.TrimSpace(out.DefaultShiftStart)
	}
	if !hasDefaultShiftTypes || normalizeBaseShiftTimeType(out.DefaultEndTimeType) == basedomain.ShiftTimeTypeFixed {
		end = strings.TrimSpace(out.DefaultShiftEnd)
	}
	if start != "" || end != "" {
		switch {
		case start != "" && end != "":
			return start + "-" + end, nil
		case start != "":
			return start, nil
		default:
			return end, nil
		}
	}
	return "", nil
}

func (r *DutyRosterRepository) GetHeaderByID(ctx context.Context, id []byte) (*dutyroster.HeaderView, error) {
	return r.GetHeaderByIDByBaseType(ctx, id, "hems")
}

func (r *DutyRosterRepository) GetHeaderByIDByBaseType(ctx context.Context, id []byte, baseType string) (*dutyroster.HeaderView, error) {
	categoryKey := rosterBaseCategoryKey(baseType)
	out := &dutyroster.HeaderView{}
	defaultShiftStartExpr := r.baseDefaultShiftDisplayExpr("hb", "default_shift_start", "default_start_time_type")
	defaultShiftEndExpr := r.baseDefaultShiftDisplayExpr("hb", "default_shift_end", "default_end_time_type")
	query := r.db.WithContext(ctx).
		Table("duty_rosters hdr").
		Select(`
				hdr.id,
				hdr.base_id AS base_id,
				hb.base AS base_name,
				hb.base_abbreviation,
				`+defaultShiftStartExpr+` AS default_shift_start,
				`+defaultShiftEndExpr+` AS default_shift_end,
				'' AS helicopter_id,
				'' AS helicopter_type,
				'' AS helicopter_designation,
				'' AS helicopter_identifier,
			hdr.duty_date,
			TIME_FORMAT(hdr.shift_start, '%H:%i:%s') AS shift_start,
			TIME_FORMAT(hdr.shift_end, '%H:%i:%s') AS shift_end,
				hdr.created_at,
				hdr.created_by,
				hdr.updated_at`).
		Joins("LEFT JOIN bases hb ON hb.id = hdr.base_id").
		Joins("LEFT JOIN base_categories bc ON bc.id = hb.base_category_id").
		Where("hdr.id = ? AND hdr.deleted_at IS NULL", id)
	err := query.Where("bc.key = ?", categoryKey).
		Where("hb.id IS NOT NULL").
		Take(out).Error
	if errors.Is(err, gorm.ErrRecordNotFound) || len(out.ID) == 0 {
		out = &dutyroster.HeaderView{}
		err = r.db.WithContext(ctx).
			Table("duty_rosters hdr").
			Select(`
				hdr.id,
				hdr.base_id AS base_id,
				hb.base AS base_name,
				hb.base_abbreviation,
				`+defaultShiftStartExpr+` AS default_shift_start,
				`+defaultShiftEndExpr+` AS default_shift_end,
				'' AS helicopter_id,
				'' AS helicopter_type,
				'' AS helicopter_designation,
				'' AS helicopter_identifier,
			hdr.duty_date,
			TIME_FORMAT(hdr.shift_start, '%H:%i:%s') AS shift_start,
			TIME_FORMAT(hdr.shift_end, '%H:%i:%s') AS shift_end,
			hdr.created_at,
			hdr.created_by,
			hdr.updated_at`).
			Joins("LEFT JOIN bases hb ON hb.id = hdr.base_id").
			Where("hdr.id = ? AND hdr.deleted_at IS NULL", id).
			Where("hb.id IS NOT NULL").
			Take(out).Error
	}
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}
	return out, nil
}

func (r *DutyRosterRepository) hasBaseDefaultShiftTypeColumns() bool {
	if r == nil || r.db == nil {
		return false
	}
	return r.db.Migrator().HasColumn("bases", "default_start_time_type") &&
		r.db.Migrator().HasColumn("bases", "default_end_time_type")
}

func (r *DutyRosterRepository) baseDefaultShiftDisplayExpr(alias, shiftColumn, typeColumn string) string {
	qualifiedShift := alias + "." + shiftColumn
	if !r.hasBaseDefaultShiftTypeColumns() {
		return "CASE WHEN " + qualifiedShift + " IS NULL THEN '' ELSE TIME_FORMAT(" + qualifiedShift + ", '%H:%i:%s') END"
	}
	qualifiedType := alias + "." + typeColumn
	return "CASE WHEN COALESCE(" + qualifiedType + ", 'FIXED') <> 'FIXED' OR " + qualifiedShift + " IS NULL THEN '' ELSE TIME_FORMAT(" + qualifiedShift + ", '%H:%i:%s') END"
}

func (r *DutyRosterRepository) GetCrewsByRosterIDs(ctx context.Context, rosterIDs [][]byte) ([]dutyroster.CrewView, error) {
	if len(rosterIDs) == 0 {
		return []dutyroster.CrewView{}, nil
	}
	rows := make([]dutyroster.CrewView, 0)
	err := r.db.WithContext(ctx).
		Table("duty_roster_crews c").
		Select(`
			c.id,
			c.roster_id,
			c.user_id,
			COALESCE(NULLIF(c.name_label, ''), TRIM(CONCAT(COALESCE(u.first_name, ''), ' ', COALESCE(u.last_name, ''))), '') AS name,
			COALESCE(NULLIF(r.name, ''), c.role_code) AS role,
			c.role_code,
			c.crew_type,
			COALESCE(NULLIF(c.mobile_phone, ''), u.mobile_phone, '') AS mobile_phone,
			COALESCE(NULLIF(c.email, ''), u.email, '') AS email,
			c.date_start,
			c.date_end,
			CASE
				WHEN c.shift_start IS NULL OR TIME_FORMAT(c.shift_start, '%H:%i:%s') = '00:00:00' THEN ''
				ELSE TIME_FORMAT(c.shift_start, '%H:%i:%s')
			END AS shift_start,
			CASE
				WHEN c.shift_end IS NULL OR TIME_FORMAT(c.shift_end, '%H:%i:%s') = '00:00:00' THEN ''
				ELSE TIME_FORMAT(c.shift_end, '%H:%i:%s')
			END AS shift_end,
			c.flight_instructor,
			c.line_checker,
			c.supervisor,
			c.examiner,
			c.co_pilot`).
		Joins("LEFT JOIN users u ON u.id = c.user_id").
		Joins("LEFT JOIN roles r ON r.id = u.role_id").
		Where("c.roster_id IN ? AND c.deleted_at IS NULL", rosterIDs).
		Order("c.role_code ASC, c.crew_type ASC, c.created_at ASC").
		Scan(&rows).Error
	if err != nil {
		return nil, err
	}
	return rows, nil
}

func (r *DutyRosterRepository) ListHeadersByRange(ctx context.Context, baseID []byte, from, to time.Time) ([]dutyroster.HeaderView, error) {
	return r.ListHeadersByRangeByBaseType(ctx, baseID, from, to, "hems")
}

func (r *DutyRosterRepository) ListHeadersByRangeByBaseType(ctx context.Context, baseID []byte, from, to time.Time, baseType string) ([]dutyroster.HeaderView, error) {
	categoryKey := rosterBaseCategoryKey(baseType)
	rows := make([]dutyroster.HeaderView, 0)
	q := r.db.WithContext(ctx).
		Table("duty_rosters hdr").
		Select(`
				hdr.id,
				hdr.base_id AS base_id,
				hb.base AS base_name,
				hb.base_abbreviation,
				'' AS helicopter_id,
				'' AS helicopter_type,
				'' AS helicopter_designation,
				'' AS helicopter_identifier,
			hdr.duty_date,
			TIME_FORMAT(hdr.shift_start, '%H:%i:%s') AS shift_start,
			TIME_FORMAT(hdr.shift_end, '%H:%i:%s') AS shift_end,
			hdr.created_at,
			hdr.created_by,
			hdr.updated_at`).
		Joins("LEFT JOIN bases hb ON hb.id = hdr.base_id").
		Joins("LEFT JOIN base_categories bc ON bc.id = hb.base_category_id").
		Where("hdr.deleted_at IS NULL AND hdr.duty_date BETWEEN ? AND ?", from, to).
		Where("bc.key = ?", categoryKey).
		Where("hb.id IS NOT NULL")
	if len(baseID) == 16 {
		q = q.Where("hdr.base_id = ?", baseID)
	}
	err := q.Order("hb.base ASC, hdr.duty_date ASC").Scan(&rows).Error
	if err != nil {
		return nil, err
	}
	return rows, nil
}

func (r *DutyRosterRepository) ListHEMSBases(ctx context.Context, baseID []byte) ([]dutyroster.BaseView, error) {
	return r.ListBasesByType(ctx, baseID, "hems")
}

func (r *DutyRosterRepository) ListBasesByType(ctx context.Context, baseID []byte, baseType string) ([]dutyroster.BaseView, error) {
	categoryKey := rosterBaseCategoryKey(baseType)
	rows := make([]dutyroster.BaseView, 0)
	q := r.db.WithContext(ctx).
		Table("bases b").
		Select(`
			b.id,
			b.base AS base_name,
			b.base_abbreviation`).
		Joins("JOIN base_categories bc ON bc.id = b.base_category_id").
		Where("b.is_active = ?", true).
		Where("bc.key = ?", categoryKey)

	if len(baseID) == 16 {
		q = q.Where("b.id = ?", baseID)
	}

	if err := q.Order("CASE WHEN b.sortkey IS NULL THEN 1 ELSE 0 END ASC, b.sortkey ASC, b.base ASC").Scan(&rows).Error; err != nil {
		return nil, err
	}
	return rows, nil
}

func rosterBaseCategoryKey(baseType string) string {
	switch strings.ToLower(strings.TrimSpace(baseType)) {
	case "base":
		return basedomain.CategoryKeyRegular
	default:
		return basedomain.CategoryKeyHEMS
	}
}

func dateOnlyUTC(t time.Time) time.Time {
	return time.Date(t.UTC().Year(), t.UTC().Month(), t.UTC().Day(), 0, 0, 0, 0, time.UTC)
}

func softReplaceCrewByUser(tx *gorm.DB, rosterID []byte, roleCode, crewType string, userID []byte, updatedBy []byte) error {
	if len(rosterID) == 0 || len(userID) == 0 {
		return nil
	}
	now := time.Now().UTC()
	return tx.Model(&dutyroster.DutyRosterCrew{}).
		Where("roster_id = ? AND role_code = ? AND crew_type = ? AND user_id = ? AND deleted_at IS NULL", rosterID, roleCode, crewType, userID).
		Updates(map[string]any{
			"deleted_at": now,
			"deleted_by": updatedBy,
			"updated_by": updatedBy,
		}).Error
}

func crewExists(tx *gorm.DB, row dutyroster.DutyRosterCrew) (bool, error) {
	q := tx.Model(&dutyroster.DutyRosterCrew{}).
		Where("roster_id = ? AND role_code = ? AND crew_type = ? AND deleted_at IS NULL", row.RosterID, row.RoleCode, row.CrewType).
		Where("name_label = ? AND mobile_phone = ? AND email = ?", row.NameLabel, row.MobilePhone, row.Email).
		Where("shift_start = ? AND shift_end = ?", row.ShiftStart, row.ShiftEnd)

	if len(row.UserID) == 0 {
		q = q.Where("user_id IS NULL")
	} else {
		q = q.Where("user_id = ?", row.UserID)
	}
	if row.DateStart == nil {
		q = q.Where("date_start IS NULL")
	} else {
		q = q.Where("date_start = ?", dateOnlyUTC(*row.DateStart))
	}
	if row.DateEnd == nil {
		q = q.Where("date_end IS NULL")
	} else {
		q = q.Where("date_end = ?", dateOnlyUTC(*row.DateEnd))
	}

	var count int64
	if err := q.Count(&count).Error; err != nil {
		return false, err
	}
	return count > 0, nil
}

func softDeleteCrewForDate(
	tx *gorm.DB,
	baseID []byte,
	dutyDate time.Time,
	crew dutyroster.DutyRosterCrew,
	now time.Time,
	updatedBy []byte,
) error {
	date := dateOnlyUTC(dutyDate)
	var rosterIDs [][]byte
	if err := tx.
		Table("duty_rosters").
		Select("id").
		Where("base_id = ? AND duty_date = ? AND deleted_at IS NULL", baseID, date).
		Scan(&rosterIDs).Error; err != nil {
		return err
	}
	if len(rosterIDs) == 0 {
		return nil
	}

	q := tx.Model(&dutyroster.DutyRosterCrew{}).
		Where("roster_id IN ? AND role_code = ? AND crew_type = ? AND deleted_at IS NULL", rosterIDs, crew.RoleCode, crew.CrewType).
		Where("name_label = ? AND mobile_phone = ? AND email = ? AND shift_start = ? AND shift_end = ?",
			crew.NameLabel,
			crew.MobilePhone,
			crew.Email,
			crew.ShiftStart,
			crew.ShiftEnd,
		)
	if len(crew.UserID) == 0 {
		q = q.Where("user_id IS NULL")
	} else {
		q = q.Where("user_id = ?", crew.UserID)
	}
	if crew.DateStart == nil {
		q = q.Where("date_start IS NULL")
	} else {
		q = q.Where("date_start = ?", dateOnlyUTC(*crew.DateStart))
	}
	if crew.DateEnd == nil {
		q = q.Where("date_end IS NULL")
	} else {
		q = q.Where("date_end = ?", dateOnlyUTC(*crew.DateEnd))
	}

	return q.Updates(map[string]any{
		"deleted_at": now,
		"deleted_by": updatedBy,
		"updated_by": updatedBy,
	}).Error
}

func findRosterIDsByBaseAndDate(tx *gorm.DB, baseID []byte, dutyDate time.Time) ([][]byte, error) {
	type rowID struct {
		ID []byte `gorm:"column:id"`
	}
	date := dateOnlyUTC(dutyDate)
	rows := make([]rowID, 0)
	if err := tx.
		Table("duty_rosters").
		Select("id").
		Where("base_id = ? AND duty_date = ? AND deleted_at IS NULL", baseID, date).
		Scan(&rows).Error; err != nil {
		return nil, err
	}
	out := make([][]byte, 0, len(rows))
	for i := range rows {
		out = append(out, rows[i].ID)
	}
	return out, nil
}

func softDeleteCrewByFilter(
	tx *gorm.DB,
	rosterIDs [][]byte,
	crew dutyroster.DutyRosterCrew,
	now time.Time,
	updatedBy []byte,
) error {
	if len(rosterIDs) == 0 {
		return nil
	}
	q := tx.Model(&dutyroster.DutyRosterCrew{}).
		Table("duty_roster_crews c").
		Where("c.roster_id IN ? AND c.role_code = ? AND c.deleted_at IS NULL", rosterIDs, crew.RoleCode)

	if strings.TrimSpace(crew.CrewType) != "" {
		q = q.Where("c.crew_type = ?", crew.CrewType)
	}
	if len(crew.UserID) > 0 {
		q = q.Where("c.user_id = ?", crew.UserID)
	} else if strings.TrimSpace(crew.NameLabel) != "" {
		q = q.Where("c.name_label = ?", crew.NameLabel)
	}
	if crew.DateStart != nil {
		q = q.Where("c.date_start = ?", dateOnlyUTC(*crew.DateStart))
	}
	if crew.DateEnd != nil {
		q = q.Where("c.date_end = ?", dateOnlyUTC(*crew.DateEnd))
	}
	if strings.TrimSpace(crew.ShiftStart) != "" {
		q = q.Where("c.shift_start = ?", crew.ShiftStart)
	}
	if strings.TrimSpace(crew.ShiftEnd) != "" {
		q = q.Where("c.shift_end = ?", crew.ShiftEnd)
	}

	return q.Updates(map[string]any{
		"deleted_at": now,
		"deleted_by": updatedBy,
		"updated_by": updatedBy,
	}).Error
}

func matchCrewByFilter(tx *gorm.DB, rosterIDs [][]byte, crew dutyroster.DutyRosterCrew) ([]dutyroster.DeletedCrewResult, error) {
	if len(rosterIDs) == 0 {
		return []dutyroster.DeletedCrewResult{}, nil
	}
	q := tx.Model(&dutyroster.DutyRosterCrew{}).
		Select(`
			c.id,
			c.roster_id,
			c.user_id,
			COALESCE(NULLIF(c.name_label, ''), TRIM(CONCAT(COALESCE(u.first_name, ''), ' ', COALESCE(u.last_name, ''))), '') AS name,
			c.role_code,
			c.crew_type,
			c.date_start,
			c.date_end`).
		Table("duty_roster_crews c").
		Joins("LEFT JOIN users u ON u.id = c.user_id").
		Where("roster_id IN ? AND role_code = ? AND deleted_at IS NULL", rosterIDs, crew.RoleCode)

	if strings.TrimSpace(crew.CrewType) != "" {
		q = q.Where("crew_type = ?", crew.CrewType)
	}
	if len(crew.UserID) > 0 {
		q = q.Where("user_id = ?", crew.UserID)
	} else if strings.TrimSpace(crew.NameLabel) != "" {
		q = q.Where("name_label = ?", crew.NameLabel)
	}
	if crew.DateStart != nil {
		q = q.Where("date_start = ?", dateOnlyUTC(*crew.DateStart))
	}
	if crew.DateEnd != nil {
		q = q.Where("date_end = ?", dateOnlyUTC(*crew.DateEnd))
	}
	if strings.TrimSpace(crew.ShiftStart) != "" {
		q = q.Where("shift_start = ?", crew.ShiftStart)
	}
	if strings.TrimSpace(crew.ShiftEnd) != "" {
		q = q.Where("shift_end = ?", crew.ShiftEnd)
	}

	out := make([]dutyroster.DeletedCrewResult, 0)
	if err := q.Scan(&out).Error; err != nil {
		return nil, err
	}
	return out, nil
}

func softDeleteCrewByIDs(tx *gorm.DB, rows []dutyroster.DeletedCrewResult, now time.Time, updatedBy []byte) error {
	if len(rows) == 0 {
		return nil
	}
	ids := make([][]byte, 0, len(rows))
	for i := range rows {
		ids = append(ids, rows[i].ID)
	}
	return tx.Model(&dutyroster.DutyRosterCrew{}).
		Where("id IN ? AND deleted_at IS NULL", ids).
		Updates(map[string]any{
			"deleted_at": now,
			"deleted_by": updatedBy,
			"updated_by": updatedBy,
		}).Error
}
