package handlers

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"log/slog"
	"strconv"
	"strings"
	"time"

	"github.com/gofiber/fiber/v2"
	"gorm.io/gorm"

	authdomain "wucher/internal/domain/auth"
	basedomain "wucher/internal/domain/base"
	beforeflightinspection "wucher/internal/domain/before_flight_inspection"
	dutyroster "wucher/internal/domain/duty_roster"
	filemanager "wucher/internal/domain/file_manager"
	fleetstatus "wucher/internal/domain/fleet_status"
	"wucher/internal/domain/flight"
	flightinspection "wucher/internal/domain/flight_inspection"
	flightinspectionfilechecklist "wucher/internal/domain/flight_inspection_file_checklist"
	flightprepcheck "wucher/internal/domain/flight_prep_check"
	"wucher/internal/domain/helicopter"
	helicopterfile "wucher/internal/domain/helicopter_file"
	reserveac "wucher/internal/domain/reserve_ac"
	takeover "wucher/internal/domain/takeover"
	"wucher/internal/repository/mysql"
	"wucher/internal/service"
	"wucher/internal/shared/pkg/appctx"
	"wucher/internal/shared/pkg/apperrorsx"
	"wucher/internal/shared/pkg/userctx"
	"wucher/internal/shared/pkg/uuidv7"
	requestdto "wucher/internal/transport/http/dto/request"
	responsedto "wucher/internal/transport/http/dto/response"
	shareddto "wucher/internal/transport/http/dto/shared"
	"wucher/internal/transport/http/jsonapi"
	"wucher/internal/transport/http/response"
	"wucher/internal/transport/http/validators"
)

// takeoverHelicopterChecker exposes the helicopter availability check the
// takeover handler needs to reject already-booked helicopters.
type takeoverHelicopterChecker interface {
	IsInUse(ctx context.Context, id []byte) (bool, error)
	GetByID(ctx context.Context, id []byte) (*helicopter.Helicopter, error)
}

type TakeoverHandler struct {
	flight       flight.FlightService
	base         basedomain.Service
	reserveAc    reserveac.ReserveAcService
	dutyRoster   dutyroster.Service
	db           *gorm.DB
	takeoverAc   *service.TakeoverModuleService
	inspect      flightinspection.FlightInspectionService
	before       beforeflightinspection.Service
	prep         flightprepcheck.Service
	heliFile     helicopterfile.Service
	fileCk       flightinspectionfilechecklist.Service
	fileManager  filemanager.Service
	helicopter   takeoverHelicopterChecker
	fleetStatus  helicopterFleetStatusProvider
	complaintSvc complaintGroundingLister
	mcfSvc       mcfPendingChecker
	storage      fileManagerDownloadURLStorage
	auth         takeoverAuthService
	validate     *validators.Validator
}

type takeoverAuthService interface {
	GetUserByID(ctx context.Context, id []byte) (*authdomain.User, error)
}

func NewTakeoverHandler(db *gorm.DB) *TakeoverHandler {
	return &TakeoverHandler{
		db:       db,
		validate: validators.New(),
	}
}

func (h *TakeoverHandler) WithDB(db *gorm.DB) *TakeoverHandler {
	h.db = db
	return h
}

func (h *TakeoverHandler) takeoverService() *service.TakeoverService {
	if h != nil && h.db != nil {
		return service.NewTakeoverService(h.db).
			WithChecklistTemplateRepository(mysql.NewTakeoverRepository(h.db))
	}
	return nil
}

func (h *TakeoverHandler) WithFlightService(flightSvc flight.FlightService) *TakeoverHandler {
	h.flight = flightSvc
	return h
}

func (h *TakeoverHandler) WithAuthService(authSvc takeoverAuthService) *TakeoverHandler {
	h.auth = authSvc
	return h
}

func (h *TakeoverHandler) WithBaseService(baseSvc basedomain.Service) *TakeoverHandler {
	h.base = baseSvc
	return h
}

func (h *TakeoverHandler) WithReserveAcService(reserveAcSvc reserveac.ReserveAcService) *TakeoverHandler {
	h.reserveAc = reserveAcSvc
	return h
}

func (h *TakeoverHandler) WithDutyRosterService(dutyRosterSvc dutyroster.Service) *TakeoverHandler {
	h.dutyRoster = dutyRosterSvc
	return h
}

func (h *TakeoverHandler) WithFleetStatusService(fleetStatusSvc helicopterFleetStatusProvider) *TakeoverHandler {
	h.fleetStatus = fleetStatusSvc
	return h
}

func (h *TakeoverHandler) WithHelicopterService(helicopterSvc takeoverHelicopterChecker) *TakeoverHandler {
	h.helicopter = helicopterSvc
	return h
}

func (h *TakeoverHandler) WithComplaintService(complaintSvc complaintGroundingLister) *TakeoverHandler {
	h.complaintSvc = complaintSvc
	return h
}

func (h *TakeoverHandler) WithMCFService(mcfSvc mcfPendingChecker) *TakeoverHandler {
	h.mcfSvc = mcfSvc
	return h
}

// existingTakeoverHelicopterID resolves the helicopter currently assigned to the
// takeover behind the given flight (via its reserve AC), used to skip the
// availability check when the helicopter is not being changed.
func (h *TakeoverHandler) existingTakeoverHelicopterID(ctx context.Context, fl *flight.Flight) []byte {
	if fl == nil || h.reserveAc == nil || len(fl.ReserveAcID) != 16 {
		return nil
	}
	rac, err := h.reserveAc.GetByID(ctx, fl.ReserveAcID)
	if err != nil || rac == nil {
		return nil
	}
	return rac.AircraftID
}

// helicopterBookedError returns a conflict error when the requested helicopter is
// already booked (in use by an active flight/takeover). When current is the same
// helicopter (no change on update) the check is skipped.
func (h *TakeoverHandler) helicopterBookedError(ctx context.Context, requested, current []byte) *apperrorsx.AppError {
	if h.helicopter == nil || len(requested) != 16 {
		return nil
	}
	if len(current) == 16 && bytes.Equal(requested, current) {
		return nil
	}
	inUse, err := h.helicopter.IsInUse(ctx, requested)
	if err != nil || !inUse {
		return nil
	}
	return apperrorsx.New(apperrorsx.ErrTakeoverHelicopterInUse)
}

func (h *TakeoverHandler) helicopterGroundedError(ctx context.Context, requested, current []byte) *apperrorsx.AppError {
	if len(requested) != 16 {
		return nil
	}
	if len(current) == 16 && bytes.Equal(requested, current) {
		return nil
	}
	if h.mcfSvc != nil && mcfPendingHelicopterSet(ctx, h.mcfSvc, [][]byte{requested})[string(requested)] {
		return apperrorsx.New(apperrorsx.ErrTakeoverHelicopterMCF)
	}
	grounded := false
	if h.helicopter != nil {
		if heli, err := h.helicopter.GetByID(ctx, requested); err == nil && heli != nil && heli.AirOnGround {
			grounded = true
		}
	}
	if !grounded && h.complaintSvc != nil {
		grounded = groundingHelicopterSet(ctx, h.complaintSvc, [][]byte{requested}, time.Now().UTC())[string(requested)]
	}
	if grounded {
		return apperrorsx.New(apperrorsx.ErrTakeoverHelicopterAOG)
	}
	return nil
}

func (h *TakeoverHandler) takeoverUserHasGlobalAccess(ctx context.Context) bool {
	return h.takeoverUserHasGlobalAccessForUserID(ctx, appctx.GetUserID(ctx))
}

func (h *TakeoverHandler) takeoverUserHasGlobalAccessForUserID(ctx context.Context, userID []byte) bool {
	if h == nil || h.auth == nil {
		return false
	}
	if len(userID) != 16 {
		return false
	}
	user, err := h.auth.GetUserByID(ctx, userID)
	if err != nil || user == nil {
		return false
	}
	return takeoverUserHasRole(user, "admin")
}

func takeoverUserHasRole(user *authdomain.User, roleName string) bool {
	if user == nil {
		return false
	}
	for i := range user.Roles {
		if strings.EqualFold(strings.TrimSpace(user.Roles[i].Name), roleName) {
			return true
		}
	}
	return false
}

func (h *TakeoverHandler) hasAfterFlightInspection(ctx context.Context, flightInspectionID []byte) bool {
	if h == nil || h.db == nil || len(flightInspectionID) != 16 {
		return false
	}
	var count int64
	if err := h.db.WithContext(ctx).
		Table("after_flight_inspections").
		Where("flight_inspection_id = ?", flightInspectionID).
		Count(&count).Error; err != nil {
		return false
	}
	return count > 0
}

func (h *TakeoverHandler) WithFlightInspectionService(inspectionSvc flightinspection.FlightInspectionService) *TakeoverHandler {
	h.inspect = inspectionSvc
	return h
}

func (h *TakeoverHandler) WithBeforeFlightInspectionService(beforeSvc beforeflightinspection.Service) *TakeoverHandler {
	h.before = beforeSvc
	return h
}

func (h *TakeoverHandler) WithFlightPrepCheckService(prepSvc flightprepcheck.Service) *TakeoverHandler {
	h.prep = prepSvc
	return h
}

func (h *TakeoverHandler) WithHelicopterFileService(heliFileSvc helicopterfile.Service) *TakeoverHandler {
	h.heliFile = heliFileSvc
	return h
}

func (h *TakeoverHandler) WithFileManagerService(fileManagerSvc filemanager.Service) *TakeoverHandler {
	h.fileManager = fileManagerSvc
	return h
}

func (h *TakeoverHandler) WithFlightInspectionFileChecklistService(fileChecklistSvc flightinspectionfilechecklist.Service) *TakeoverHandler {
	h.fileCk = fileChecklistSvc
	return h
}

func (h *TakeoverHandler) WithFileStorage(storage fileManagerDownloadURLStorage) *TakeoverHandler {
	h.storage = storage
	return h
}

func (h *TakeoverHandler) WithTakeoverAcService(takeoverAcSvc *service.TakeoverModuleService) *TakeoverHandler {
	h.takeoverAc = takeoverAcSvc
	return h
}

func logTakeoverError(c *fiber.Ctx, action string, err error) {
	if err == nil {
		return
	}
	if c != nil {
		c.Locals(apperrorsx.ContextKeyHTTPErrorInternalDetail, buildTakeoverErrorDetail(action, err))
	}
	attrs := []any{
		"action", action,
		"method", c.Method(),
		"path", c.Path(),
		"request_id", c.Get("X-Request-Id"),
		"error", err.Error(),
		"error_type", fmt.Sprintf("%T", err),
		"error_chain", buildErrorChain(err),
	}
	var appErr *apperrorsx.AppError
	if errors.As(err, &appErr) && appErr != nil {
		attrs = append(attrs,
			"app_error_status", appErr.Status,
			"app_error_code", appErr.Code,
			"app_error_error_code", appErr.ErrorCode,
			"app_error_title", appErr.Title,
			"app_error_message", appErr.Message,
			"app_error_detail", appErr.Detail,
		)
		if appErr.Cause != nil {
			attrs = append(attrs, "app_error_cause", appErr.Cause.Error())
		}
	}
	slog.Error("takeover handler error", attrs...)
}

