package service

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"strings"
	"time"

	dutyroster "wucher/internal/domain/duty_roster"
	"wucher/internal/shared/pkg/userctx"
	"wucher/internal/shared/pkg/uuidv7"
	requestdto "wucher/internal/transport/http/dto/request"
	responsedto "wucher/internal/transport/http/dto/response"
)

type DutyRosterService struct {
	repo dutyroster.Repository
}

type rangeAssignment struct {
	crew  dutyroster.DutyRosterCrew
	start time.Time
	end   time.Time
}

type DutyRosterValidationError struct {
	Status  int
	Title   string
	Pointer string
	Detail  string
}

func NewDutyRosterService(repo dutyroster.Repository) *DutyRosterService {
	return &DutyRosterService{repo: repo}
}

func (s *DutyRosterService) Create(ctx context.Context, row *dutyroster.DutyRoster, crews []dutyroster.DutyRosterCrew) error {
	normalizeDutyRosterWriteInput(row, crews)
	return s.repo.InTx(ctx, func(txRepo dutyroster.TxRepository) error {
		if err := txRepo.CreateHeader(ctx, row); err != nil {
			return err
		}

		insertRows := prepareCrewRowsForRosterInsert(row.ID, crews)
		for i := range insertRows {
			if err := txRepo.InsertCrew(ctx, &insertRows[i]); err != nil {
				return err
			}
		}
		return s.addRangeAssignments(ctx, txRepo, row, crews)
	})
}

func (s *DutyRosterService) Update(ctx context.Context, row *dutyroster.DutyRoster, crews []dutyroster.DutyRosterCrew) error {
	normalizeDutyRosterWriteInput(row, crews)
	return s.repo.InTx(ctx, func(txRepo dutyroster.TxRepository) error {
		if _, err := txRepo.LockRosterByID(ctx, row.ID); err != nil {
			return err
		}

		oldCrews := []dutyroster.DutyRosterCrew{}
		if len(crews) > 0 {
			var err error
			oldCrews, err = txRepo.ListActiveCrewsByRosterID(ctx, row.ID)
			if err != nil {
				return err
			}
		}

		if err := txRepo.UpdateHeader(ctx, row); err != nil {
			return err
		}
		if len(crews) == 0 {
			return nil
		}

		insertRows := prepareCrewRowsForRosterInsert(row.ID, crews)
		replacedKeys := map[string]struct{}{}
		for i := range insertRows {
			replaceKey := replaceByUserKey(insertRows[i].RosterID, insertRows[i].RoleCode, insertRows[i].CrewType, insertRows[i].UserID)
			if replaceKey != "" {
				if _, ok := replacedKeys[replaceKey]; !ok {
					if err := txRepo.SoftDeleteCrewByUser(ctx, insertRows[i].RosterID, insertRows[i].RoleCode, insertRows[i].CrewType, insertRows[i].UserID, row.UpdatedBy); err != nil {
						return err
					}
					replacedKeys[replaceKey] = struct{}{}
				}
			}
			exists, err := txRepo.CrewExists(ctx, insertRows[i])
			if err != nil {
				return err
			}
			if exists {
				continue
			}
			if err := txRepo.InsertCrew(ctx, &insertRows[i]); err != nil {
				return err
			}
		}
		if err := s.addRangeAssignments(ctx, txRepo, row, crews); err != nil {
			return err
		}
		return s.removeRangeAssignments(ctx, txRepo, row, oldCrews, crews)
	})
}

func (s *DutyRosterService) Delete(ctx context.Context, id []byte, deletedBy []byte) error {
	return s.repo.InTx(ctx, func(txRepo dutyroster.TxRepository) error {
		if _, err := txRepo.LockRosterByID(ctx, id); err != nil {
			return err
		}
		if err := txRepo.SoftDeleteHeader(ctx, id, deletedBy); err != nil {
			return err
		}
		return txRepo.SoftDeleteCrewsByRosterID(ctx, id, deletedBy)
	})
}

func (s *DutyRosterService) HardDelete(ctx context.Context, id []byte) error {
	return s.repo.InTx(ctx, func(txRepo dutyroster.TxRepository) error {
		if err := txRepo.HardDeleteCrewsByRosterID(ctx, id); err != nil {
			return err
		}
		return txRepo.HardDeleteHeader(ctx, id)
	})
}

func (s *DutyRosterService) DeleteCrews(ctx context.Context, id []byte, crews []dutyroster.DutyRosterCrew, deletedBy []byte) ([]dutyroster.DeletedCrewResult, error) {
	if len(crews) == 0 {
		return []dutyroster.DeletedCrewResult{}, nil
	}
	normalizeDutyRosterFilterInput(crews)
	collected := make(map[string]dutyroster.DeletedCrewResult)
	err := s.repo.InTx(ctx, func(txRepo dutyroster.TxRepository) error {
		header, err := txRepo.LockRosterByID(ctx, id)
		if err != nil {
			return err
		}
		baseDate := dateOnlyUTC(header.DutyDate)
		for i := range crews {
			dates := []time.Time{baseDate}
			if isRoleRangeAssignable(crews[i].RoleCode) && crews[i].DateStart != nil && crews[i].DateEnd != nil {
				dates = expandDateRange(*crews[i].DateStart, *crews[i].DateEnd)
			}
			for _, d := range dates {
				rosterIDs, err := txRepo.FindRosterIDsByBaseDate(ctx, header.BaseID, d)
				if err != nil {
					return err
				}
				if len(rosterIDs) == 0 {
					continue
				}
				rows, err := txRepo.MatchCrewByFilter(ctx, rosterIDs, crews[i])
				if err != nil {
					return err
				}
				if len(rows) == 0 {
					continue
				}
				for j := range rows {
					k := fmt.Sprintf("%x", rows[j].ID)
					if _, ok := collected[k]; !ok {
						collected[k] = rows[j]
					}
				}
				if err := txRepo.SoftDeleteCrewRows(ctx, rows, deletedBy); err != nil {
					return err
				}
			}
		}
		return nil
	})
	if err != nil {
		return nil, err
	}
	out := make([]dutyroster.DeletedCrewResult, 0, len(collected))
	for _, v := range collected {
		out = append(out, v)
	}
	return out, nil
}