func buildTakeoverErrorDetail(action string, err error) string {
	if err == nil {
		return ""
	}
	parts := make([]string, 0, 8)
	if strings.TrimSpace(action) != "" {
		parts = append(parts, "action="+action)
	}
	parts = append(parts, "error="+err.Error())
	if chain := buildErrorChain(err); len(chain) > 0 {
		parts = append(parts, "chain="+strings.Join(chain, " <- "))
	}
	var appErr *apperrorsx.AppError
	if errors.As(err, &appErr) && appErr != nil {
		parts = append(parts,
			fmt.Sprintf("app_error_status=%d", appErr.Status),
			fmt.Sprintf("app_error_code=%s", appErr.Code),
			fmt.Sprintf("app_error_error_code=%s", appErr.ErrorCode),
			fmt.Sprintf("app_error_title=%s", appErr.Title),
			fmt.Sprintf("app_error_message=%s", appErr.Message),
		)
		if appErr.Cause != nil {
			parts = append(parts, "app_error_cause="+appErr.Cause.Error())
		}
	}
	return strings.Join(parts, " | ")
}

func buildErrorChain(err error) []string {
	if err == nil {
		return nil
	}
	chain := make([]string, 0, 8)
	seen := map[string]struct{}{}
	for current := err; current != nil; current = errors.Unwrap(current) {
		msg := current.Error()
		if msg == "" {
			msg = fmt.Sprintf("%T", current)
		}
		key := fmt.Sprintf("%T:%s", current, msg)
		if _, ok := seen[key]; ok {
			break
		}
		seen[key] = struct{}{}
		chain = append(chain, fmt.Sprintf("%T: %s", current, msg))
	}
	return chain
}

func writeTakeoverErrors(c *fiber.Ctx, status int, errs []jsonapi.ErrorObject) error {
	enriched := make([]jsonapi.ErrorObject, 0, len(errs))
	statusStr := strconv.Itoa(status)
	for i := range errs {
		e := errs[i]
		e.Status = statusStr
		def := inferTakeoverDef(status, e.Title, e.Detail)
		if strings.TrimSpace(e.Code) == "" {
			e.Code = def.Code
		}
		if strings.TrimSpace(e.ErrorCode) == "" {
			e.ErrorCode = def.ErrorCode
		}
		if strings.TrimSpace(e.Title) == "" {
			e.Title = def.Title
		}
		e.Detail = def.Message
		enriched = append(enriched, e)
	}
	return response.WriteErrors(c, status, enriched)
}

func inferTakeoverDef(status int, title, detail string) apperrorsx.Def {
	lowerTitle := strings.ToLower(strings.TrimSpace(title))
	lowerDetail := strings.ToLower(strings.TrimSpace(detail))
	switch status {
	case fiber.StatusNotFound:
		switch {
		case strings.Contains(lowerDetail, "flight"):
			return apperrorsx.ErrTakeoverNotFound
		default:
			return apperrorsx.ErrTakeoverNotFound
		}
	case fiber.StatusUnprocessableEntity:
		switch {
		case strings.Contains(lowerDetail, "helicopter_file_id does not belong"):
			return apperrorsx.ErrTakeoverFileSectionInvalid
		case strings.Contains(lowerDetail, "invalid uuid"):
			return apperrorsx.ErrTakeoverInvalidUUID
		case strings.Contains(lowerDetail, "id is required"):
			return apperrorsx.ErrTakeoverIDRequired
		case strings.Contains(lowerDetail, "id does not match"):
			return apperrorsx.ErrTakeoverIDMismatch
		case strings.Contains(lowerTitle, "validation error"):
			return apperrorsx.ErrTakeoverInvalidPayload
		default:
			return apperrorsx.ErrTakeoverInvalidPayload
		}
	case fiber.StatusBadRequest:
		switch {
		case strings.Contains(lowerTitle, "invalid json"):
			return apperrorsx.ErrTakeoverInvalidJSON
		case strings.Contains(lowerTitle, "create failed"):
			return apperrorsx.ErrTakeoverCreateFailed
		case strings.Contains(lowerTitle, "update failed"):
			return apperrorsx.ErrTakeoverUpdateFailed
		case strings.Contains(lowerTitle, "delete failed"):
			return apperrorsx.ErrTakeoverDeleteFailed
		case strings.Contains(lowerTitle, "get failed"):
			return apperrorsx.ErrTakeoverGetFailed
		case strings.Contains(lowerTitle, "list failed"):
			return apperrorsx.ErrTakeoverListFailed
		default:
			return apperrorsx.ErrTakeoverCreateFailed
		}
	case fiber.StatusConflict:
		return apperrorsx.ErrTakeoverHelicopterInUse
	default:
		return apperrorsx.ErrInternal
	}
}

func takeoverAppErrorFromServiceErr(err error) *apperrorsx.AppError {
	if err == nil {
		return nil
	}
	detail := strings.ToLower(strings.TrimSpace(err.Error()))
	switch {
	case strings.Contains(detail, "helicopter_file_id does not belong to helicopter and section"),
		strings.Contains(detail, "helicopter_file_id does not belong to selected helicopter/section"):
		return apperrorsx.New(apperrorsx.ErrTakeoverFileSectionInvalid).WithCause(err)
	case strings.Contains(detail, "must include all helicopter files"),
		strings.Contains(detail, "must be fully checked"),
		strings.Contains(detail, "edited template file must be included in the flight preparation file checklist"),
		strings.Contains(detail, "is required"),
		strings.Contains(detail, "invalid helicopter_file_id"),
		strings.Contains(detail, "must be a valid uuid"),
		strings.Contains(detail, "invalid roster_detail"):
		return apperrorsx.New(apperrorsx.ErrTakeoverInvalidPayload).WithCause(err)
	case strings.Contains(detail, "helicopter is already booked"):
		return apperrorsx.New(apperrorsx.ErrTakeoverHelicopterInUse).WithCause(err)
	case strings.Contains(detail, "not found"):
		return apperrorsx.New(apperrorsx.ErrTakeoverNotFound).WithCause(err)
	case strings.Contains(detail, "create failed"):
		return apperrorsx.New(apperrorsx.ErrTakeoverCreateFailed).WithCause(err)
	case strings.Contains(detail, "update failed"):
		return apperrorsx.New(apperrorsx.ErrTakeoverUpdateFailed).WithCause(err)
	default:
		return nil
	}
}

// CreateTakeover godoc
// @Summary Create takeover in one request
// @Description Atomic create for duty roster + crews + reserve AC + pre-flight inspection + flight. The takeover inspection payload also includes fleet status files under `inspection.fleet_status_file`.
// @Description Optional takeover file uploads can be included via `data.attributes.files[*]` using upload intent UUIDs.
// @Description Edited template draft files created earlier can be linked by passing `data.attributes.edited_template_file_ids[*]`.
// @Tags Flight - Takeovers
// @Accept json
// @Produce json
// @Param request body requestdto.TakeoverCreateRequest true "Create takeover request"
// @Success 201 {object} responsedto.TakeoverResponse
// @Failure 400 {object} jsonapi.ErrorDocument
// @Failure 422 {object} jsonapi.ErrorDocument
// @Router /api/v1/takeovers/create [post]
func (h *TakeoverHandler) CreateTakeover(c *fiber.Ctx) error {
	takeoverSvc := h.takeoverService()
	if h == nil || takeoverSvc == nil {
		logTakeoverError(c, "CreateTakeover", errors.New("takeover service is not configured"))
		return writeTakeoverErrors(c, fiber.StatusInternalServerError, []jsonapi.ErrorObject{{Status: "500", Title: "Create failed", Detail: "takeover service is not configured"}})
	}
	var req requestdto.TakeoverCreateRequest
	if err := c.BodyParser(&req); err != nil {
		logTakeoverError(c, "CreateTakeover", err)
		return writeTakeoverErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{Status: "400", Title: "Invalid JSON", Detail: safeInternalDetail()}})
	}
	if errs := h.validate.ValidateStruct(req); errs != nil {
		return writeTakeoverErrors(c, fiber.StatusUnprocessableEntity, errs)
	}
	var errObj *jsonapi.ErrorObject
	var baseID []byte
	hasBaseID := false
	if req.Data.Attributes.BaseID != nil && strings.TrimSpace(*req.Data.Attributes.BaseID) != "" {
		baseID, errObj = parseTakeoverUUID(*req.Data.Attributes.BaseID, "/data/attributes/base_id")
		if errObj != nil {
			return writeTakeoverErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
		}
		hasBaseID = true
	}
	dutyDate, errObj := parseTakeoverDate(req.Data.Attributes.DutyDate)
	if errObj != nil {
		return writeTakeoverErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
	}
	var helicopterID []byte
	if req.Data.Attributes.AircraftID != nil && strings.TrimSpace(*req.Data.Attributes.AircraftID) != "" {
		helicopterID, errObj = parseTakeoverUUID(*req.Data.Attributes.AircraftID, "/data/attributes/helicopter_id")
		if errObj != nil {
			return writeTakeoverErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
		}
	}
	if appErr := h.helicopterGroundedError(c.UserContext(), helicopterID, nil); appErr != nil {
		return apperrorsx.Write(c, appErr)
	}
	var inspection *requestdto.ReserveAcInspectionCreateAttributes
	if req.Data.Attributes.Inspection != nil {
		inspection = req.Data.Attributes.Inspection
	}
	out, err := takeoverSvc.Create(c.UserContext(), service.TakeoverCreateInput{
		BaseID:          baseID,
		HasBaseID:       hasBaseID,
		DutyDate:        dutyDate,
		HasDutyDate:     true,
		Notes:           req.Data.Attributes.Notes,
		RosterDetail:    takeoverRosterDetailInput(req.Data.Attributes.Detail),
		HasRosterDetail: req.Data.Attributes.Detail != nil,
		HelicopterID:    helicopterID,
		HasHelicopterID: helicopterID != nil,
		Inspection:      inspection,
		HasInspection:   inspection != nil,
		ActorUserID:     actorUserID(c),
	})
	if err != nil {
		logTakeoverError(c, "CreateTakeover", err)
		if appErr := takeoverAppErrorFromServiceErr(err); appErr != nil {
			return apperrorsx.Write(c, appErr)
		}
		return apperrorsx.Write(c, apperrorsx.New(apperrorsx.ErrTakeoverCreateFailed).WithCause(err))
	}
	if err := h.attachTakeoverCreateFiles(c, takeoverSvc, out.TakeoverAcID, req.Data.Attributes.Files); err != nil {
		return err
	}
	if err := h.attachTakeoverEditedTemplateFiles(c, takeoverSvc, out.TakeoverAcID, req.Data.Attributes.EditedTemplateFileIDs); err != nil {
		return err
	}
	outResp, respErr := h.buildTakeoverResponse(c, out.TakeoverAcID)
	if respErr != nil {
		logTakeoverError(c, "CreateTakeover.buildResponse", respErr)
		return writeTakeoverErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{Status: "400", Title: "Create failed", Detail: respErr.Error()}})
	}
	if outResp == nil {
		return writeTakeoverErrors(c, fiber.StatusInternalServerError, []jsonapi.ErrorObject{{Status: "500", Title: "Create failed", Detail: "takeover response not available"}})
	}
	return response.Write(c, fiber.StatusCreated, outResp.Data)
}

func takeoverRosterDetailInput(in *requestdto.TakeoverRosterDetailsInput) *requestdto.DutyRosterDetailsInput {
	if in == nil {
		return nil
	}
	out := requestdto.DutyRosterDetailsInput{
		Pilot:       make([]requestdto.DutyRosterPilotCrewInput, 0, len(in.Pilot)),
		Doctor:      make([]requestdto.DutyRosterCrewInput, 0, len(in.Doctor)),
		Rescuer:     make([]requestdto.DutyRosterCrewInput, 0, len(in.Rescuer)),
		Helper:      make([]requestdto.DutyRosterCrewInput, 0, len(in.Helper)),
		OtherPerson: make([]requestdto.DutyRosterOtherPersonInput, 0, len(in.OtherPerson)),
	}
	for _, item := range in.Pilot {
		out.Pilot = append(out.Pilot, requestdto.DutyRosterPilotCrewInput{
			ID:               item.ID,
			FlightInstructor: item.FlightInstructor,
			LineChecker:      item.LineChecker,
			Supervisor:       item.Supervisor,
			Examiner:         item.Examiner,
			CoPilot:          item.CoPilot,
			CrewType:         item.CrewType,
		})
	}
	for _, item := range in.Doctor {
		out.Doctor = append(out.Doctor, requestdto.DutyRosterCrewInput{ID: item.ID})
	}
	for _, item := range in.Rescuer {
		out.Rescuer = append(out.Rescuer, requestdto.DutyRosterCrewInput{ID: item.ID})
	}
	for _, item := range in.Helper {
		out.Helper = append(out.Helper, requestdto.DutyRosterCrewInput{ID: item.ID})
	}
	for _, item := range in.OtherPerson {
		out.OtherPerson = append(out.OtherPerson, requestdto.DutyRosterOtherPersonInput{
			Name:        item.Name,
			MobilePhone: item.MobilePhone,
			Email:       item.Email,
		})
	}
	return &out
}

func takeoverRosterDetailResponse(in requestdto.DutyRosterDetailsInput) responsedto.TakeoverDutyRosterDetailsResponse {
	out := responsedto.TakeoverDutyRosterDetailsResponse{
		Pilot:       make([]responsedto.TakeoverDutyRosterPilotCrew, 0, len(in.Pilot)),
		Doctor:      make([]responsedto.TakeoverDutyRosterCrew, 0, len(in.Doctor)),
		Rescuer:     make([]responsedto.TakeoverDutyRosterCrew, 0, len(in.Rescuer)),
		Helper:      make([]responsedto.TakeoverDutyRosterCrew, 0, len(in.Helper)),
		OtherPerson: make([]responsedto.TakeoverDutyRosterCrew, 0, len(in.OtherPerson)),
	}
	for _, item := range in.Pilot {
		out.Pilot = append(out.Pilot, responsedto.TakeoverDutyRosterPilotCrew{
			ID:               item.ID,
			CrewType:         item.CrewType,
			FlightInstructor: takeoverBoolPtr(item.FlightInstructor),
			LineChecker:      takeoverBoolPtr(item.LineChecker),
			Supervisor:       takeoverBoolPtr(item.Supervisor),
			Examiner:         takeoverBoolPtr(item.Examiner),
			CoPilot:          takeoverBoolPtr(item.CoPilot),
			Role:             "pilot",
		})
	}
	for _, item := range in.Doctor {
		out.Doctor = append(out.Doctor, responsedto.TakeoverDutyRosterCrew{
			ID:   item.ID,
			Role: "doctor",
		})
	}
	for _, item := range in.Rescuer {
		out.Rescuer = append(out.Rescuer, responsedto.TakeoverDutyRosterCrew{
			ID:   item.ID,
			Role: "rescuer",
		})
	}
	for _, item := range in.Helper {
		out.Helper = append(out.Helper, responsedto.TakeoverDutyRosterCrew{
			ID:   item.ID,
			Role: "helper",
		})
	}
	for _, item := range in.OtherPerson {
		out.OtherPerson = append(out.OtherPerson, responsedto.TakeoverDutyRosterCrew{
			Name:        item.Name,
			MobilePhone: item.MobilePhone,
			Email:       item.Email,
		})
	}
	return out
}

func takeoverDutyRosterDetailsFromRosterDetails(in responsedto.DutyRosterDetailsResponse) responsedto.TakeoverDutyRosterDetailsResponse {
	out := responsedto.TakeoverDutyRosterDetailsResponse{
		Pilot:       make([]responsedto.TakeoverDutyRosterPilotCrew, 0, len(in.Pilot)),
		Doctor:      make([]responsedto.TakeoverDutyRosterCrew, 0, len(in.Doctor)),
		Rescuer:     make([]responsedto.TakeoverDutyRosterCrew, 0, len(in.Rescuer)),
		Helper:      make([]responsedto.TakeoverDutyRosterCrew, 0, len(in.Helper)),
		OtherPerson: make([]responsedto.TakeoverDutyRosterCrew, 0, len(in.OtherPerson)),
	}
	for _, item := range in.Pilot {
		out.Pilot = append(out.Pilot, responsedto.TakeoverDutyRosterPilotCrew{
			ID:               item.ID,
			Name:             item.Name,
			Role:             item.Role,
			CrewType:         item.CrewType,
			FlightInstructor: item.FlightInstructor,
			LineChecker:      item.LineChecker,
			Supervisor:       item.Supervisor,
			Examiner:         item.Examiner,
			CoPilot:          item.CoPilot,
		})
	}
	for _, item := range in.Doctor {
		out.Doctor = append(out.Doctor, responsedto.TakeoverDutyRosterCrew{
			ID:          item.ID,
			Name:        item.Name,
			Role:        item.Role,
			MobilePhone: item.MobilePhone,
			Email:       item.Email,
		})
	}
	for _, item := range in.Rescuer {
		out.Rescuer = append(out.Rescuer, responsedto.TakeoverDutyRosterCrew{
			ID:          item.ID,
			Name:        item.Name,
			Role:        item.Role,
			MobilePhone: item.MobilePhone,
			Email:       item.Email,
		})
	}
	for _, item := range in.Helper {
		out.Helper = append(out.Helper, responsedto.TakeoverDutyRosterCrew{
			ID:          item.ID,
			Name:        item.Name,
			Role:        item.Role,
			MobilePhone: item.MobilePhone,
			Email:       item.Email,
		})
	}
	for _, item := range in.OtherPerson {
		out.OtherPerson = append(out.OtherPerson, responsedto.TakeoverDutyRosterCrew{
			Name:        item.Name,
			MobilePhone: item.MobilePhone,
			Email:       item.Email,
		})
	}
	return out
}

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

func loadTakeoverRosterCrewRows(ctx context.Context, db *gorm.DB, takeoverID []byte) ([]takeover.TakeoverRosterCrew, error) {
	rows := make([]takeover.TakeoverRosterCrew, 0)
	if err := db.WithContext(ctx).
		Where("takeover_id = ? AND deleted_at IS NULL", takeoverID).
		Preload("User").
		Order("created_at ASC").
		Find(&rows).Error; err != nil {
		return nil, err
	}
	return rows, nil
}

func loadTakeoverOtherPeopleRows(ctx context.Context, db *gorm.DB, takeoverID []byte) ([]takeover.TakeoverOtherPerson, error) {
	rows := make([]takeover.TakeoverOtherPerson, 0)
	if err := db.WithContext(ctx).
		Where("takeover_id = ? AND deleted_at IS NULL", takeoverID).
		Order("created_at ASC").
		Find(&rows).Error; err != nil {
		return nil, err
	}
	return rows, nil
}

func buildDutyRosterResourceFromTakeoverRows(resource responsedto.DutyRosterResource, rows []takeover.TakeoverRosterCrew, flightRow *flight.Flight) responsedto.DutyRosterResource {
	for i := range rows {
		item := rows[i]
		if item.User != nil && len(item.User.FirstName+item.User.LastName) > 0 {
			item.NameLabel = item.User.FirstName + " " + item.User.LastName
		}
		switch strings.ToLower(strings.TrimSpace(item.RoleCode)) {
		case "pilot":
			resource.Attributes.Detail.Pilot = append(resource.Attributes.Detail.Pilot, responsedto.DutyRosterPilotCrew{
				ID:               idString(item.UserID),
				Name:             item.NameLabel,
				Role:             "pilot",
				MobilePhone:      item.MobilePhone,
				Email:            item.Email,
				CrewType:         item.CrewType,
				FlightInstructor: boolPtr(item.FlightInstructor),
				LineChecker:      boolPtr(item.LineChecker),
				Supervisor:       boolPtr(item.Supervisor),
				Examiner:         boolPtr(item.Examiner),
				CoPilot:          boolPtr(item.CoPilot),
			})
		case "doctor":
			resource.Attributes.Detail.Doctor = append(resource.Attributes.Detail.Doctor, responsedto.DutyRosterCrew{
				ID:          firstNonEmptyString(idString(item.UserID), idString(item.ID)),
				Name:        item.NameLabel,
				Role:        "doctor",
				MobilePhone: item.MobilePhone,
				Email:       item.Email,
			})
		case "rescuer":
			resource.Attributes.Detail.Rescuer = append(resource.Attributes.Detail.Rescuer, responsedto.DutyRosterCrew{
				ID:          firstNonEmptyString(idString(item.UserID), idString(item.ID)),
				Name:        item.NameLabel,
				Role:        "rescuer",
				MobilePhone: item.MobilePhone,
				Email:       item.Email,
			})
		case "helper":
			resource.Attributes.Detail.Helper = append(resource.Attributes.Detail.Helper, responsedto.DutyRosterCrew{
				ID:          firstNonEmptyString(idString(item.UserID), idString(item.ID)),
				Name:        item.NameLabel,
				Role:        "helper",
				MobilePhone: item.MobilePhone,
				Email:       item.Email,
			})
		}
	}
	if flightRow != nil {
		resource.ID = idString(flightRow.TakeoverAcID)
		resource.Attributes.FlightID = idString(flightRow.ID)
		resource.Attributes.DutyDate = calendarDateString(flightRow.Date)
	}
	return resource
}

func buildDutyRosterResourceFromOtherPeople(resource responsedto.DutyRosterResource, rows []takeover.TakeoverOtherPerson) responsedto.DutyRosterResource {
	for i := range rows {
		resource.Attributes.Detail.OtherPerson = append(resource.Attributes.Detail.OtherPerson, responsedto.DutyRosterCrew{
			ID:          idString(rows[i].ID),
			Name:        rows[i].Name,
			Role:        "other_person",
			MobilePhone: rows[i].MobilePhone,
			Email:       rows[i].Email,
		})
	}
	return resource
}

func softDeleteTakeoverRosterChildren(ctx context.Context, db *gorm.DB, takeoverID []byte, deletedBy []byte) error {
	now := time.Now().UTC()
	if err := db.WithContext(ctx).Model(&takeover.TakeoverRosterCrew{}).
		Where("takeover_id = ? AND deleted_at IS NULL", takeoverID).
		Updates(map[string]any{"deleted_at": now, "deleted_by": deletedBy, "updated_by": deletedBy}).Error; err != nil {
		return err
	}
	if err := db.WithContext(ctx).Model(&takeover.TakeoverOtherPerson{}).
		Where("takeover_id = ? AND deleted_at IS NULL", takeoverID).
		Updates(map[string]any{"deleted_at": now, "deleted_by": deletedBy, "updated_by": deletedBy}).Error; err != nil {
		return err
	}
	return nil
}