func (s *DutyRosterService) GetBaseDefaultShift(ctx context.Context, baseID []byte, baseType string) (string, error) {
	return s.repo.GetBaseDefaultShift(ctx, baseID, baseType)
}

func (s *DutyRosterService) ResolveBaseDefaultShiftRange(ctx context.Context, baseID []byte, baseType string) (string, string, error) {
	if len(baseID) == 0 {
		return "", "", nil
	}
	raw, err := s.repo.GetBaseDefaultShift(ctx, baseID, baseType)
	if err != nil {
		return "", "", err
	}
	start, end := ParseDutyRosterShiftRange(raw)
	return start, end, nil
}

func (s *DutyRosterService) GetHeaderByID(ctx context.Context, id []byte) (*dutyroster.HeaderView, error) {
	return s.repo.GetHeaderByID(ctx, id)
}

func (s *DutyRosterService) GetHeaderByIDByBaseType(ctx context.Context, id []byte, baseType string) (*dutyroster.HeaderView, error) {
	return s.repo.GetHeaderByIDByBaseType(ctx, id, baseType)
}

func (s *DutyRosterService) GetCrewsByRosterIDs(ctx context.Context, rosterIDs [][]byte) ([]dutyroster.CrewView, error) {
	return s.repo.GetCrewsByRosterIDs(ctx, rosterIDs)
}

func (s *DutyRosterService) ListHeadersByRange(ctx context.Context, baseID []byte, from, to time.Time) ([]dutyroster.HeaderView, error) {
	return s.repo.ListHeadersByRange(ctx, baseID, from, to)
}

func (s *DutyRosterService) ListHeadersByRangeByBaseType(ctx context.Context, baseID []byte, from, to time.Time, baseType string) ([]dutyroster.HeaderView, error) {
	return s.repo.ListHeadersByRangeByBaseType(ctx, baseID, from, to, baseType)
}

func (s *DutyRosterService) ListHEMSBases(ctx context.Context, baseID []byte) ([]dutyroster.BaseView, error) {
	return s.repo.ListHEMSBases(ctx, baseID)
}

func (s *DutyRosterService) ListBasesByType(ctx context.Context, baseID []byte, baseType string) ([]dutyroster.BaseView, error) {
	return s.repo.ListBasesByType(ctx, baseID, baseType)
}

// ===== Write-path internals: normalization + persistence helpers =====
func normalizeDutyRosterWriteInput(row *dutyroster.DutyRoster, crews []dutyroster.DutyRosterCrew) {
	for i := range crews {
		crews[i].ShiftStart = normalizeCrewShiftForWrite(crews[i].ShiftStart)
		crews[i].ShiftEnd = normalizeCrewShiftForWrite(crews[i].ShiftEnd)
	}
}

func normalizeDutyRosterFilterInput(crews []dutyroster.DutyRosterCrew) {
	for i := range crews {
		crews[i].ShiftStart = normalizeCrewShiftForFilter(crews[i].ShiftStart)
		crews[i].ShiftEnd = normalizeCrewShiftForFilter(crews[i].ShiftEnd)
	}
}

func normalizeRosterShiftForWrite(v string) string {
	s := strings.TrimSpace(v)
	if s == "" {
		return ""
	}
	if len(s) == 8 {
		return "2000-01-01 " + s
	}
	if len(s) == 5 {
		return "2000-01-01 " + s + ":00"
	}
	return s
}

func normalizeCrewShiftForWrite(v string) string {
	s := strings.TrimSpace(v)
	if s == "" {
		return "2000-01-01 00:00:00"
	}
	return normalizeRosterShiftForWrite(s)
}

func normalizeCrewShiftForFilter(v string) string {
	s := strings.TrimSpace(v)
	if s == "" {
		return ""
	}
	return normalizeRosterShiftForWrite(s)
}

func prepareCrewRowsForRosterInsert(rosterID []byte, crews []dutyroster.DutyRosterCrew) []dutyroster.DutyRosterCrew {
	if len(crews) == 0 {
		return nil
	}
	out := make([]dutyroster.DutyRosterCrew, 0, len(crews))
	for i := range crews {
		row := crews[i]
		row.RosterID = append([]byte(nil), rosterID...)
		out = append(out, row)
	}
	return out
}

// Multi-day assignment orchestration.
func (s *DutyRosterService) addRangeAssignments(
	ctx context.Context,
	txRepo dutyroster.TxRepository,
	row *dutyroster.DutyRoster,
	crews []dutyroster.DutyRosterCrew,
) error {
	assignments := collectRangeAssignments(crews)
	if len(assignments) == 0 {
		return nil
	}
	currentDate := dateOnlyUTC(row.DutyDate)
	replacedKeys := map[string]struct{}{}
	for i := range assignments {
		dates := expandDateRange(assignments[i].start, assignments[i].end)
		for _, d := range dates {
			if d.Equal(currentDate) {
				continue
			}
			rosterID, err := s.ensureRosterForDate(ctx, txRepo, row, d)
			if err != nil {
				return err
			}
			insert := assignments[i].crew
			insert.RosterID = append([]byte(nil), rosterID...)
			replaceKey := replaceByUserKey(insert.RosterID, insert.RoleCode, insert.CrewType, insert.UserID)
			if replaceKey != "" {
				if _, ok := replacedKeys[replaceKey]; !ok {
					if err := txRepo.SoftDeleteCrewByUser(ctx, insert.RosterID, insert.RoleCode, insert.CrewType, insert.UserID, row.UpdatedBy); err != nil {
						return err
					}
					replacedKeys[replaceKey] = struct{}{}
				}
			}
			exists, err := txRepo.CrewExists(ctx, insert)
			if err != nil {
				return err
			}
			if exists {
				continue
			}
			if err := txRepo.InsertCrew(ctx, &insert); err != nil {
				return err
			}
		}
	}
	return nil
}