// ListTakeovers godoc
// @Summary List takeovers
// @Description Return composite takeover records built from flight, reserve AC, duty roster, and inspection. The inspection payload includes `before`, `prepare`, and `fleet_status_file`.
// @Tags Flight - Takeovers
// @Produce json
// @Success 200 {object} map[string]any
// @Failure 400 {object} jsonapi.ErrorDocument
// @Router /api/v1/takeovers/get-all [get]
func (h *TakeoverHandler) ListTakeovers(c *fiber.Ctx) error {
	if h.takeoverAc == nil || h.flight == nil {
		logTakeoverError(c, "ListTakeovers", errors.New("takeover service is not configured"))
		return writeTakeoverErrors(c, fiber.StatusInternalServerError, []jsonapi.ErrorObject{{Status: "500", Title: "Get failed", Detail: "takeover service is not configured"}})
	}
	rows, _, err := h.takeoverAc.List(c.UserContext(), 100, 0)
	if err != nil {
		logTakeoverError(c, "ListTakeovers", err)
		return writeTakeoverErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{Status: "400", Title: "Get failed", Detail: safeInternalDetail()}})
	}
	takeoverIDs := make([][]byte, 0, len(rows))
	for i := range rows {
		takeoverIDs = append(takeoverIDs, rows[i].ID)
	}
	flights, err := h.flight.ListByTakeoverAcIDs(c.UserContext(), takeoverIDs)
	if err != nil {
		logTakeoverError(c, "ListTakeovers", err)
		return writeTakeoverErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{Status: "400", Title: "Get failed", Detail: safeInternalDetail()}})
	}
	flightByTakeoverID := make(map[string]flight.Flight, len(flights))
	for i := range flights {
		flightByTakeoverID[idString(flights[i].TakeoverAcID)] = flights[i]
	}
	out := make([]responsedto.TakeoverResource, 0, len(rows))
	for i := range rows {
		if flightRow, ok := flightByTakeoverID[idString(rows[i].ID)]; ok {
			item, err := h.buildTakeoverResponseFromRows(c, &rows[i], &flightRow)
			if err != nil || item == nil {
				continue
			}
			out = append(out, item.Data)
			continue
		}
		item, err := h.buildTakeoverResponse(c, rows[i].ID)
		if err != nil || item == nil {
			continue
		}
		out = append(out, item.Data)
	}
	return response.WriteWithMeta(c, fiber.StatusOK, out, map[string]any{"total": len(out)})
}

// GetTakeover godoc
// @Summary Get takeover by flight UUID
// @Tags Flight - Takeovers
// @Produce json
// @Param id path string true "Flight UUID"
// @Success 200 {object} responsedto.TakeoverResponse
// @Failure 400 {object} jsonapi.ErrorDocument
// @Failure 404 {object} jsonapi.ErrorDocument
// @Router /api/v1/takeovers/get/{id} [get]
func (h *TakeoverHandler) GetTakeover(c *fiber.Ctx) error {
	idRaw := strings.TrimSpace(c.Params("id"))
	id, err := uuidv7.ParseString(idRaw)
	if err != nil {
		return writeTakeoverErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{Status: "422", Title: "Validation error", Detail: "id is invalid UUID", Source: &jsonapi.ErrorSource{Pointer: "/data/id"}}})
	}
	out, err := h.buildTakeoverResponse(c, id)
	if err != nil {
		logTakeoverError(c, "GetTakeover", err)
		return writeTakeoverErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{Status: "400", Title: "Get failed", Detail: safeInternalDetail()}})
	}
	if out == nil {
		return writeTakeoverErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{Status: "404", Title: "Not found", Detail: "takeover not found"}})
	}
	return response.Write(c, fiber.StatusOK, out.Data)
}

// UpdateTakeover godoc
// @Summary Replace takeover by flight UUID
// @Tags Flight - Takeovers
// @Accept json
// @Produce json
// @Param id path string true "Flight UUID"
// @Param request body requestdto.TakeoverUpdateRequest true "Update takeover request"
// @Success 200 {object} responsedto.TakeoverResponse
// @Failure 400 {object} jsonapi.ErrorDocument
// @Failure 404 {object} jsonapi.ErrorDocument
// @Router /api/v1/takeovers/update/{id} [patch]
func (h *TakeoverHandler) UpdateTakeover(c *fiber.Ctx) error {
	takeoverSvc := h.takeoverService()
	if h == nil || takeoverSvc == nil {
		logTakeoverError(c, "UpdateTakeover", errors.New("takeover service is not configured"))
		return writeTakeoverErrors(c, fiber.StatusInternalServerError, []jsonapi.ErrorObject{{Status: "500", Title: "Update failed", Detail: "takeover service is not configured"}})
	}
	idRaw := strings.TrimSpace(c.Params("id"))
	id, err := uuidv7.ParseString(idRaw)
	if err != nil {
		return writeTakeoverErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{Status: "422", Title: "Validation error", Detail: "id is invalid UUID", Source: &jsonapi.ErrorSource{Pointer: "/data/id"}}})
	}
	existingFlight, err := h.flight.GetByID(c.UserContext(), id)
	if err != nil || existingFlight == nil {
		return writeTakeoverErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{Status: "404", Title: "Not found", Detail: "takeover not found"}})
	}
	var req requestdto.TakeoverUpdateRequest
	if err := c.BodyParser(&req); err != nil {
		logTakeoverError(c, "UpdateTakeover", err)
		return writeTakeoverErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{Status: "400", Title: "Invalid JSON", Detail: safeInternalDetail()}})
	}
	if errs := h.validate.ValidateStruct(req); errs != nil {
		return writeTakeoverErrors(c, fiber.StatusUnprocessableEntity, errs)
	}
	var errObj *jsonapi.ErrorObject
	var baseID []byte
	if req.Data.Attributes.BaseID != nil && strings.TrimSpace(*req.Data.Attributes.BaseID) != "" {
		baseID, errObj = parseTakeoverUUID(*req.Data.Attributes.BaseID, "/data/attributes/base_id")
		if errObj != nil {
			return writeTakeoverErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
		}
	}
	var dutyDate time.Time
	if req.Data.Attributes.DutyDate != nil && strings.TrimSpace(*req.Data.Attributes.DutyDate) != "" {
		dutyDate, errObj = parseTakeoverDate(*req.Data.Attributes.DutyDate)
		if errObj != nil {
			return writeTakeoverErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
		}
	}
	var helicopterID []byte
	if req.Data.Attributes.AircraftID != nil && strings.TrimSpace(*req.Data.Attributes.AircraftID) != "" {
		helicopterID, errObj = parseTakeoverUUID(*req.Data.Attributes.AircraftID, "/data/attributes/helicopter_id")
		if errObj != nil {
			return writeTakeoverErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
		}
	}
	var inspection *requestdto.ReserveAcInspectionCreateAttributes
	if req.Data.Attributes.Inspection != nil {
		inspection = req.Data.Attributes.Inspection
	}
	filesAdd := takeoverFileCreateInputs(req.Data.Attributes.Files, req.Data.Attributes.FilesAdd)
	filesRemove := req.Data.Attributes.FilesRemove
	filesReplace := req.Data.Attributes.FilesReplace
	if len(filesReplace) > 0 && (len(filesAdd) > 0 || len(filesRemove) > 0) {
		return writeTakeoverErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
			Status: "422",
			Title:  "Validation error",
			Detail: "files_replace cannot be combined with files, files_add, or files_remove",
			Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/files_replace"},
		}})
	}
	if appErr := h.helicopterGroundedError(c.UserContext(), helicopterID, h.existingTakeoverHelicopterID(c.UserContext(), existingFlight)); appErr != nil {
		return apperrorsx.Write(c, appErr)
	}
	if len(existingFlight.ReserveAcID) == 16 && h.hasAfterFlightInspection(c.UserContext(), existingFlight.ReserveAcID) {
		if req.Data.Attributes.BaseID != nil || req.Data.Attributes.Bases != nil {
			return writeTakeoverErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
				Status: "422",
				Title:  "Validation error",
				Detail: "base_id cannot be changed after after_flight_inspection exists",
				Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/base_id"},
			}})
		}
		if req.Data.Attributes.Detail != nil || req.Data.Attributes.RosterDetail != nil {
			return writeTakeoverErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
				Status: "422",
				Title:  "Validation error",
				Detail: "roster_detail cannot be changed after after_flight_inspection exists",
				Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/roster_detail"},
			}})
		}
	}
	out, err := takeoverSvc.Replace(c.UserContext(), existingFlight.ID, service.TakeoverCreateInput{
		BaseID:          baseID,
		HasBaseID:       baseID != nil,
		DutyDate:        dutyDate,
		HasDutyDate:     req.Data.Attributes.DutyDate != nil && strings.TrimSpace(*req.Data.Attributes.DutyDate) != "",
		Notes:           req.Data.Attributes.Notes,
		RosterDetail:    takeoverRosterDetailInput(req.Data.Attributes.Detail),
		HasRosterDetail: req.Data.Attributes.Detail != nil,
		HelicopterID:    helicopterID,
		HasHelicopterID: helicopterID != nil,
		Inspection:      inspection,
		HasInspection:   inspection != nil,
		ActorUserID:     actorUserID(c),
	})
	if err != nil {
		logTakeoverError(c, "UpdateTakeover", err)
		if appErr := takeoverAppErrorFromServiceErr(err); appErr != nil {
			return apperrorsx.Write(c, appErr)
		}
		return apperrorsx.Write(c, apperrorsx.New(apperrorsx.ErrTakeoverUpdateFailed).WithCause(err))
	}
	if err := h.applyTakeoverUpdateFiles(c, takeoverSvc, existingFlight.TakeoverAcID, filesAdd, filesRemove, filesReplace); err != nil {
		return err
	}
	responseTakeoverID := out.TakeoverAcID
	if len(responseTakeoverID) != 16 {
		responseTakeoverID = existingFlight.TakeoverAcID
	}
	outResp, respErr := h.buildTakeoverResponse(c, responseTakeoverID)
	if respErr != nil {
		logTakeoverError(c, "UpdateTakeover.buildResponse", respErr)
		return writeTakeoverErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{Status: "400", Title: "Update failed", Detail: respErr.Error()}})
	}
	if outResp == nil {
		return writeTakeoverErrors(c, fiber.StatusInternalServerError, []jsonapi.ErrorObject{{Status: "500", Title: "Update failed", Detail: "takeover response not available"}})
	}
	return response.Write(c, fiber.StatusOK, outResp.Data)
}

func takeoverFileCreateInputs(primary, fallback []requestdto.TakeoverCreateFileInput) []requestdto.TakeoverCreateFileInput {
	total := len(primary) + len(fallback)
	if total == 0 {
		return nil
	}
	out := make([]requestdto.TakeoverCreateFileInput, 0, total)
	out = append(out, primary...)
	out = append(out, fallback...)
	return out
}

func (h *TakeoverHandler) applyTakeoverUpdateFiles(
	c *fiber.Ctx,
	takeoverSvc *service.TakeoverService,
	takeoverID []byte,
	filesAdd []requestdto.TakeoverCreateFileInput,
	filesRemove []string,
	filesReplace []requestdto.TakeoverCreateFileInput,
) error {
	if len(filesAdd) == 0 && len(filesRemove) == 0 && len(filesReplace) == 0 {
		return nil
	}
	if h.fileManager == nil {
		logTakeoverError(c, "TakeoverFiles.FileManager", errors.New("file manager service is not configured"))
		return writeTakeoverErrors(c, fiber.StatusServiceUnavailable, []jsonapi.ErrorObject{{Status: "503", Title: "Update failed", Detail: "file manager service is not configured"}})
	}

	current, err := takeoverSvc.GetByID(c.UserContext(), takeoverID)
	if err != nil {
		logTakeoverError(c, "TakeoverFiles.GetByID", err)
		return apperrorsx.Write(c, apperrorsx.New(apperrorsx.ErrTakeoverUpdateFailed).WithCause(err))
	}
	if current == nil {
		return writeTakeoverErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{Status: "404", Title: "Not found", Detail: "takeover not found"}})
	}

	existingFiles := make(map[string]takeover.TakeoverFile, len(current.Files))
	for i := range current.Files {
		existingFiles[idString(current.Files[i].FileAttachmentID)] = current.Files[i]
	}

	if len(filesReplace) > 0 {
		filesAdd = filesReplace
		filesRemove = filesRemove[:0]
		for i := range current.Files {
			filesRemove = append(filesRemove, idString(current.Files[i].FileAttachmentID))
		}
	}

	for i := range filesRemove {
		attachmentID, errObj := parseTakeoverUUID(filesRemove[i], "/data/attributes/files_remove/"+strconv.Itoa(i))
		if errObj != nil {
			return writeTakeoverErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
		}
		attachmentKey := idString(attachmentID)
		if _, ok := existingFiles[attachmentKey]; !ok {
			return writeTakeoverErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{
				Status: "404",
				Title:  "Not found",
				Detail: "takeover file not found",
				Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/files_remove/" + strconv.Itoa(i)},
			}})
		}
		if err := takeoverSvc.DetachFile(c.UserContext(), takeoverID, attachmentID); err != nil {
			logTakeoverError(c, "TakeoverFiles.DetachFile", err)
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return writeTakeoverErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{
					Status: "404",
					Title:  "Not found",
					Detail: "takeover file not found",
					Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/files_remove/" + strconv.Itoa(i)},
				}})
			}
			return apperrorsx.Write(c, apperrorsx.New(apperrorsx.ErrTakeoverUpdateFailed).WithCause(err))
		}
		if err := h.fileManager.DeleteAttachment(c.UserContext(), attachmentID); err != nil {
			logTakeoverError(c, "TakeoverFiles.DeleteAttachment", err)
			return apperrorsx.Write(c, apperrorsx.New(apperrorsx.ErrTakeoverUpdateFailed).WithCause(err))
		}
	}

	if len(filesAdd) == 0 {
		return nil
	}
	return h.attachTakeoverCreateFiles(c, takeoverSvc, takeoverID, filesAdd)
}

func (h *TakeoverHandler) attachTakeoverCreateFiles(c *fiber.Ctx, takeoverSvc *service.TakeoverService, takeoverID []byte, files []requestdto.TakeoverCreateFileInput) error {
	if len(files) == 0 {
		return nil
	}
	if h.fileManager == nil {
		logTakeoverError(c, "TakeoverFiles.FileManager", errors.New("file manager service is not configured"))
		return writeTakeoverErrors(c, fiber.StatusServiceUnavailable, []jsonapi.ErrorObject{{Status: "503", Title: "Create failed", Detail: "file manager service is not configured"}})
	}
	for i := range files {
		item := files[i]
		var fileID []byte
		if strings.TrimSpace(item.FileID) != "" {
			parsedFileID, errObj := parseTakeoverUUID(item.FileID, "/data/attributes/files/"+strconv.Itoa(i)+"/file_id")
			if errObj != nil {
				return writeTakeoverErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
			}
			fileID = parsedFileID
		} else {
			if strings.TrimSpace(item.UploadIntentUUID) == "" {
				return writeTakeoverErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
					Status: "422",
					Title:  "Validation error",
					Detail: "file_id or upload_intent_uuid is required",
					Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/files/" + strconv.Itoa(i)},
				}})
			}
			uploadIntentID, errObj := parseTakeoverUUID(item.UploadIntentUUID, "/data/attributes/files/"+strconv.Itoa(i)+"/upload_intent_uuid")
			if errObj != nil {
				return writeTakeoverErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
			}
			createdFile, createErr := h.fileManager.CreateFileNodeFromUploadIntent(c.UserContext(), filemanager.CreateFileFromUploadIntentParams{
				UploadIntentID: uploadIntentID,
				Name:           strings.TrimSpace(item.Name),
				ActorID:        actorUserID(c),
			})
			if createErr != nil {
				logTakeoverError(c, "TakeoverFiles.CreateFile", createErr)
				return apperrorsx.Write(c, apperrorsx.New(apperrorsx.ErrTakeoverCreateFailed).WithCause(createErr))
			}
			fileID = createdFile.ID
		}
		attachment, attachErr := h.fileManager.CreateAttachment(c.UserContext(), filemanager.CreateAttachmentParams{
			FileID:    fileID,
			RefType:   "takeover",
			RefID:     idString(takeoverID),
			SortOrder: 0,
			IsPrimary: false,
			ActorID:   actorUserID(c),
		})
		if attachErr != nil {
			logTakeoverError(c, "TakeoverFiles.CreateAttachment", attachErr)
			return apperrorsx.Write(c, apperrorsx.New(apperrorsx.ErrTakeoverCreateFailed).WithCause(attachErr))
		}
		if err := takeoverSvc.AttachFile(c.UserContext(), takeoverID, attachment.ID); err != nil {
			logTakeoverError(c, "TakeoverFiles.AttachFile", err)
			return apperrorsx.Write(c, apperrorsx.New(apperrorsx.ErrTakeoverCreateFailed).WithCause(err))
		}
	}
	return nil
}

func (h *TakeoverHandler) attachTakeoverEditedTemplateFiles(c *fiber.Ctx, takeoverSvc *service.TakeoverService, takeoverID []byte, fileIDs []string) error {
	if len(fileIDs) == 0 {
		return nil
	}
	if h.fileManager == nil {
		logTakeoverError(c, "TakeoverEditedTemplateFiles.FileManager", errors.New("file manager service is not configured"))
		return writeTakeoverErrors(c, fiber.StatusServiceUnavailable, []jsonapi.ErrorObject{{Status: "503", Title: "Create failed", Detail: "file manager service is not configured"}})
	}
	for i := range fileIDs {
		fileID, errObj := parseTakeoverUUID(fileIDs[i], "/data/attributes/edited_template_file_ids/"+strconv.Itoa(i))
		if errObj != nil {
			return writeTakeoverErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
		}
		row, err := h.fileManager.GetFileByIDAny(c.UserContext(), fileID)
		if err != nil {
			logTakeoverError(c, "TakeoverEditedTemplateFiles.GetFile", err)
			return apperrorsx.Write(c, apperrorsx.New(apperrorsx.ErrTakeoverCreateFailed).WithCause(err))
		}
		if row == nil {
			return writeTakeoverErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{
				Status: "404",
				Title:  "Not found",
				Detail: "edited template file not found",
				Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/edited_template_file_ids/" + strconv.Itoa(i)},
			}})
		}
		if !takeoverEditedTemplateFileBelongsToTakeover(row, takeoverID) {
			return writeTakeoverErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{
				Status: "404",
				Title:  "Not found",
				Detail: "edited template file not found",
				Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/edited_template_file_ids/" + strconv.Itoa(i)},
			}})
		}
		if !bytes.Equal(row.CreatedBy, actorUserID(c)) {
			return writeTakeoverErrors(c, fiber.StatusForbidden, []jsonapi.ErrorObject{{
				Status: "403",
				Title:  "Forbidden",
				Detail: "forbidden access",
				Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/edited_template_file_ids/" + strconv.Itoa(i)},
			}})
		}
		attachment, attachErr := h.fileManager.CreateAttachment(c.UserContext(), filemanager.CreateAttachmentParams{
			FileID:    fileID,
			RefType:   "takeover",
			RefID:     idString(takeoverID),
			SortOrder: 0,
			IsPrimary: false,
			ActorID:   actorUserID(c),
		})
		if attachErr != nil {
			logTakeoverError(c, "TakeoverEditedTemplateFiles.CreateAttachment", attachErr)
			return apperrorsx.Write(c, apperrorsx.New(apperrorsx.ErrTakeoverCreateFailed).WithCause(attachErr))
		}
		if err := takeoverSvc.AttachFile(c.UserContext(), takeoverID, attachment.ID); err != nil {
			logTakeoverError(c, "TakeoverEditedTemplateFiles.AttachFile", err)
			return apperrorsx.Write(c, apperrorsx.New(apperrorsx.ErrTakeoverCreateFailed).WithCause(err))
		}
	}
	return nil
}

func takeoverEditedTemplateFileBelongsToTakeover(row *filemanager.File, takeoverID []byte) bool {
	if row == nil {
		return false
	}
	if len(row.TakeoverID) == 0 {
		return true
	}
	return len(row.TakeoverID) == 16 && len(takeoverID) == 16 && bytes.Equal(row.TakeoverID, takeoverID)
}

// DeleteTakeover godoc
// @Summary Delete takeover by flight UUID
// @Tags Flight - Takeovers
// @Produce json
// @Param id path string true "Flight UUID"
// @Success 200 {object} map[string]any
// @Failure 400 {object} jsonapi.ErrorDocument
// @Failure 404 {object} jsonapi.ErrorDocument
// @Router /api/v1/takeovers/delete/{id} [delete]
func (h *TakeoverHandler) DeleteTakeover(c *fiber.Ctx) error {
	idRaw := strings.TrimSpace(c.Params("id"))
	id, err := uuidv7.ParseString(idRaw)
	if err != nil {
		return writeTakeoverErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{Status: "422", Title: "Validation error", Detail: "id is invalid UUID", Source: &jsonapi.ErrorSource{Pointer: "/path/id"}}})
	}
	flightRow, err := h.flight.GetByID(c.UserContext(), id)
	if err != nil || flightRow == nil {
		if err != nil {
			logTakeoverError(c, "DeleteTakeover", err)
		}
		return writeTakeoverErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{Status: "404", Title: "Not found", Detail: "takeover not found"}})
	}
	takeoverAcID := flightRow.TakeoverAcID
	if len(takeoverAcID) != 16 {
		return writeTakeoverErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{Status: "404", Title: "Not found", Detail: "takeover not found"}})
	}
	if err := h.flight.Delete(c.UserContext(), flightRow.ID, actorUserID(c)); err != nil {
		logTakeoverError(c, "DeleteTakeover", err)
		return writeTakeoverErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{Status: "400", Title: "Delete failed", Detail: safeInternalDetail()}})
	}
	if row, _ := h.reserveAc.GetByID(c.UserContext(), flightRow.ReserveAcID); row != nil {
		_ = h.reserveAc.Delete(c.UserContext(), row.ID, actorUserID(c))
	}
	if h.db != nil && len(takeoverAcID) == 16 {
		_ = softDeleteTakeoverRosterChildren(c.UserContext(), h.db, takeoverAcID, actorUserID(c))
	}
	if h.takeoverAc != nil {
		_ = h.takeoverAc.Delete(c.UserContext(), takeoverAcID, actorUserID(c))
	}
	return response.Write(c, fiber.StatusOK, map[string]any{"data": map[string]any{"deleted": true, "takeover_ac_id": idString(takeoverAcID), "flight_id": idString(flightRow.ID)}})
}