func (s *DutyRosterService) removeRangeAssignments(
	ctx context.Context,
	txRepo dutyroster.TxRepository,
	row *dutyroster.DutyRoster,
	oldCrews []dutyroster.DutyRosterCrew,
	newCrews []dutyroster.DutyRosterCrew,
) error {
	oldAssignments := collectRangeAssignments(oldCrews)
	if len(oldAssignments) == 0 {
		return nil
	}
	newAssignments := collectRangeAssignments(newCrews)
	newByKey := make(map[string][]rangeAssignment)
	for i := range newAssignments {
		k := assignmentKey(newAssignments[i].crew)
		newByKey[k] = append(newByKey[k], newAssignments[i])
	}
	for i := range oldAssignments {
		k := assignmentKey(oldAssignments[i].crew)
		keep := map[string]struct{}{}
		for _, n := range newByKey[k] {
			for _, d := range expandDateRange(n.start, n.end) {
				keep[d.Format("2006-01-02")] = struct{}{}
			}
		}
		for _, d := range expandDateRange(oldAssignments[i].start, oldAssignments[i].end) {
			if _, ok := keep[d.Format("2006-01-02")]; ok {
				continue
			}
			if err := txRepo.SoftDeleteCrewForDate(ctx, row.BaseID, d, oldAssignments[i].crew, row.UpdatedBy); err != nil {
				return err
			}
		}
	}
	return nil
}

func (s *DutyRosterService) ensureRosterForDate(
	ctx context.Context,
	txRepo dutyroster.TxRepository,
	source *dutyroster.DutyRoster,
	dutyDate time.Time,
) ([]byte, error) {
	date := dateOnlyUTC(dutyDate)
	id, err := txRepo.FindRosterIDByBaseDate(ctx, source.BaseID, date)
	if err != nil {
		return nil, err
	}
	if len(id) > 0 {
		if _, err := txRepo.LockRosterByID(ctx, id); err != nil {
			return nil, err
		}
		return id, nil
	}
	newRow := &dutyroster.DutyRoster{
		BaseID:    append([]byte(nil), source.BaseID...),
		DutyDate:  date,
		CreatedBy: append([]byte(nil), source.CreatedBy...),
		UpdatedBy: append([]byte(nil), source.UpdatedBy...),
	}
	if err := txRepo.CreateHeader(ctx, newRow); err != nil {
		id, fetchErr := txRepo.FindRosterIDByBaseDate(ctx, source.BaseID, date)
		if fetchErr == nil && len(id) > 0 {
			if _, lockErr := txRepo.LockRosterByID(ctx, id); lockErr != nil {
				return nil, lockErr
			}
			return id, nil
		}
		if fetchErr != nil {
			return nil, fetchErr
		}
		return nil, err
	}
	return newRow.ID, nil
}

func collectRangeAssignments(crews []dutyroster.DutyRosterCrew) []rangeAssignment {
	out := make([]rangeAssignment, 0)
	for i := range crews {
		c := crews[i]
		if !isRoleRangeAssignable(c.RoleCode) {
			continue
		}
		if c.DateStart == nil || c.DateEnd == nil {
			continue
		}
		start := dateOnlyUTC(*c.DateStart)
		end := dateOnlyUTC(*c.DateEnd)
		if end.Before(start) {
			continue
		}
		out = append(out, rangeAssignment{crew: c, start: start, end: end})
	}
	return out
}

func assignmentKey(c dutyroster.DutyRosterCrew) string {
	return fmt.Sprintf(
		"%s|%x|%s|%s",
		strings.ToLower(strings.TrimSpace(c.RoleCode)),
		c.UserID,
		strings.ToLower(strings.TrimSpace(c.CrewType)),
		strings.ToLower(strings.TrimSpace(c.NameLabel)),
	)
}

func replaceByUserKey(rosterID []byte, roleCode, crewType string, userID []byte) string {
	if len(rosterID) == 0 || len(userID) == 0 {
		return ""
	}
	return fmt.Sprintf(
		"%x|%s|%s|%x",
		rosterID,
		strings.ToLower(strings.TrimSpace(roleCode)),
		strings.ToLower(strings.TrimSpace(crewType)),
		userID,
	)
}

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 expandDateRange(start, end time.Time) []time.Time {
	start = dateOnlyUTC(start)
	end = dateOnlyUTC(end)
	if end.Before(start) {
		return nil
	}
	out := make([]time.Time, 0, int(end.Sub(start).Hours()/24)+1)
	for d := start; !d.After(end); d = d.AddDate(0, 0, 1) {
		out = append(out, d)
	}
	return out
}

func isRoleRangeAssignable(roleCode string) bool {
	switch strings.ToLower(strings.TrimSpace(roleCode)) {
	case "other_person", "other":
		return false
	default:
		return true
	}
}