func (h *TakeoverHandler) buildTakeoverResponse(c *fiber.Ctx, flightID []byte) (*responsedto.TakeoverResponse, error) {
	if h.takeoverAc == nil {
		return nil, errors.New("takeover service is not configured")
	}
	takeoverRow, err := h.takeoverAc.GetByID(c.UserContext(), flightID)
	if err != nil {
		logTakeoverError(c, "buildTakeoverResponse.takeoverAc.GetByID", err)
		return nil, err
	}
	if takeoverRow == nil && h.flight != nil {
		flightRow, ferr := h.flight.GetByID(c.UserContext(), flightID)
		if ferr != nil || flightRow == nil || len(flightRow.TakeoverAcID) != 16 {
			return nil, nil
		}
		takeoverRow, err = h.takeoverAc.GetByID(c.UserContext(), flightRow.TakeoverAcID)
		if err != nil || takeoverRow == nil {
			if err != nil {
				logTakeoverError(c, "buildTakeoverResponse.takeoverAc.GetByIDFallback", err)
			}
			return nil, err
		}
	}
	if takeoverRow == nil {
		return nil, nil
	}
	var flightRow *flight.Flight
	if h.flight != nil {
		flightRow, _ = h.flight.GetByTakeoverAcID(c.UserContext(), takeoverRow.ID)
	}
	reserveRow := h.resolveTakeoverReserveRow(c, takeoverRow, flightRow)
	inspectionSummary := h.resolveTakeoverInspectionSummary(c, reserveRow)
	dutyResource := h.resolveTakeoverDutyResource(c, flightRow)
	attrs := h.buildTakeoverResponseAttributes(c, takeoverRow, flightRow, reserveRow, inspectionSummary, dutyResource)
	return &responsedto.TakeoverResponse{
		Data: responsedto.TakeoverResource{
			Type:       "takeover",
			Attributes: attrs,
		},
	}, nil
}

func (h *TakeoverHandler) buildTakeoverResponseFromRows(c *fiber.Ctx, takeoverRow *takeover.TakeoverAc, flightRow *flight.Flight) (*responsedto.TakeoverResponse, error) {
	if takeoverRow == nil || flightRow == nil {
		return nil, nil
	}
	reserveRow := h.resolveTakeoverReserveRow(c, takeoverRow, flightRow)
	inspectionSummary := h.resolveTakeoverInspectionSummary(c, reserveRow)
	dutyResource := h.resolveTakeoverDutyResource(c, flightRow)
	attrs := h.buildTakeoverResponseAttributes(c, takeoverRow, flightRow, reserveRow, inspectionSummary, dutyResource)
	return &responsedto.TakeoverResponse{
		Data: responsedto.TakeoverResource{
			Type:       "takeover",
			Attributes: attrs,
		},
	}, nil
}

func (h *TakeoverHandler) resolveTakeoverReserveRow(c *fiber.Ctx, takeoverRow *takeover.TakeoverAc, flightRow *flight.Flight) *reserveac.ReserveAc {
	if takeoverRow != nil && takeoverRow.ReserveAc != nil {
		return takeoverRow.ReserveAc
	}
	if flightRow == nil || h == nil || h.reserveAc == nil || len(flightRow.ReserveAcID) != 16 {
		return nil
	}
	reserveRow, _ := h.reserveAc.GetByID(c.UserContext(), flightRow.ReserveAcID)
	return reserveRow
}

func (h *TakeoverHandler) resolveTakeoverInspectionSummary(c *fiber.Ctx, reserveRow *reserveac.ReserveAc) *shareddto.ReserveAcInspectionSummary {
	if reserveRow == nil {
		return nil
	}
	summary, _ := h.buildCreateInspectionSummary(c, reserveRow.InspectionID, reserveRow.AircraftID)
	return summary
}

func (h *TakeoverHandler) resolveTakeoverDutyResource(c *fiber.Ctx, flightRow *flight.Flight) responsedto.DutyRosterResource {
	dutyResource := responsedto.DutyRosterResource{
		Type: "duty_roster",
		Attributes: responsedto.DutyRosterAttributes{
			Bases:     responsedto.DutyRosterBaseInfo{},
			ShiftTime: responsedto.DutyRosterShiftTime{},
			Detail:    responsedto.DutyRosterDetailsResponse{Pilot: []responsedto.DutyRosterPilotCrew{}, Doctor: []responsedto.DutyRosterCrew{}, Rescuer: []responsedto.DutyRosterCrew{}, Helper: []responsedto.DutyRosterCrew{}, OtherPerson: []responsedto.DutyRosterCrew{}},
		},
	}
	if h == nil || h.db == nil || flightRow == nil || len(flightRow.TakeoverAcID) != 16 {
		return dutyResource
	}
	if rows, err := loadTakeoverRosterCrewRows(c.UserContext(), h.db, flightRow.TakeoverAcID); err == nil {
		dutyResource = buildDutyRosterResourceFromTakeoverRows(dutyResource, rows, flightRow)
	}
	if others, err := loadTakeoverOtherPeopleRows(c.UserContext(), h.db, flightRow.TakeoverAcID); err == nil {
		dutyResource = buildDutyRosterResourceFromOtherPeople(dutyResource, others)
	}
	if takeoverRow, err := h.takeoverAc.GetByID(c.UserContext(), flightRow.TakeoverAcID); err == nil && takeoverRow != nil {
		shiftTime := h.resolveTakeoverShiftTime(c, takeoverRow.BaseID, flightRow.Date)
		if takeoverRow.Base != nil {
			dutyResource.Attributes.Bases = responsedto.DutyRosterBaseInfo{
				ID:               idString(takeoverRow.BaseID),
				Type:             "hems",
				BaseName:         strings.TrimSpace(takeoverRow.Base.BaseName),
				BaseAbbreviation: strings.TrimSpace(takeoverRow.Base.BaseAbbreviation),
				ShiftTime:        shiftTime,
			}
		} else if baseInfo := h.takeoverBaseInfo(c, &dutyResource, takeoverRow.BaseID); baseInfo != nil {
			dutyResource.Attributes.Bases = *baseInfo
		}
		if shiftTime != (responsedto.DutyRosterShiftTime{}) {
			dutyResource.Attributes.ShiftTime = shiftTime
		} else if len(takeoverRow.BaseID) == 16 && h.db != nil {
			if shiftStart, shiftEnd, serr := service.ResolveBaseShiftRangeTx(h.db.WithContext(c.UserContext()), takeoverRow.BaseID, flightRow.Date); serr == nil {
				dutyResource.Attributes.ShiftTime = responsedto.DutyRosterShiftTime{ShiftStart: shiftStart, ShiftEnd: shiftEnd}
			}
		}
	}
	normalizeTakeoverDutyRosterDetail(&dutyResource)
	return dutyResource
}

func (h *TakeoverHandler) buildTakeoverResponseAttributes(
	c *fiber.Ctx,
	takeoverRow *takeover.TakeoverAc,
	flightRow *flight.Flight,
	reserveRow *reserveac.ReserveAc,
	inspectionSummary *shareddto.ReserveAcInspectionSummary,
	dutyResource responsedto.DutyRosterResource,
) responsedto.TakeoverResponseAttributes {
	shiftTime := dutyResource.Attributes.ShiftTime
	if shiftTime == (responsedto.DutyRosterShiftTime{}) {
		var dutyDate time.Time
		if flightRow != nil {
			dutyDate = flightRow.Date
		}
		shiftTime = h.resolveTakeoverShiftTime(c, takeoverRow.BaseID, dutyDate)
	}
	attrs := responsedto.TakeoverResponseAttributes{
		ID:           idString(takeoverRow.ID),
		ShiftStart:   shiftTime.ShiftStart,
		ShiftEnd:     shiftTime.ShiftEnd,
		BaseID:       idString(takeoverRow.BaseID),
		DutyDate:     dutyResource.Attributes.DutyDate,
		RosterDetail: takeoverDutyRosterDetailsFromRosterDetails(dutyResource.Attributes.Detail),
		CreatedBy:    uuidStringOrEmpty(takeoverRow.CreatedBy),
		UpdatedBy:    uuidStringOrEmpty(takeoverRow.UpdatedBy),
	}
	if !takeoverRow.CreatedAt.IsZero() {
		attrs.CreatedAt = takeoverRow.CreatedAt.UTC().Format(time.RFC3339)
	}
	if !takeoverRow.UpdatedAt.IsZero() {
		attrs.UpdatedAt = takeoverRow.UpdatedAt.UTC().Format(time.RFC3339)
	}
	if takeoverRow.Base != nil {
		attrs.Base = &responsedto.DutyRosterBaseInfo{
			ID:               idString(takeoverRow.BaseID),
			Type:             "hems",
			BaseName:         strings.TrimSpace(takeoverRow.Base.BaseName),
			BaseAbbreviation: strings.TrimSpace(takeoverRow.Base.BaseAbbreviation),
			ShiftTime:        shiftTime,
		}
	} else {
		attrs.Base = h.takeoverBaseInfo(c, &dutyResource, takeoverRow.BaseID)
	}
	if flightRow != nil {
		attrs.FlightID = idString(flightRow.ID)
	}
	attrs.Notes = takeoverRow.Notes
	if reserveRow != nil {
		attrs.ReserveAcID = idString(reserveRow.ID)
		attrs.FlightInspectionID = idString(reserveRow.InspectionID)
		if reserveRow.Aircraft != nil {
			attrs.HelicopterID = idString(reserveRow.Aircraft.ID)
		} else {
			attrs.HelicopterID = idString(reserveRow.AircraftID)
		}
		ctx := context.Background()
		if c != nil {
			ctx = c.UserContext()
		}
		attrs.Helicopter = h.takeoverHelicopterResource(ctx, reserveRow)
	}
	if inspectionSummary != nil {
		attrs.Inspection = *inspectionSummary
	}
	ctx := context.Background()
	if c != nil {
		ctx = c.UserContext()
	}
	files := takeoverRow.Files
	if len(files) == 0 && h != nil && h.db != nil {
		if loaded, err := loadTakeoverFilesByTakeoverID(ctx, h.db, takeoverRow.ID); err == nil && len(loaded) > 0 {
			files = loaded
		}
	}
	if len(files) == 0 && h != nil && h.db != nil {
		if loaded, err := loadTakeoverFilesByAttachmentRef(ctx, h.db, takeoverRow.ID); err == nil && len(loaded) > 0 {
			files = takeoverFilesFromAttachments(loaded)
		}
	}
	attrs.Files = takeoverFileResources(ctx, h.storage, files)
	return attrs
}