// ===== Request parsing + validation helpers =====
func BuildDutyRosterCrewRows(details requestdto.DutyRosterDetailsInput, actor []byte) ([]dutyroster.DutyRosterCrew, *DutyRosterValidationError) {
	out := make([]dutyroster.DutyRosterCrew, 0)
	seen := map[string]struct{}{}

	appendNonPilotRole := func(roleCode string, items []requestdto.DutyRosterCrewInput) *DutyRosterValidationError {
		for _, in := range items {
			row, errObj := buildDutyRosterCrewRow(
				actor,
				roleCode,
				"main",
				in.ID,
				"",
				"",
				"",
				in.ShiftDate,
				true,
			)
			if errObj != nil {
				return errObj
			}
			if errObj := appendDutyRosterCrewRow(&out, seen, row); errObj != nil {
				return errObj
			}
		}
		return nil
	}
	appendOtherPersonRole := func(items []requestdto.DutyRosterOtherPersonInput) *DutyRosterValidationError {
		for _, in := range items {
			if strings.TrimSpace(in.Name) == "" {
				return &DutyRosterValidationError{
					Pointer: "/data/attributes/roster_detail/other_person",
					Detail:  "other_person name is required",
				}
			}
			row := dutyroster.DutyRosterCrew{
				RoleCode:    "other_person",
				CrewType:    "main",
				NameLabel:   strings.TrimSpace(in.Name),
				MobilePhone: strings.TrimSpace(in.MobilePhone),
				Email:       strings.TrimSpace(in.Email),
				ShiftStart:  "",
				ShiftEnd:    "",
				CreatedBy:   actor,
				UpdatedBy:   actor,
			}
			if errObj := appendDutyRosterCrewRow(&out, seen, row); errObj != nil {
				return errObj
			}
		}
		return nil
	}

	appendPilotRole := func(items []requestdto.DutyRosterPilotCrewInput) *DutyRosterValidationError {
		for _, in := range items {
			crewType := normalizeCrewType(in.CrewType)
			row, errObj := buildDutyRosterCrewRow(
				actor,
				"pilot",
				crewType,
				in.ID,
				"",
				"",
				"",
				in.ShiftDate,
				true,
			)
			if errObj != nil {
				return errObj
			}
			if crewType == "additional" {
				row.FlightInstructor = in.FlightInstructor
				row.LineChecker = in.LineChecker
				row.Supervisor = in.Supervisor
				row.Examiner = in.Examiner
				row.CoPilot = in.CoPilot
			}
			if errObj := appendDutyRosterCrewRow(&out, seen, row); errObj != nil {
				return errObj
			}
		}
		return nil
	}

	if err := appendPilotRole(details.Pilot); err != nil {
		return nil, err
	}
	if err := appendNonPilotRole("doctor", details.Doctor); err != nil {
		return nil, err
	}
	if err := appendNonPilotRole("rescuer", details.Rescuer); err != nil {
		return nil, err
	}
	if err := appendNonPilotRole("helper", details.Helper); err != nil {
		return nil, err
	}
	if err := appendOtherPersonRole(details.OtherPerson); err != nil {
		return nil, err
	}
	return out, nil
}

func buildDutyRosterCrewRow(
	actor []byte,
	roleCode string,
	crewType string,
	id string,
	name string,
	mobilePhone string,
	email string,
	shiftDate *requestdto.DutyRosterShiftDate,
	allowUserID bool,
) (dutyroster.DutyRosterCrew, *DutyRosterValidationError) {
	row := dutyroster.DutyRosterCrew{
		RoleCode:    roleCode,
		CrewType:    crewType,
		NameLabel:   strings.TrimSpace(name),
		MobilePhone: strings.TrimSpace(mobilePhone),
		Email:       strings.TrimSpace(email),
		ShiftStart:  "",
		ShiftEnd:    "",
		CreatedBy:   actor,
		UpdatedBy:   actor,
	}
	if allowUserID && strings.TrimSpace(id) != "" {
		parsedID, err := uuidv7.ParseString(strings.TrimSpace(id))
		if err != nil {
			return dutyroster.DutyRosterCrew{}, &DutyRosterValidationError{
				Pointer: "/data/attributes/roster_detail",
				Detail:  "crew id is invalid UUID",
			}
		}
		row.UserID = parsedID
	}
	if errObj := applyCrewShiftDateInput(&row, shiftDate); errObj != nil {
		return dutyroster.DutyRosterCrew{}, errObj
	}
	return row, nil
}

func appendDutyRosterCrewRow(out *[]dutyroster.DutyRosterCrew, seen map[string]struct{}, row dutyroster.DutyRosterCrew) *DutyRosterValidationError {
	dupKey := buildDutyRosterCrewDupKey(row)
	if _, ok := seen[dupKey]; ok {
		return &DutyRosterValidationError{
			Pointer: "/data/attributes/roster_detail",
			Detail:  "duplicate crew assignment in request payload",
		}
	}
	seen[dupKey] = struct{}{}
	*out = append(*out, row)
	return nil
}

func validateDutyRosterDeleteCrews(crews []dutyroster.DutyRosterCrew) *DutyRosterValidationError {
	for i := range crews {
		if !roleNeedsShiftDateForDelete(crews[i].RoleCode) {
			continue
		}
		hasStart := crews[i].DateStart != nil
		hasEnd := crews[i].DateEnd != nil
		if hasStart != hasEnd {
			return &DutyRosterValidationError{
				Pointer: "/data/attributes/roster_detail",
				Detail:  "shift_date.date_start and shift_date.date_end must be provided together",
			}
		}
		if hasStart && crews[i].DateEnd.Before(*crews[i].DateStart) {
			return &DutyRosterValidationError{
				Pointer: "/data/attributes/roster_detail",
				Detail:  "shift_date.date_end must be >= shift_date.date_start",
			}
		}
	}
	return nil
}

func applyCrewShiftDateInput(row *dutyroster.DutyRosterCrew, in *requestdto.DutyRosterShiftDate) *DutyRosterValidationError {
	if in == nil {
		return nil
	}
	if strings.TrimSpace(in.DateStart) != "" {
		d, err := parseDateOnlyForCrew(in.DateStart)
		if err != nil {
			return &DutyRosterValidationError{
				Pointer: "/data/attributes/roster_detail",
				Detail:  "ERR_DUTY_ROSTER_SHIFT_DATE_START_FORMAT",
			}
		}
		row.DateStart = &d
	}
	if strings.TrimSpace(in.DateEnd) != "" {
		d, err := parseDateOnlyForCrew(in.DateEnd)
		if err != nil {
			return &DutyRosterValidationError{
				Pointer: "/data/attributes/roster_detail",
				Detail:  "ERR_DUTY_ROSTER_SHIFT_DATE_END_FORMAT",
			}
		}
		row.DateEnd = &d
	}
	return nil
}

func buildDutyRosterCrewDupKey(row dutyroster.DutyRosterCrew) string {
	return strings.ToLower(strings.TrimSpace(row.RoleCode)) + "|" +
		strings.ToLower(strings.TrimSpace(row.CrewType)) + "|" +
		uuidNoCtx(row.UserID) + "|" +
		strings.ToLower(strings.TrimSpace(row.NameLabel)) + "|" +
		dateToString(row.DateStart) + "|" +
		dateToString(row.DateEnd) + "|" +
		strings.TrimSpace(row.ShiftStart) + "|" +
		strings.TrimSpace(row.ShiftEnd)
}

func normalizeClockOrDefaultForCrew(v, def string) string {
	s := strings.TrimSpace(v)
	if s == "" {
		return def
	}
	if len(s) == 5 {
		return s + ":00"
	}
	return s
}

func normalizeClockInputForCrew(v, def, pointer string) (string, *DutyRosterValidationError) {
	out := normalizeClockOrDefaultForCrew(v, def)
	if strings.TrimSpace(out) == "" {
		return out, nil
	}
	if _, err := time.Parse("15:04:05", out); err != nil {
		return "", &DutyRosterValidationError{
			Pointer: pointer,
			Detail:  "time must be HH:MM or HH:MM:SS",
		}
	}
	return out, nil
}

func NormalizeDutyRosterClockInput(v, def, pointer string) (string, *DutyRosterValidationError) {
	return normalizeClockInputForCrew(v, def, pointer)
}

// ===== Date/time parsing helpers used by handlers =====
func ParseDutyRosterShiftRange(raw string) (string, string) {
	tokens := extractClockTokensForShift(raw)
	if len(tokens) == 0 {
		return "", ""
	}
	start := normalizeClockOrDefaultForCrew(tokens[0], "")
	if _, err := time.Parse("15:04:05", start); err != nil {
		start = ""
	}
	end := start
	if len(tokens) > 1 {
		end = normalizeClockOrDefaultForCrew(tokens[1], "")
		if _, err := time.Parse("15:04:05", end); err != nil {
			end = ""
		}
	}
	return start, end
}

func ResolveDutyRosterShiftDefaultsForUpdate(ctx context.Context, svc dutyroster.Service, baseID []byte, baseType, currentShiftStart, currentShiftEnd string) (string, string, error) {
	defaultShiftStart := strings.TrimSpace(currentShiftStart)
	defaultShiftEnd := strings.TrimSpace(currentShiftEnd)
	if defaultShiftStart != "" && defaultShiftEnd != "" {
		return defaultShiftStart, defaultShiftEnd, nil
	}

	baseShiftStart, baseShiftEnd, err := svc.ResolveBaseDefaultShiftRange(ctx, baseID, baseType)
	if err != nil {
		return "", "", err
	}
	if defaultShiftStart == "" {
		defaultShiftStart = baseShiftStart
	}
	if defaultShiftEnd == "" {
		defaultShiftEnd = baseShiftEnd
	}
	return defaultShiftStart, defaultShiftEnd, nil
}

func extractClockTokensForShift(raw string) []string {
	s := strings.TrimSpace(raw)
	if s == "" {
		return nil
	}
	out := make([]string, 0, 2)
	for i := 0; i < len(s) && len(out) < 2; i++ {
		if i+5 > len(s) || !isDigitForShift(s[i]) || !isDigitForShift(s[i+1]) || s[i+2] != ':' || !isDigitForShift(s[i+3]) || !isDigitForShift(s[i+4]) {
			continue
		}
		token := s[i : i+5]
		if i+8 <= len(s) && s[i+5] == ':' && isDigitForShift(s[i+6]) && isDigitForShift(s[i+7]) {
			token = s[i : i+8]
		}
		out = append(out, token)
	}
	return out
}

func isDigitForShift(b byte) bool {
	return b >= '0' && b <= '9'
}

func ParseDutyRosterDateRangeOrDefault(fromRaw, toRaw string, now time.Time) (time.Time, time.Time, *DutyRosterValidationError) {
	defaultFrom := time.Date(now.UTC().Year(), now.UTC().Month(), 1, 0, 0, 0, 0, time.UTC)
	defaultTo := defaultFrom.AddDate(0, 1, -1)

	fromText := strings.TrimSpace(fromRaw)
	toText := strings.TrimSpace(toRaw)
	from := defaultFrom
	to := defaultTo

	if fromText != "" {
		parsedFrom, err := parseDateOnlyForCrew(fromText)
		if err != nil {
			return time.Time{}, time.Time{}, &DutyRosterValidationError{
				Pointer: "/query/from",
				Detail:  "from must be YYYY-MM-DD",
			}
		}
		from = parsedFrom
	}
	if toText != "" {
		parsedTo, err := parseDateOnlyForCrew(toText)
		if err != nil {
			return time.Time{}, time.Time{}, &DutyRosterValidationError{
				Pointer: "/query/to",
				Detail:  "to must be YYYY-MM-DD",
			}
		}
		to = parsedTo
	}
	return from, to, nil
}