func loadTakeoverFilesByTakeoverID(ctx context.Context, db *gorm.DB, takeoverID []byte) ([]takeover.TakeoverFile, error) {
	rows := make([]takeover.TakeoverFile, 0)
	if db == nil || len(takeoverID) != 16 {
		return rows, nil
	}
	if err := db.WithContext(ctx).
		Where("takeover_id = ?", takeoverID).
		Order("id ASC").
		Preload("FileAttachment").
		Preload("FileAttachment.File").
		Find(&rows).Error; err != nil {
		return nil, err
	}
	return rows, nil
}

func loadTakeoverFilesByAttachmentRef(ctx context.Context, db *gorm.DB, takeoverID []byte) ([]filemanager.Attachment, error) {
	rows := make([]filemanager.Attachment, 0)
	if db == nil || len(takeoverID) != 16 {
		return rows, nil
	}
	if err := db.WithContext(ctx).
		Where("ref_type = ? AND ref_id = ?", "takeover", idString(takeoverID)).
		Order("sort_order ASC, created_at ASC, id ASC").
		Preload("File").
		Find(&rows).Error; err != nil {
		return nil, err
	}
	return rows, nil
}

func takeoverFilesFromAttachments(attachments []filemanager.Attachment) []takeover.TakeoverFile {
	if len(attachments) == 0 {
		return nil
	}
	out := make([]takeover.TakeoverFile, 0, len(attachments))
	for i := range attachments {
		att := attachments[i]
		clone := att
		out = append(out, takeover.TakeoverFile{
			ID:               append([]byte(nil), att.ID...),
			FileAttachmentID: append([]byte(nil), att.ID...),
			FileAttachment:   &clone,
		})
	}
	return out
}

func takeoverFileResources(ctx context.Context, storage fileManagerDownloadURLStorage, files []takeover.TakeoverFile) []responsedto.TakeoverFileResource {
	if len(files) == 0 {
		return nil
	}
	out := make([]responsedto.TakeoverFileResource, 0, len(files))
	for i := range files {
		// Documents created from a template (via /files/create-from-template) are attached
		// to the takeover for WOPI editing, but must not surface in the takeover files
		// listing — only regular attached files are shown there.
		if takeoverFileCreatedFromTemplate(&files[i]) {
			continue
		}
		item := responsedto.TakeoverFileResource{
			Type: "takeover_file",
			ID:   idString(files[i].ID),
			Attributes: responsedto.TakeoverFileAttributes{
				FileAttachmentID: idString(files[i].FileAttachmentID),
			},
		}
		if files[i].FileAttachment != nil && files[i].FileAttachment.File != nil {
			file := files[i].FileAttachment.File
			item.Attributes.FileName = strings.TrimSpace(file.Name)
			item.Attributes.DownloadURL = resolveFileManagerDownloadURL(ctx, storage, file)
		}
		out = append(out, item)
	}
	return out
}

// takeoverFileCreatedFromTemplate reports whether a takeover file points to a document
// instantiated from a template (identified by a non-empty template_uuid on the underlying
// file). Such files stay attached in the database for WOPI editing but are hidden from the
// takeover files listing.
func takeoverFileCreatedFromTemplate(row *takeover.TakeoverFile) bool {
	if row == nil || row.FileAttachment == nil || row.FileAttachment.File == nil {
		return false
	}
	return len(row.FileAttachment.File.TemplateUUID) == 16
}

func (h *TakeoverHandler) buildCreateInspectionSummary(c *fiber.Ctx, inspectionID []byte, helicopterID []byte) (*shareddto.ReserveAcInspectionSummary, error) {
	if h.inspect == nil || h.fileCk == nil || h.heliFile == nil || h.before == nil || h.prep == nil {
		return nil, errors.New("inspection services are not configured")
	}
	inspection, err := h.inspect.GetByID(c.UserContext(), inspectionID)
	if err != nil || inspection == nil {
		return nil, err
	}
	ckRows, err := h.fileCk.ListByInspectionID(c.UserContext(), inspectionID)
	if err != nil {
		return nil, err
	}
	isDoneByHeliFileID := make(map[string]bool, len(ckRows))
	for i := range ckRows {
		isDoneByHeliFileID[idString(ckRows[i].HelicopterFileID)] = ckRows[i].IsDone
	}
	beforeRows, err := h.heliFile.ListByHelicopterAndSection(c.UserContext(), helicopterID, helicopterfile.SectionBeforeFirstFlightInspection)
	if err != nil {
		return nil, err
	}
	prepareRows, err := h.heliFile.ListByHelicopterAndSection(c.UserContext(), helicopterID, helicopterfile.SectionFlightPreparationAndWB)
	if err != nil {
		return nil, err
	}
	beforeData, _ := h.before.GetByFlightInspectionID(c.UserContext(), inspectionID)
	prepareData, _ := h.prep.GetByFlightInspection(c.UserContext(), inspectionID)
	beforeSection := h.buildChecklistSectionSummary(c, beforeRows, isDoneByHeliFileID)
	prepareSection := h.buildChecklistSectionSummary(c, prepareRows, isDoneByHeliFileID)
	fleetStatusFileSection := h.buildFleetStatusFileSection(c, helicopterID)
	out := &shareddto.ReserveAcInspectionSummary{
		ID:             idString(inspection.ID),
		InspectionDate: calendarDateString(inspection.InspectionDate),
		Before: shareddto.ReserveAcBeforeSectionDTO{
			Total:         beforeSection.Total,
			Completed:     beforeSection.Completed,
			DoneFiles:     beforeSection.DoneFiles,
			FileChecklist: beforeSection.FileChecklist,
		},
		Prepare: shareddto.ReserveAcPrepareSectionDTO{
			Total:         prepareSection.Total,
			Completed:     prepareSection.Completed,
			DoneFiles:     prepareSection.DoneFiles,
			FileChecklist: prepareSection.FileChecklist,
		},
		FleetStatusFile: fleetStatusFileSection,
	}
	if beforeData != nil {
		out.Before.Fields = &shareddto.ReserveAcBeforeInspectionFields{
			OilEngineNR1Checked:       beforeData.OilEngineNR1Checked,
			OilEngineNR2Checked:       beforeData.OilEngineNR2Checked,
			HydraulicLHChecked:        beforeData.HydraulicLHChecked,
			HydraulicRHChecked:        beforeData.HydraulicRHChecked,
			OilTransmissionMGBChecked: beforeData.OilTransmissionMGBChecked,
			OilTransmissionIGBChecked: beforeData.OilTransmissionIGBChecked,
			OilTransmissionTGBChecked: beforeData.OilTransmissionTGBChecked,
			FuelAmount:                beforeData.FuelAmount,
			FuelUnit:                  strings.TrimSpace(beforeData.FuelUnit),
			FuelAddedAmount:           beforeData.FuelAddedAmount,
		}
	}
	if prepareData != nil {
		out.Prepare.PrepareFields = &shareddto.ReserveAcPrepareFields{
			NotamBriefing:         boolPtr(prepareData.NOTAMBriefing),
			WeatherBriefing:       boolPtr(prepareData.WeatherBriefing),
			OperationalFlightPlan: boolPtr(prepareData.OperationalFlightPlan),
		}
	}
	return out, nil
}

func (h *TakeoverHandler) buildFleetStatusFileSection(c *fiber.Ctx, helicopterID []byte) shareddto.ReserveAcFleetStatusFileSectionDTO {
	if h == nil || h.fleetStatus == nil || len(helicopterID) != 16 {
		return shareddto.ReserveAcFleetStatusFileSectionDTO{}
	}
	latest, err := h.fleetStatus.LatestByHelicopterIDs(c.UserContext(), [][]byte{helicopterID})
	if err != nil || latest == nil {
		return shareddto.ReserveAcFleetStatusFileSectionDTO{}
	}
	row := latest[string(helicopterID)]
	if row == nil || len(row.Files) == 0 {
		return shareddto.ReserveAcFleetStatusFileSectionDTO{}
	}
	out := shareddto.ReserveAcFleetStatusFileSectionDTO{
		Total:         len(row.Files),
		Completed:     len(row.Files),
		FileChecklist: make([]shareddto.ReserveAcFileChecklistItemDTO, 0, len(row.Files)),
		DoneFiles:     make([]shareddto.ReserveAcFileChecklistItemDTO, 0, len(row.Files)),
	}
	for i := range row.Files {
		item := shareddto.ReserveAcFileChecklistItemDTO{
			HelicopterFileID: idString(row.Files[i].ID),
			FileAttachmentID: idString(row.Files[i].FileAttachmentID),
			IsDone:           true,
			IsMandatory:      row.Files[i].IsMandatory,
		}
		if row.Files[i].Attachment != nil && row.Files[i].Attachment.File != nil {
			file := row.Files[i].Attachment.File
			item.FileName = strings.TrimSpace(file.Name)
			if h.storage != nil {
				asset := resolveFileManagerDownloadURLs(c.UserContext(), h.storage, file)
				item.DownloadURL = strings.TrimSpace(asset.OriginalURL)
			}
		}
		out.FileChecklist = append(out.FileChecklist, item)
		out.DoneFiles = append(out.DoneFiles, item)
	}
	return out
}

func flightTakeoverStep(ctx context.Context, row *flight.Flight, takeoverSvc takeoverLookupService) responsedto.FlightStepItem {
	if row == nil || len(row.TakeoverAcID) != 16 {
		return responsedto.FlightStepItem{
			Phase:    "takeover",
			Position: "takeover",
			Step:     1,
			Complete: false,
			Status:   "unassigned",
		}
	}
	takeoverRow := resolveTakeoverRow(ctx, row, takeoverSvc)
	if !takeoverRowIsComplete(takeoverRow) {
		return responsedto.FlightStepItem{
			Phase:    "takeover",
			Position: "takeover",
			Step:     1,
			Complete: false,
			Status:   "in_progress",
			ID:       idString(row.TakeoverAcID),
		}
	}
	return responsedto.FlightStepItem{
		Phase:       "takeover",
		Position:    "takeover",
		Step:        1,
		Complete:    true,
		Status:      "completed",
		ID:          idString(row.TakeoverAcID),
		CompletedAt: row.CreatedAt.UTC().Format(time.RFC3339),
		CompletedBy: takeoverCompletedBy(takeoverRow),
	}
}

func takeoverCompletedBy(takeoverRow *takeover.TakeoverAc) string {
	if takeoverRow == nil {
		return ""
	}
	if short := strings.TrimSpace(userctx.GetShortName(context.Background(), takeoverRow.UpdatedBy)); short != "" {
		return short
	}
	if short := strings.TrimSpace(userctx.GetShortName(context.Background(), takeoverRow.CreatedBy)); short != "" {
		return short
	}
	for i := range takeoverRow.RosterCrews {
		crew := takeoverRow.RosterCrews[i]
		if !strings.EqualFold(strings.TrimSpace(crew.RoleCode), "pilot") {
			continue
		}
		if short := strings.TrimSpace(userctx.GetShortName(context.Background(), crew.UserID)); short != "" {
			return short
		}
		if label := strings.TrimSpace(crew.NameLabel); label != "" {
			return label
		}
	}
	return ""
}