func ParseDutyRosterDateOnly(v string) (time.Time, error) {
	return parseDateOnlyForCrew(v)
}

// ===== Request body decoder helpers =====
func BuildDutyRosterDeleteCrews(raw []byte, actor []byte) ([]dutyroster.DutyRosterCrew, *DutyRosterValidationError) {
	trimmed := bytes.TrimSpace(raw)
	if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("{}")) {
		return nil, nil
	}
	var req struct {
		Data struct {
			Attributes struct {
				Detail *requestdto.DutyRosterDetailsInput `json:"roster_detail,omitempty"`
			} `json:"attributes"`
		} `json:"data"`
	}
	if err := json.Unmarshal(trimmed, &req); err != nil {
		return nil, &DutyRosterValidationError{
			Pointer: "/data",
			Detail:  "invalid delete payload",
		}
	}
	detail := req.Data.Attributes.Detail
	if detail == nil {
		return nil, nil
	}
	crews, crewErr := BuildDutyRosterCrewRows(*detail, actor)
	if crewErr != nil {
		return nil, crewErr
	}
	if errObj := validateDutyRosterDeleteCrews(crews); errObj != nil {
		return nil, errObj
	}
	return crews, nil
}

func isDutyRosterDetailsInputEmpty(d requestdto.DutyRosterDetailsInput) bool {
	return len(d.Pilot) == 0 &&
		len(d.Doctor) == 0 &&
		len(d.Rescuer) == 0 &&
		len(d.Helper) == 0 &&
		len(d.OtherPerson) == 0
}

func parseDateOnlyForCrew(v string) (time.Time, error) {
	return time.ParseInLocation("2006-01-02", strings.TrimSpace(v), time.UTC)
}

func normalizeCrewType(v string) string {
	s := strings.ToLower(strings.TrimSpace(v))
	if s == "additional" {
		return "additional"
	}
	return "main"
}

func roleNeedsShiftDateForDelete(roleCode string) bool {
	switch strings.ToLower(strings.TrimSpace(roleCode)) {
	case "other_person", "other":
		return false
	default:
		return true
	}
}

func dateToString(t *time.Time) string {
	if t == nil || t.IsZero() {
		return ""
	}
	return t.UTC().Format("2006-01-02")
}

// uuidNoCtx converts a UUID byte slice to string without any context-based lookup.
// Used only for internal deduplication keys where user names are not needed.
func uuidNoCtx(id []byte) string {
	if len(id) == 0 {
		return ""
	}
	s, err := uuidv7.BytesToString(id)
	if err != nil {
		return ""
	}
	return s
}

func uuidStringOrEmpty(ctx context.Context, id []byte) string {
	if len(id) == 0 {
		return ""
	}
	// Try GlobalUserNameGetter first.
	if name := getLastNameFromServiceGetter(ctx, id); name != "" {
		return name
	}
	return uuidNoCtx(id)
}

func getLastNameFromServiceGetter(ctx context.Context, id []byte) string {
	if name := userctx.GetDisplayName(ctx, id); name != "" {
		return name
	}
	return ""
}

// ===== DTO/resource mapping helpers =====
func DutyRosterResource(ctx context.Context, header dutyroster.HeaderView, crews []dutyroster.CrewView) responsedto.DutyRosterResource {
	details := responsedto.DutyRosterDetailsResponse{
		Pilot:       []responsedto.DutyRosterPilotCrew{},
		Doctor:      []responsedto.DutyRosterCrew{},
		Rescuer:     []responsedto.DutyRosterCrew{},
		Helper:      []responsedto.DutyRosterCrew{},
		OtherPerson: []responsedto.DutyRosterCrew{},
	}

	for i := range crews {
		baseItem := responsedto.DutyRosterCrew{
			ID:          firstNonEmpty(uuidNoCtx(crews[i].UserID), uuidNoCtx(crews[i].ID)),
			Name:        strings.TrimSpace(crews[i].Name),
			Role:        strings.TrimSpace(crews[i].Role),
			MobilePhone: strings.TrimSpace(crews[i].MobilePhone),
			Email:       strings.TrimSpace(crews[i].Email),
		}
		if crews[i].DateStart != nil || crews[i].DateEnd != nil {
			baseItem.ShiftDate = &responsedto.DutyRosterShiftDate{DateStart: dateToString(crews[i].DateStart), DateEnd: dateToString(crews[i].DateEnd)}
		}
		switch roleBucket(crews[i].RoleCode) {
		case "pilot":
			applyEffectiveCrewShiftTime(&baseItem, crews[i], header)
			pilotItem := responsedto.DutyRosterPilotCrew{
				ID:          baseItem.ID,
				Name:        baseItem.Name,
				Role:        baseItem.Role,
				CrewType:    strings.TrimSpace(crews[i].CrewType),
				MobilePhone: baseItem.MobilePhone,
				Email:       baseItem.Email,
				ShiftDate:   baseItem.ShiftDate,
			}
			if strings.EqualFold(strings.TrimSpace(crews[i].CrewType), "additional") {
				pilotItem.FlightInstructor = boolPtrIfTrue(crews[i].FlightInstructor)
				pilotItem.LineChecker = boolPtrIfTrue(crews[i].LineChecker)
				pilotItem.Supervisor = boolPtrIfTrue(crews[i].Supervisor)
				pilotItem.Examiner = boolPtrIfTrue(crews[i].Examiner)
				pilotItem.CoPilot = boolPtrIfTrue(crews[i].CoPilot)
			}
			details.Pilot = append(details.Pilot, pilotItem)
		case "doctor":
			applyEffectiveCrewShiftTime(&baseItem, crews[i], header)
			details.Doctor = append(details.Doctor, baseItem)
		case "rescuer":
			applyEffectiveCrewShiftTime(&baseItem, crews[i], header)
			details.Rescuer = append(details.Rescuer, baseItem)
		case "helper":
			applyEffectiveCrewShiftTime(&baseItem, crews[i], header)
			details.Helper = append(details.Helper, baseItem)
		default:
			baseItem.ShiftDate = nil
			baseItem.ShiftTime = nil
			details.OtherPerson = append(details.OtherPerson, baseItem)
		}
	}

	return responsedto.DutyRosterResource{
		Type: "duty_roster",
		ID:   uuidNoCtx(header.ID),
		Attributes: responsedto.DutyRosterAttributes{
			Bases: responsedto.DutyRosterBaseInfo{
				ID:               uuidNoCtx(header.BaseID),
				Type:             "hems",
				BaseName:         header.BaseName, 
				BaseAbbreviation: header.BaseAbbreviation,
			},
			Helicopter: buildDutyRosterHelicopterFromHeader(header),
			DutyDate:   header.DutyDate.UTC().Format("2006-01-02"),
			ShiftTime:  responsedto.DutyRosterShiftTime{ShiftStart: header.ShiftStart, ShiftEnd: header.ShiftEnd},
			Detail:     details,
			CreatedBy:  uuidStringOrEmpty(ctx, header.CreatedBy),
			CreatedAt:  header.CreatedAt.UTC().Format(time.RFC3339),
			UpdatedAt:  header.UpdatedAt.UTC().Format(time.RFC3339),
		},
	}
}

func applyEffectiveCrewShiftTime(baseItem *responsedto.DutyRosterCrew, crew dutyroster.CrewView, header dutyroster.HeaderView) {
	effectiveShiftStart := strings.TrimSpace(crew.ShiftStart)
	effectiveShiftEnd := strings.TrimSpace(crew.ShiftEnd)
	if effectiveShiftStart == "" {
		effectiveShiftStart = header.ShiftStart
	}
	if effectiveShiftEnd == "" {
		effectiveShiftEnd = header.ShiftEnd
	}
	if effectiveShiftStart == "" && effectiveShiftEnd == "" {
		return
	}
	baseItem.ShiftTime = &responsedto.DutyRosterShiftTime{ShiftStart: effectiveShiftStart, ShiftEnd: effectiveShiftEnd}
}

func buildDutyRosterHelicopterFromHeader(header dutyroster.HeaderView) responsedto.DutyRosterHelicopterSummary {
	id := uuidNoCtx(header.HelicopterID)
	typ := strings.TrimSpace(header.HelicopterType)
	designation := strings.TrimSpace(header.HelicopterDesignation)
	identifier := strings.TrimSpace(header.HelicopterIdentifier)
	return responsedto.DutyRosterHelicopterSummary{
		ID:          id,
		Designation: designation,
		Identifier:  identifier,
		Type:        typ,
	}
}

func DutyRosterDeletedCrewItems(rows []dutyroster.DeletedCrewResult) []map[string]any {
	out := make([]map[string]any, 0, len(rows))
	for i := range rows {
		out = append(out, map[string]any{
			"id":         uuidNoCtx(rows[i].ID),
			"roster_id":  uuidNoCtx(rows[i].RosterID),
			"user_id":    uuidNoCtx(rows[i].UserID),
			"name":       strings.TrimSpace(rows[i].Name),
			"role_code":  strings.TrimSpace(rows[i].RoleCode),
			"crew_type":  strings.TrimSpace(rows[i].CrewType),
			"date_start": dateToString(rows[i].DateStart),
			"date_end":   dateToString(rows[i].DateEnd),
		})
	}
	return out
}

func DutyRosterCrewAuditMetadata(rosterID []byte, baseType string, crews []dutyroster.DutyRosterCrew) map[string]any {
	items := make([]map[string]any, 0, len(crews))
	for i := range crews {
		items = append(items, map[string]any{
			"user_id":    uuidNoCtx(crews[i].UserID),
			"name_label": strings.TrimSpace(crews[i].NameLabel),
			"role_code":  strings.TrimSpace(crews[i].RoleCode),
			"crew_type":  strings.TrimSpace(crews[i].CrewType),
			"date_start": dateToString(crews[i].DateStart),
			"date_end":   dateToString(crews[i].DateEnd),
		})
	}
	return map[string]any{
		"roster_id":   uuidNoCtx(rosterID),
		"base_type":   strings.TrimSpace(baseType),
		"crew_count":  len(crews),
		"crew_detail": items,
	}
}