// resolveTakeoverRow loads the takeover record for a flight, preferring the
// lookup service and falling back to the row's eagerly-loaded association.
// Returns nil when the id is missing or the lookup errors.
func resolveTakeoverRow(ctx context.Context, row *flight.Flight, takeoverSvc takeoverLookupService) *takeover.TakeoverAc {
	if row == nil || len(row.TakeoverAcID) != 16 {
		return nil
	}
	var takeoverRow *takeover.TakeoverAc
	if takeoverSvc != nil {
		var err error
		takeoverRow, err = takeoverSvc.GetByID(ctx, row.TakeoverAcID)
		if err != nil {
			return nil
		}
	}
	if takeoverRow == nil {
		takeoverRow = row.Takeover
	}
	return takeoverRow
}

func takeoverRowIsComplete(takeoverRow *takeover.TakeoverAc) bool {
	if takeoverRow == nil {
		return false
	}
	if len(takeoverRow.BaseID) != 16 {
		return false
	}
	if len(takeoverRow.RosterCrews) == 0 && len(takeoverRow.OtherPeople) == 0 {
		return false
	}
	if takeoverRow.ReserveAc == nil {
		return false
	}
	if len(takeoverRow.ReserveAc.AircraftID) != 16 || takeoverRow.ReserveAc.Aircraft == nil {
		return false
	}
	if takeoverRow.ReserveAc.Inspection == nil {
		return false
	}
	return true
}

func takeoverDutyRosterResponse(resource *responsedto.DutyRosterResource, flightRow *flight.Flight) *responsedto.TakeoverDutyRosterResource {
	if resource == nil {
		return nil
	}
	helicopter := resource.Attributes.Helicopter
	if helicopter.ID == "" && helicopter.Designation == "" && helicopter.Identifier == "" && helicopter.Type == "" {
		if fallback := takeoverHelicopterFromFlight(flightRow); fallback != nil {
			helicopter = *fallback
		}
	}
	return &responsedto.TakeoverDutyRosterResource{
		Type: resource.Type,
		ID:   resource.ID,
		Attributes: responsedto.TakeoverDutyRosterAttributes{
			Bases:      resource.Attributes.Bases,
			FlightID:   resource.Attributes.FlightID,
			Helicopter: helicopter,
			DutyDate:   resource.Attributes.DutyDate,
			Detail:     takeoverDutyRosterDetailsFromRosterDetails(resource.Attributes.Detail),
			CreatedBy:  resource.Attributes.CreatedBy,
			CreatedAt:  resource.Attributes.CreatedAt,
			UpdatedAt:  resource.Attributes.UpdatedAt,
		},
	}
}

func (h *TakeoverHandler) takeoverBaseInfo(c *fiber.Ctx, resource *responsedto.DutyRosterResource, baseID []byte) *responsedto.DutyRosterBaseInfo {
	if resource != nil {
		base := resource.Attributes.Bases
		if base.ID != "" || base.Type != "" || base.BaseName != "" || base.BaseAbbreviation != "" {
			if base.ShiftTime == (responsedto.DutyRosterShiftTime{}) {
				if shift := h.resolveTakeoverShiftTime(c, baseID, parseDutyRosterDate(resource.Attributes.DutyDate)); shift != (responsedto.DutyRosterShiftTime{}) {
					base.ShiftTime = shift
				}
			}
			if base.ShiftTime == (responsedto.DutyRosterShiftTime{}) {
				base.ShiftTime = resource.Attributes.ShiftTime
			}
			return &base
		}
	}
	if len(baseID) == 0 {
		return nil
	}
	out := &responsedto.DutyRosterBaseInfo{ID: idString(baseID)}
	if row := h.takeoverBaseRow(c, baseID, "hems", "regular"); row != nil {
		out.Type = takeoverBaseType(row)
		out.BaseName = strings.TrimSpace(row.BaseName)
		out.BaseAbbreviation = strings.TrimSpace(row.BaseAbbreviation)
	}
	if resource != nil {
		out.ShiftTime = h.resolveTakeoverShiftTime(c, baseID, parseDutyRosterDate(resource.Attributes.DutyDate))
		if out.ShiftTime == (responsedto.DutyRosterShiftTime{}) {
			out.ShiftTime = resource.Attributes.ShiftTime
		}
	}
	return out
}

func (h *TakeoverHandler) takeoverBaseRow(c *fiber.Ctx, baseID []byte, categories ...string) *basedomain.Base {
	if h == nil || h.base == nil || len(baseID) != 16 {
		return nil
	}
	ctx := context.Background()
	if c != nil {
		ctx = c.UserContext()
	}
	tried := map[string]struct{}{}
	for _, category := range categories {
		category = strings.TrimSpace(category)
		if category == "" {
			continue
		}
		if _, ok := tried[category]; ok {
			continue
		}
		tried[category] = struct{}{}
		row, err := h.base.GetBaseByID(ctx, baseID, category)
		if err == nil && row != nil {
			return row
		}
	}
	return nil
}

func takeoverBaseType(row *basedomain.Base) string {
	if row == nil {
		return ""
	}
	if normalized, ok := basedomain.NormalizeCategoryType(row.BaseCategory.Key); ok {
		return normalized
	}
	return ""
}

func (h *TakeoverHandler) resolveTakeoverShiftTime(c *fiber.Ctx, baseID []byte, dutyDate time.Time) responsedto.DutyRosterShiftTime {
	if h == nil || h.db == nil || len(baseID) != 16 || dutyDate.IsZero() {
		return responsedto.DutyRosterShiftTime{}
	}
	ctx := context.Background()
	if c != nil {
		ctx = c.UserContext()
	}
	shiftStart, shiftEnd, err := service.ResolveBaseShiftRangeTx(h.db.WithContext(ctx), baseID, dutyDate)
	if err != nil || shiftStart == "" || shiftEnd == "" {
		return responsedto.DutyRosterShiftTime{}
	}
	return responsedto.DutyRosterShiftTime{ShiftStart: shiftStart, ShiftEnd: shiftEnd}
}

func parseDutyRosterDate(value string) time.Time {
	parsed, err := time.Parse("2006-01-02", strings.TrimSpace(value))
	if err != nil {
		return time.Time{}
	}
	return parsed
}

func calendarDateString(value time.Time) string {
	if value.IsZero() {
		return ""
	}
	return time.Date(value.Year(), value.Month(), value.Day(), 0, 0, 0, 0, time.UTC).Format("2006-01-02")
}

func takeoverHelicopterInfo(resource *responsedto.DutyRosterResource, helicopterID []byte) *responsedto.DutyRosterHelicopterSummary {
	if resource != nil {
		heli := resource.Attributes.Helicopter
		if heli.ID != "" || heli.Designation != "" || heli.Identifier != "" || heli.Type != "" {
			return &heli
		}
	}
	if len(helicopterID) == 0 {
		return nil
	}
	return &responsedto.DutyRosterHelicopterSummary{ID: idString(helicopterID)}
}

// takeoverHelicopterResource builds the selected helicopter for a takeover,
// enriched with its last daily inspection and its maintenance schedule (the
// "next due" per inspection type) from the helicopter's latest fleet status.
func (h *TakeoverHandler) takeoverHelicopterResource(ctx context.Context, row *reserveac.ReserveAc) *responsedto.TakeoverHelicopter {
	if row == nil {
		return nil
	}
	helicopterID := row.AircraftID
	heli := responsedto.TakeoverHelicopter{ID: idString(row.AircraftID)}
	if row.Aircraft != nil {
		helicopterID = row.Aircraft.ID
		heli.ID = idString(row.Aircraft.ID)
		heli.Designation = strings.TrimSpace(row.Aircraft.Designation)
		heli.Identifier = strings.TrimSpace(row.Aircraft.Identifier)
		heli.Type = strings.TrimSpace(row.Aircraft.Type)
	}
	if heli.ID == "" && heli.Designation == "" && heli.Identifier == "" && heli.Type == "" {
		return nil
	}
	// Last daily inspection: reserveAc concretely implements the provider.
	if provider, ok := h.reserveAc.(helicopterLastDailyInspectionProvider); ok {
		heli.LastDailyInspection = helicopterLastDailyInspectionResource(ctx, provider, helicopterID)
	}
	heli.MaintenanceSchedules = h.takeoverMaintenanceSchedules(ctx, helicopterID)
	return &heli
}

// takeoverMaintenanceSchedules returns the full inspection-type set (with due/ext)
// from the helicopter's latest fleet status; empty entries when none exists.
func (h *TakeoverHandler) takeoverMaintenanceSchedules(ctx context.Context, helicopterID []byte) []shareddto.MaintenanceScheduleAttributes {
	var schedules []fleetstatus.MaintenanceSchedule
	if h.fleetStatus != nil && len(helicopterID) == 16 {
		if latest, err := h.fleetStatus.LatestByHelicopterIDs(ctx, [][]byte{helicopterID}); err == nil {
			if fs := latest[string(helicopterID)]; fs != nil {
				schedules = fs.MaintenanceSchedules
			}
		}
	}
	return fleetStatusMaintenanceSchedules(schedules)
}

func takeoverHelicopterFromFlight(row *flight.Flight) *responsedto.DutyRosterHelicopterSummary {
	return nil
}

func (h *TakeoverHandler) buildChecklistSectionSummary(c *fiber.Ctx, files []helicopterfile.HelicopterFile, isDoneByHeliFileID map[string]bool) reserveAcChecklistSectionCommon {
	out := reserveAcChecklistSectionCommon{
		FileChecklist: make([]shareddto.ReserveAcFileChecklistItemDTO, 0, len(files)),
		DoneFiles:     make([]shareddto.ReserveAcFileChecklistItemDTO, 0, len(files)),
	}
	for i := range files {
		if takeoverChecklistFileCreatedFromTemplate(&files[i]) {
			continue
		}
		fileID := idString(files[i].ID)
		isDone := isDoneByHeliFileID[fileID]
		if isDone {
			out.Completed++
		}
		out.Total++
		fileAttachmentID := ""
		fileName := ""
		if files[i].FileAttachment != nil {
			fileAttachmentID = idString(files[i].FileAttachment.ID)
			if files[i].FileAttachment.File != nil {
				fileName = strings.TrimSpace(files[i].FileAttachment.File.Name)
			}
		}
		downloadURL := ""
		if files[i].FileAttachment != nil && files[i].FileAttachment.File != nil {
			downloadURL = resolveFileManagerDownloadURL(c.UserContext(), h.storage, files[i].FileAttachment.File)
		}
		item := shareddto.ReserveAcFileChecklistItemDTO{
			HelicopterFileID: fileID,
			FileAttachmentID: fileAttachmentID,
			FileName:         fileName,
			DownloadURL:      downloadURL,
			IsDone:           isDone,
		}
		out.FileChecklist = append(out.FileChecklist, item)
		if item.IsDone {
			out.DoneFiles = append(out.DoneFiles, item)
		}
	}
	return out
}

// takeoverChecklistFileCreatedFromTemplate reports whether a helicopter file row is a
// document instantiated from a template. Those rows should remain hidden from checklist
// summaries, which only surface the template catalog entries themselves.
func takeoverChecklistFileCreatedFromTemplate(row *helicopterfile.HelicopterFile) bool {
	if row == nil {
		return false
	}
	if row.SourceFile != nil {
		return len(row.SourceFile.TemplateUUID) == 16
	}
	if row.FileAttachment != nil && row.FileAttachment.File != nil {
		return len(row.FileAttachment.File.TemplateUUID) == 16
	}
	return false
}