// ===== Grouping/filtering/pagination helpers for list endpoints =====
func GroupRostersByBase(items []responsedto.DutyRosterResource) []responsedto.DutyRosterBaseGroupResource {
	return groupRosterResourcesByBase(
		items,
		func(item responsedto.DutyRosterResource) string { return item.Attributes.Bases.ID },
		func(item responsedto.DutyRosterResource) responsedto.DutyRosterBaseGroupResource {
			return responsedto.DutyRosterBaseGroupResource{
				Type: "hems_base_roster_group",
				Attributes: responsedto.DutyRosterBaseGroupAttributes{
					Bases:   item.Attributes.Bases,
					Rosters: []responsedto.DutyRosterGroupRoster{},
				},
			}
		},
		func(group *responsedto.DutyRosterBaseGroupResource, item responsedto.DutyRosterResource) {
			group.Attributes.Rosters = append(group.Attributes.Rosters, responsedto.DutyRosterGroupRoster{
				ID:         item.ID,
				FlightID:   item.Attributes.FlightID,
				Helicopter: item.Attributes.Helicopter,
				DutyDate:   item.Attributes.DutyDate,
				ShiftTime:  item.Attributes.ShiftTime,
				Detail:     item.Attributes.Detail,
				CreatedBy:  item.Attributes.CreatedBy,
				CreatedAt:  item.Attributes.CreatedAt,
				UpdatedAt:  item.Attributes.UpdatedAt,
			})
		},
	)
}

func groupRosterResourcesByBase[T any, G any](
	items []T,
	getBaseID func(item T) string,
	newGroup func(item T) G,
	appendToGroup func(group *G, item T),
) []G {
	out := make([]G, 0)
	indexByBase := map[string]int{}
	for i := range items {
		baseKey := normalizeRosterBaseGroupKey(getBaseID(items[i]))
		idx, ok := indexByBase[baseKey]
		if !ok {
			out = append(out, newGroup(items[i]))
			idx = len(out) - 1
			indexByBase[baseKey] = idx
		}
		appendToGroup(&out[idx], items[i])
	}
	return out
}

func normalizeRosterBaseGroupKey(baseID string) string {
	s := strings.TrimSpace(baseID)
	if s == "" {
		return "unknown"
	}
	return s
}

func MergeEmptyBaseGroups(groups []responsedto.DutyRosterBaseGroupResource, bases []dutyroster.BaseView) []responsedto.DutyRosterBaseGroupResource {
	indexByBase := make(map[string]int, len(groups))
	for i := range groups {
		baseID := strings.TrimSpace(groups[i].Attributes.Bases.ID)
		if baseID == "" || baseID == "unknown" {
			continue
		}
		indexByBase[baseID] = i
	}
	for i := range bases {
		baseID := uuidNoCtx(bases[i].ID)
		if baseID == "" {
			continue
		}
		if _, exists := indexByBase[baseID]; exists {
			continue
		}
		groups = append(groups, responsedto.DutyRosterBaseGroupResource{
			Type: "hems_base_roster_group",
			Attributes: responsedto.DutyRosterBaseGroupAttributes{
				Bases: responsedto.DutyRosterBaseInfo{
					ID:               baseID,
					Type:             "hems",
					BaseName:         strings.TrimSpace(bases[i].BaseName),
					BaseAbbreviation: strings.TrimSpace(bases[i].BaseAbbreviation),
				},
				Rosters: []responsedto.DutyRosterGroupRoster{},
			},
		})
	}
	return groups
}

func LoadHEMSRosterGroups(ctx context.Context, svc dutyroster.Service, baseID []byte, from, to time.Time, baseName string) ([]responsedto.DutyRosterBaseGroupResource, error) {
	headers, err := svc.ListHeadersByRange(ctx, baseID, from, to)
	if err != nil {
		return nil, err
	}
	bases, err := svc.ListHEMSBases(ctx, baseID)
	if err != nil {
		return nil, err
	}
	rosterIDs := make([][]byte, 0, len(headers))
	for i := range headers {
		rosterIDs = append(rosterIDs, headers[i].ID)
	}
	crews, err := svc.GetCrewsByRosterIDs(ctx, rosterIDs)
	if err != nil {
		return nil, err
	}

	byRoster := make(map[string][]dutyroster.CrewView)
	for i := range crews {
		k, _ := uuidv7.BytesToString(crews[i].RosterID)
		byRoster[k] = append(byRoster[k], crews[i])
	}
	out := make([]responsedto.DutyRosterResource, 0, len(headers))
	for i := range headers {
		k, _ := uuidv7.BytesToString(headers[i].ID)
		out = append(out, DutyRosterResource(ctx, headers[i], byRoster[k]))
	}
	grouped := MergeEmptyBaseGroups(GroupRostersByBase(out), bases)

	needle := strings.ToLower(strings.TrimSpace(baseName))
	if needle == "" {
		return grouped, nil
	}
	filtered := make([]responsedto.DutyRosterBaseGroupResource, 0, len(grouped))
	for i := range grouped {
		name := strings.ToLower(strings.TrimSpace(grouped[i].Attributes.Bases.BaseName))
		if strings.Contains(name, needle) {
			filtered = append(filtered, grouped[i])
		}
	}
	return filtered, nil
}

func PaginateHEMSRosterGroups(groups []responsedto.DutyRosterBaseGroupResource, start, length int) ([]responsedto.DutyRosterBaseGroupResource, int) {
	total := len(groups)
	if start > total {
		start = total
	}
	end := start + length
	if end > total {
		end = total
	}
	return groups[start:end], total
}

// ===== Small generic helpers =====
func roleBucket(code string) string {
	s := strings.ToLower(strings.TrimSpace(code))
	switch s {
	case "pilot":
		return "pilot"
	case "doctor":
		return "doctor"
	case "air_rescuer", "rescuer":
		return "rescuer"
	case "flight_assistant", "helper":
		return "helper"
	case "other", "other_person":
		return "other_person"
	default:
		return "other_person"
	}
}

func firstNonEmpty(a, b string) string {
	if strings.TrimSpace(a) != "" {
		return a
	}
	return b
}

func boolPtrIfTrue(v bool) *bool {
	if !v {
		return nil
	}
	b := true
	return &b
}
