package handlers

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

	"github.com/gofiber/fiber/v2"

	afterflightinspection "wucher/internal/domain/after_flight_inspection"
	authdomain "wucher/internal/domain/auth"
	"wucher/internal/domain/flight"
	flightdata "wucher/internal/domain/flight_data"
	fmreport "wucher/internal/domain/fm_report"
	helicopterdomain "wucher/internal/domain/helicopter"
	"wucher/internal/domain/mission"
	reserveac "wucher/internal/domain/reserve_ac"
	"wucher/internal/service"
	sharedconst "wucher/internal/shared/const"
	"wucher/internal/shared/pkg/appctx"
	"wucher/internal/shared/pkg/apperrorsx"
	"wucher/internal/shared/pkg/reserveacutil"
	"wucher/internal/shared/pkg/userctx"
	"wucher/internal/shared/pkg/uuidv7"
	requestdto "wucher/internal/transport/http/dto/request"
	responsedto "wucher/internal/transport/http/dto/response"
	"wucher/internal/transport/http/jsonapi"
	"wucher/internal/transport/http/response"
	"wucher/internal/transport/http/validators"
)

type FlightHandler struct {
	svc                   flight.FlightService
	afterFlightInspection afterflightinspection.Service
	fmReport              fmreport.Service
	takeover              takeoverLookupService
	reserveAc             reserveac.ReserveAcService
	helicopter            helicopterdomain.Service
	mission               mission.Service
	flightData            flightdata.Service
	auth                  *service.AuthService
	validate              *validators.Validator
}

func NewFlightHandler(svc flight.FlightService) *FlightHandler {
	return &FlightHandler{
		svc:      svc,
		validate: validators.New(),
	}
}

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

func (h *FlightHandler) WithAfterFlightInspectionService(afterSvc afterflightinspection.Service) *FlightHandler {
	h.afterFlightInspection = afterSvc
	return h
}

func (h *FlightHandler) WithFMReportService(reportSvc fmreport.Service) *FlightHandler {
	h.fmReport = reportSvc
	return h
}

func (h *FlightHandler) WithTakeoverService(takeoverSvc takeoverLookupService) *FlightHandler {
	h.takeover = takeoverSvc
	return h
}

func (h *FlightHandler) WithHelicopterService(heliSvc helicopterdomain.Service) *FlightHandler {
	h.helicopter = heliSvc
	return h
}

func (h *FlightHandler) WithMissionService(missionSvc mission.Service) *FlightHandler {
	h.mission = missionSvc
	return h
}

func (h *FlightHandler) WithFlightDataService(flightDataSvc flightdata.Service) *FlightHandler {
	h.flightData = flightDataSvc
	return h
}

func (h *FlightHandler) WithAuthService(authSvc *service.AuthService) *FlightHandler {
	h.auth = authSvc
	return h
}

// Create keeps internal compatibility but is no longer exposed as public HTTP route.
func (h *FlightHandler) Create(c *fiber.Ctx) error {
	var req requestdto.FlightCreateRequest
	if err := c.BodyParser(&req); err != nil {
		return writeFlightErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "Invalid JSON",
			Detail: safeInternalDetail(),
		}})
	}
	if errs := h.validate.ValidateStruct(req); errs != nil {
		return writeFlightErrors(c, fiber.StatusUnprocessableEntity, errs)
	}

	date, err := time.Parse("2006-01-02", strings.TrimSpace(req.Data.Attributes.Date))
	if err != nil {
		return writeFlightErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
			Status: "422",
			Title:  "Validation error",
			Detail: "date must be YYYY-MM-DD",
			Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/date"},
		}})
	}
	if _, errObj := parseOptionalUUIDFlight(req.Data.Attributes.DutyRosterID, "/data/attributes/duty_roster_id"); errObj != nil {
		return writeFlightErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
	}
	if _, errObj := parseOptionalUUIDFlight(req.Data.Attributes.ReserveAcID, "/data/attributes/reserve_ac_id"); errObj != nil {
		return writeFlightErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
	}

	row := &flight.Flight{
		Date:      date.UTC(),
		CreatedBy: actorUserID(c),
		UpdatedBy: actorUserID(c),
	}

	if err := h.svc.Create(c.UserContext(), row); err != nil {
		return writeFlightErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "Create failed",
			Detail: safeInternalDetail(),
		}})
	}

	created, err := h.svc.GetByID(c.UserContext(), row.ID)
	if err == nil && created != nil {
		return response.Write(c, fiber.StatusCreated, h.flightMutationResource(c.UserContext(), created))
	}
	return response.Write(c, fiber.StatusCreated, h.flightMutationResource(c.UserContext(), row))
}

// GetFlightsByAuthUser godoc
// @Summary Get flights for current auth user (from duty roster assignment)
// @Tags Flight
// @Produce json
// @Description Returns flights assigned to current auth user, including workflow steps (`duty_roster`, `reserve_ac`, `mission`, `after_flight_inspection`, `fm_report`).
// @Description Each step includes `status`, `completed_at`, and `completed_by` for consistent FE rendering.
// @Param page[number] query int false "Page number"
// @Param page[size] query int false "Page size"
// @Param page query int false "Page number (fallback)"
// @Param size query int false "Page size (fallback)"
// @Param limit query int false "Page size override"
// @Success 200 {object} responsedto.FlightListResponse
// @Failure 401 {object} jsonapi.ErrorDocument
// @Router /api/v1/flights/get [get]
func (h *FlightHandler) GetByAuthUser(c *fiber.Ctx) error {
	userID := actorUserID(c)
	if len(userID) == 0 {
		return writeFlightErrors(c, fiber.StatusUnauthorized, []jsonapi.ErrorObject{{
			Status: "401",
			Title:  "Unauthorized",
			Detail: "missing auth context",
		}})
	}

	pageNumber := parseIntDefault(c.Query("page[number]"), 0)
	if pageNumber <= 0 {
		pageNumber = parseIntDefault(c.Query("page"), 1)
	}
	pageSize := parseIntDefault(c.Query("page[size]"), 0)
	if pageSize <= 0 {
		pageSize = parseIntDefault(c.Query("limit"), 0)
	}
	if pageSize <= 0 {
		pageSize = parseIntDefault(c.Query("size"), 20)
	}
	if pageSize > 100 {
		pageSize = 100
	}
	if pageSize < 1 {
		pageSize = 20
	}
	if pageNumber < 1 {
		pageNumber = 1
	}

	offset := (pageNumber - 1) * pageSize
	_ = offset
	rows, total, err := h.svc.ListByUserID(c.UserContext(), userID, pageSize, offset)
	if err != nil {
		return writeFlightErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "Get failed",
			Detail: safeInternalDetail(),
		}})
	}
	missionsByFlightID, err := h.prefetchMissionsByFlightID(c.UserContext(), rows)
	if err != nil {
		return writeFlightErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "List failed",
			Detail: safeInternalDetail(),
		}})
	}

	dailyInspectionCache := make(map[string]*responsedto.FlightDailyInspectionSummary)
	afterFlightCache := make(map[string]responsedto.FlightStepItem)
	fmReportCache := make(map[string]responsedto.FlightStepItem)

	data := make([]responsedto.FlightResource, 0, len(rows))
	for i := range rows {
		data = append(data, h.flightResourceForList(c.UserContext(), c, &rows[i], missionsByFlightID[string(rows[i].ID)], dailyInspectionCache, afterFlightCache, fmReportCache))
	}
	return response.WriteWithMeta(c, fiber.StatusOK, data, map[string]any{
		"total": total,
	})
}

// ListFlights godoc
// @Summary List flights
// @Description Supports `search`, `filter[search]`, `sort`, `page[number]`, `page[size]`, `page`, `size`, and `limit`.
// @Description Supports `date` and `filter[date]` for exact flight date filtering (`YYYY-MM-DD`).
// @Description Returns paginated flights with workflow steps (`takeover`, `mission`, `after_flight_inspection`, `fm_report`).
// @Description Each step includes `status`, `completed_at`, and `completed_by` for consistent FE rendering.
// @Tags Flight
// @Produce json
// @Param search query string false "Search text"
// @Param filter[search] query string false "Search text (JSON:API style)"
// @Param date query string false "Filter by flight date (YYYY-MM-DD)"
// @Param filter[date] query string false "Filter by flight date (YYYY-MM-DD) (JSON:API style)"
// @Param sort query string false "Sort field"
// @Param page[number] query int false "Page number"
// @Param page[size] query int false "Page size"
// @Param page query int false "Page number (fallback)"
// @Param size query int false "Page size (fallback)"
// @Param limit query int false "Page size override"
// @Success 200 {object} responsedto.FlightListResponse
// @Failure 400 {object} jsonapi.ErrorDocument
// @Router /api/v1/flights/get-all [get]
func (h *FlightHandler) List(c *fiber.Ctx) error {
	filter := c.Query("filter[search]")
	if filter == "" {
		filter = c.Query("search")
	}
	dateFilter := strings.TrimSpace(c.Query("filter[date]"))
	if dateFilter == "" {
		dateFilter = strings.TrimSpace(c.Query("date"))
	}
	if dateFilter != "" {
		if _, err := time.Parse("2006-01-02", dateFilter); err != nil {
			return writeFlightErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
				Status: "422",
				Title:  "Validation error",
				Detail: "date must be YYYY-MM-DD",
				Source: &jsonapi.ErrorSource{Pointer: "/date"},
			}})
		}
	}

	pageNumber := parseIntDefault(c.Query("page[number]"), 0)
	if pageNumber <= 0 {
		pageNumber = parseIntDefault(c.Query("page"), 1)
	}
	pageSize := parseIntDefault(c.Query("page[size]"), 0)
	if pageSize <= 0 {
		pageSize = parseIntDefault(c.Query("limit"), 0)
	}
	if pageSize <= 0 {
		pageSize = parseIntDefault(c.Query("size"), 20)
	}
	if pageSize > 100 {
		pageSize = 100
	}
	if pageSize < 1 {
		pageSize = 20
	}
	if pageNumber < 1 {
		pageNumber = 1
	}

	offset := (pageNumber - 1) * pageSize
	_ = offset
	sort := c.Query("sort")
	rows, total, err := h.listFlightsForCurrentUser(c, filter, dateFilter, sort, 0, 0)
	if err != nil {
		return writeFlightErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "List failed",
			Detail: safeInternalDetail(),
		}})
	}

	data := make([]responsedto.FlightResource, 0, len(rows))
	for i := range rows {
		data = append(data, h.flightResource(c.UserContext(), c, &rows[i]))
	}
	return response.WriteWithMeta(c, fiber.StatusOK, data, map[string]any{
		"total": total,
	})
}

func (h *FlightHandler) listFlightsForCurrentUser(c *fiber.Ctx, filter, date, sort string, limit, offset int) ([]flight.Flight, int64, error) {
	if h == nil || h.svc == nil {
		return nil, 0, errors.New("flight service not configured")
	}
	if h.auth == nil {
		return h.svc.List(c.UserContext(), filter, date, sort, limit, offset)
	}
	userID := appctx.GetUserID(c.UserContext())
	if len(userID) == 0 {
		return nil, 0, errors.New("missing auth context")
	}
	user, err := h.auth.GetUserByID(c.UserContext(), userID)
	if err != nil {
		return nil, 0, err
	}
	if user == nil {
		return nil, 0, errors.New("user not found")
	}
	if flightUserHasRole(user, "admin") {
		return h.svc.List(c.UserContext(), filter, date, sort, limit, offset)
	}
	return h.svc.ListByCreatedBy(c.UserContext(), userID, filter, date, sort, limit, offset)
}

func flightUserHasRole(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 *FlightHandler) flightResource(ctx context.Context, c *fiber.Ctx, row *flight.Flight) responsedto.FlightResource {
	missionStep := flightMissionStep(row)
	takeoverStep := flightTakeoverStep(c.Context(), row, h.takeover)
	afterFlightInspectionStep := h.flightAfterFlightInspectionStep(ctx, row)
	fmReportStep := h.flightFMReportStep(ctx, row)
	var takeover *responsedto.FlightTakeoverSummary
	var draft []responsedto.FlightDraftGroup
	totalDraft := 0
	if row != nil && len(row.TakeoverAcID) == 16 {
		takeover = flightTakeoverSummary(ctx, h.reserveAc, row)
		if takeover == nil {
			takeover = &responsedto.FlightTakeoverSummary{}
		}
		takeover.ID = idString(row.TakeoverAcID)
		takeover.Helicopter = h.helicopterSummaryFromTakeover(ctx, row)
		takeover.RosterDetail = h.takeoverRosterDetailSummary(ctx, row)
	}
	missionID := ""
	if h != nil && h.mission != nil && row != nil && len(row.ID) == 16 {
		rows, err := h.mission.ListByFlightID(ctx, row.ID)
		if err == nil && len(rows) > 0 {
			missionID = idString(rows[0].ID)
			grouped := make(map[string][]responsedto.FlightDraftItem)
			summaries := make(map[string]responsedto.FlightMissionTypeSummary)
			missionComplete := true
			var completedAt time.Time
			var completedBy []byte
			for i := range rows {
				rowComplete := h.missionFlightDataComplete(ctx, rows[i].ID)
				if !rowComplete {
					missionComplete = false
				}
				if rowComplete && rows[i].UpdatedAt.After(completedAt) {
					completedAt = rows[i].UpdatedAt
					completedBy = rows[i].UpdatedBy
				}
				missionType := strings.ToUpper(strings.TrimSpace(rows[i].Type))
				status := mission.StatusDraft
				if rowComplete {
					status = mission.StatusCompleted
				}
				grouped[missionType] = append(grouped[missionType], responsedto.FlightDraftItem{
					ID:           idString(rows[i].ID),
					Type:         missionType,
					Status:       status,
					FlightDataID: idString(rows[i].FlightDataID),
				})
				sum := summaries[missionType]
				sum.Type = missionType
				sum.Draft++
				if status == mission.StatusCompleted {
					sum.Completed++
				}
				summaries[missionType] = sum
			}
			for _, missionType := range []string{sharedconst.MissionTypeHEMS, sharedconst.MissionTypeCAT, sharedconst.MissionTypeSPO, sharedconst.MissionTypeNCO} {
				items := grouped[missionType]
				if len(items) == 0 {
					continue
				}
				draft = append(draft, responsedto.FlightDraftGroup{
					Type:  missionType,
					Items: items,
				})
				totalDraft += len(items)
			}
			missionStep.ID = missionID
			missionStep.Status = "in_progress"
			if missionComplete {
				missionStep.Status = "completed"
			}
			missionStep.Complete = missionComplete
			if missionComplete && !completedAt.IsZero() {
				missionStep.CompletedAt = completedAt.UTC().Format(time.RFC3339)
				missionStep.CompletedBy = userctx.GetDisplayName(ctx, completedBy)
			}
			missionStep.MissionType = make([]responsedto.FlightMissionTypeSummary, 0, len(draft))
			for _, missionType := range []string{sharedconst.MissionTypeHEMS, sharedconst.MissionTypeCAT, sharedconst.MissionTypeSPO, sharedconst.MissionTypeNCO} {
				if sum, ok := summaries[missionType]; ok {
					missionStep.MissionType = append(missionStep.MissionType, sum)
				}
			}
		}
	}
	steps := []responsedto.FlightStepItem{missionStep}
	if row != nil && len(row.TakeoverAcID) == 16 {
		steps = []responsedto.FlightStepItem{takeoverStep, missionStep}
		if afterFlightInspectionStep.Phase != "" {
			steps = append(steps, afterFlightInspectionStep)
		}
		if fmReportStep.Phase != "" {
			steps = append(steps, fmReportStep)
		}
	}

	updatedAt, updatedBy := updatedAuditOrCreated(row.UpdatedAt, row.CreatedAt, row.UpdatedBy, row.UpdatedByName, row.CreatedBy, row.CreatedByName)
	return responsedto.FlightResource{
		Type: "flight",
		ID:   idString(row.ID),
		Attributes: responsedto.FlightAttributes{
			MissionID:  missionID,
			Code:       stringPtrOrEmpty(row.MissionCode),
			Date:       row.Date.UTC().Format("2006-01-02"),
			TotalDraft: totalDraft,
			Takeover:   takeover,
			Steps:      steps,
			Draft:      draft,
			CreatedAt:  row.CreatedAt.UTC().Format(time.RFC3339),
			CreatedBy:  userRef(row.CreatedBy, row.CreatedByName),
			UpdatedAt:  updatedAt,
			UpdatedBy:  updatedBy,
			DeletedAt:  timeStringOrEmpty(row.DeletedAt),
			DeletedBy:  userRef(row.DeletedBy, ""),
		},
	}
}

func (h *FlightHandler) flightResourceForList(ctx context.Context, c *fiber.Ctx, row *flight.Flight, missions []mission.Mission, dailyInspectionCache map[string]*responsedto.FlightDailyInspectionSummary, afterFlightCache map[string]responsedto.FlightStepItem, fmReportCache map[string]responsedto.FlightStepItem) responsedto.FlightResource {
	missionStep := flightMissionStep(row)
	takeoverStep := flightTakeoverStep(c.Context(), row, nil)
	afterFlightInspectionStep := h.flightAfterFlightInspectionStepCached(ctx, row, afterFlightCache)
	fmReportStep := h.flightFMReportStepCached(ctx, row, fmReportCache)
	var takeover *responsedto.FlightTakeoverSummary
	var draft []responsedto.FlightDraftGroup
	totalDraft := 0
	if row != nil && len(row.TakeoverAcID) == 16 {
		takeover = flightTakeoverSummaryForList(ctx, h.reserveAc, row, dailyInspectionCache)
		if takeover == nil {
			takeover = &responsedto.FlightTakeoverSummary{}
		}
		takeover.ID = idString(row.TakeoverAcID)
		takeover.Helicopter = h.helicopterSummaryFromTakeoverForList(ctx, row)
		takeover.RosterDetail = h.takeoverRosterDetailSummary(ctx, row)
	}
	missionID := ""
	if len(missions) > 0 {
		missionID = idString(missions[0].ID)
		grouped := make(map[string][]responsedto.FlightDraftItem)
		summaries := make(map[string]responsedto.FlightMissionTypeSummary)
		missionComplete := true
		var completedAt time.Time
		var completedBy []byte
		for i := range missions {
			rowComplete := strings.EqualFold(strings.TrimSpace(missions[i].FlightDataStatus), flightdata.StatusCompleted)
			if !rowComplete {
				missionComplete = false
			}
			if rowComplete && missions[i].UpdatedAt.After(completedAt) {
				completedAt = missions[i].UpdatedAt
				completedBy = missions[i].UpdatedBy
			}
			missionType := strings.ToUpper(strings.TrimSpace(missions[i].Type))
			status := mission.StatusDraft
			if rowComplete {
				status = mission.StatusCompleted
			}
			grouped[missionType] = append(grouped[missionType], responsedto.FlightDraftItem{
				ID:           idString(missions[i].ID),
				Type:         missionType,
				Status:       status,
				FlightDataID: idString(missions[i].FlightDataID),
			})
			sum := summaries[missionType]
			sum.Type = missionType
			sum.Draft++
			if status == mission.StatusCompleted {
				sum.Completed++
			}
			summaries[missionType] = sum
		}
		for _, missionType := range []string{sharedconst.MissionTypeHEMS, sharedconst.MissionTypeCAT, sharedconst.MissionTypeSPO, sharedconst.MissionTypeNCO} {
			items := grouped[missionType]
			if len(items) == 0 {
				continue
			}
			draft = append(draft, responsedto.FlightDraftGroup{
				Type:  missionType,
				Items: items,
			})
			totalDraft += len(items)
		}
		missionStep.ID = missionID
		missionStep.Status = "in_progress"
		if missionComplete {
			missionStep.Status = "completed"
		}
		missionStep.Complete = missionComplete
		if missionComplete && !completedAt.IsZero() {
			missionStep.CompletedAt = completedAt.UTC().Format(time.RFC3339)
			missionStep.CompletedBy = userctx.GetDisplayName(ctx, completedBy)
		}
		missionStep.MissionType = make([]responsedto.FlightMissionTypeSummary, 0, len(draft))
		for _, missionType := range []string{sharedconst.MissionTypeHEMS, sharedconst.MissionTypeCAT, sharedconst.MissionTypeSPO, sharedconst.MissionTypeNCO} {
			if sum, ok := summaries[missionType]; ok {
				missionStep.MissionType = append(missionStep.MissionType, sum)
			}
		}
	}
	steps := []responsedto.FlightStepItem{missionStep}
	if row != nil && len(row.TakeoverAcID) == 16 {
		steps = []responsedto.FlightStepItem{takeoverStep, missionStep}
		if afterFlightInspectionStep.Phase != "" {
			steps = append(steps, afterFlightInspectionStep)
		}
		if fmReportStep.Phase != "" {
			steps = append(steps, fmReportStep)
		}
	}

	updatedAt, updatedBy := updatedAuditOrCreated(row.UpdatedAt, row.CreatedAt, row.UpdatedBy, row.UpdatedByName, row.CreatedBy, row.CreatedByName)
	return responsedto.FlightResource{
		Type: "flight",
		ID:   idString(row.ID),
		Attributes: responsedto.FlightAttributes{
			MissionID:  missionID,
			Code:       stringPtrOrEmpty(row.MissionCode),
			Date:       row.Date.UTC().Format("2006-01-02"),
			TotalDraft: totalDraft,
			Takeover:   takeover,
			Steps:      steps,
			Draft:      draft,
			CreatedAt:  row.CreatedAt.UTC().Format(time.RFC3339),
			CreatedBy:  userRef(row.CreatedBy, row.CreatedByName),
			UpdatedAt:  updatedAt,
			UpdatedBy:  updatedBy,
			DeletedAt:  timeStringOrEmpty(row.DeletedAt),
			DeletedBy:  userRef(row.DeletedBy, ""),
		},
	}
}

func (h *FlightHandler) prefetchMissionsByFlightID(ctx context.Context, rows []flight.Flight) (map[string][]mission.Mission, error) {
	if h == nil || h.mission == nil || len(rows) == 0 {
		return map[string][]mission.Mission{}, nil
	}
	flightIDs := make([][]byte, 0, len(rows))
	for i := range rows {
		if len(rows[i].ID) == 16 {
			flightIDs = append(flightIDs, rows[i].ID)
		}
	}
	if len(flightIDs) == 0 {
		return map[string][]mission.Mission{}, nil
	}
	missions, err := h.mission.ListByFlightIDs(ctx, flightIDs)
	if err != nil {
		return nil, err
	}
	grouped := make(map[string][]mission.Mission, len(flightIDs))
	for i := range missions {
		key := string(missions[i].FlightID)
		grouped[key] = append(grouped[key], missions[i])
	}
	return grouped, nil
}

func (h *FlightHandler) flightAfterFlightInspectionStepCached(ctx context.Context, row *flight.Flight, cache map[string]responsedto.FlightStepItem) responsedto.FlightStepItem {
	if row == nil || row.Takeover == nil || row.Takeover.ReserveAc == nil || row.Takeover.ReserveAc.Inspection == nil || len(row.Takeover.ReserveAc.Inspection.ID) != 16 {
		return responsedto.FlightStepItem{}
	}
	inspectionID := string(row.Takeover.ReserveAc.Inspection.ID)
	if cache != nil {
		if step, ok := cache[inspectionID]; ok {
			return step
		}
	}
	step := h.flightAfterFlightInspectionStep(ctx, row)
	if cache != nil {
		cache[inspectionID] = step
	}
	return step
}

func (h *FlightHandler) flightFMReportStepCached(ctx context.Context, row *flight.Flight, cache map[string]responsedto.FlightStepItem) responsedto.FlightStepItem {
	if row == nil || row.Takeover == nil || len(row.ID) != 16 {
		return responsedto.FlightStepItem{}
	}
	flightID := string(row.ID)
	if cache != nil {
		if step, ok := cache[flightID]; ok {
			return step
		}
	}
	step := h.flightFMReportStep(ctx, row)
	if cache != nil {
		cache[flightID] = step
	}
	return step
}

func flightTakeoverSummaryForList(ctx context.Context, reserveAcSvc reserveac.ReserveAcService, row *flight.Flight, cache map[string]*responsedto.FlightDailyInspectionSummary) *responsedto.FlightTakeoverSummary {
	if row == nil || row.Takeover == nil {
		return nil
	}
	reserveRow := row.Takeover.ReserveAc
	if reserveRow == nil || len(reserveRow.AircraftID) == 0 {
		if reserveAcSvc == nil || len(row.Takeover.ReserveAcID) != 16 {
			return nil
		}
		var err error
		reserveRow, err = reserveAcSvc.GetByID(ctx, row.Takeover.ReserveAcID)
		if err != nil || reserveRow == nil || len(reserveRow.AircraftID) == 0 {
			return nil
		}
	}
	out := &responsedto.FlightTakeoverSummary{}
	heliID := string(reserveRow.AircraftID)
	if cache != nil {
		if daily, ok := cache[heliID]; ok {
			out.DailyInspection = daily
		}
	}
	if out.DailyInspection == nil && reserveAcSvc != nil {
		lastDaily, _ := reserveAcSvc.GetLastDailyInspectionByHelicopterID(ctx, reserveRow.AircraftID)
		if lastDaily != nil {
			out.DailyInspection = &responsedto.FlightDailyInspectionSummary{
				ID:        idString(lastDaily.ID),
				Name:      strings.TrimSpace(lastDaily.Name),
				ShortName: strings.TrimSpace(lastDaily.ShortName),
				Date:      lastDaily.At.UTC().Format("02.01.2006"),
				UTCTime:   lastDaily.At.UTC().Format("15:04"),
			}
			if cache != nil {
				cache[heliID] = out.DailyInspection
			}
		}
	}
	return out
}

func (h *FlightHandler) helicopterSummaryFromTakeoverForList(ctx context.Context, row *flight.Flight) *responsedto.FlightHelicopterSummary {
	if row == nil || row.Takeover == nil || row.Takeover.ReserveAc == nil {
		return h.helicopterSummaryFromTakeover(ctx, row)
	}
	reserveRow := row.Takeover.ReserveAc
	if reserveRow.Aircraft == nil {
		return h.helicopterSummaryFromTakeover(ctx, row)
	}
	heliRow := reserveRow.Aircraft
	return &responsedto.FlightHelicopterSummary{
		AOG:         heliRow.AirOnGround,
		Designation: strings.TrimSpace(heliRow.Designation),
		ID:          idString(heliRow.ID),
		Identifier:  strings.TrimSpace(heliRow.Identifier),
		IsActive:    heliRow.IsActive,
		Type:        strings.TrimSpace(heliRow.Type),
	}
}

func (h *FlightHandler) missionFlightDataComplete(ctx context.Context, missionID []byte) bool {
	if h == nil || h.flightData == nil || len(missionID) != 16 {
		return false
	}
	fd, err := h.flightData.GetByMissionID(ctx, missionID)
	if err != nil || fd == nil {
		return false
	}
	return flightdata.IsComplete(fd)
}

func (h *FlightHandler) flightMutationResource(ctx context.Context, row *flight.Flight) responsedto.FlightResource {
	if row == nil {
		return responsedto.FlightResource{}
	}
	takeover := &responsedto.FlightTakeoverSummary{
		ID: idString(row.TakeoverAcID),
	}
	missionID := h.flightMissionID(ctx, row)
	updatedAt, updatedBy := updatedAuditOrCreated(row.UpdatedAt, row.CreatedAt, row.UpdatedBy, row.UpdatedByName, row.CreatedBy, row.CreatedByName)
	return responsedto.FlightResource{
		Type: "flight",
		ID:   idString(row.ID),
		Attributes: responsedto.FlightAttributes{
			MissionID:  missionID,
			Code:       stringPtrOrEmpty(row.MissionCode),
			Date:       row.Date.UTC().Format("2006-01-02"),
			TotalDraft: 0,
			Takeover:   takeover,
			Steps:      nil,
			Draft:      nil,
			CreatedAt:  row.CreatedAt.UTC().Format(time.RFC3339),
			CreatedBy:  userRef(row.CreatedBy, row.CreatedByName),
			UpdatedAt:  updatedAt,
			UpdatedBy:  updatedBy,
			DeletedAt:  timeStringOrEmpty(row.DeletedAt),
			DeletedBy:  userRef(row.DeletedBy, ""),
		},
	}
}

func (h *FlightHandler) flightMissionID(ctx context.Context, row *flight.Flight) string {
	if h == nil || h.mission == nil || row == nil || len(row.ID) != 16 {
		return ""
	}
	rows, err := h.mission.ListByFlightID(ctx, row.ID)
	if err != nil || len(rows) == 0 {
		return ""
	}
	return idString(rows[0].ID)
}

func flightMissionStep(row *flight.Flight) responsedto.FlightStepItem {
	if row == nil {
		return responsedto.FlightStepItem{Phase: "mission", Step: 2, Status: "unassigned"}
	}
	return responsedto.FlightStepItem{Phase: "mission", Step: 2, Status: "unassigned"}
}

func (h *FlightHandler) flightAfterFlightInspectionStep(ctx context.Context, row *flight.Flight) responsedto.FlightStepItem {
	if row == nil || row.Takeover == nil || row.Takeover.ReserveAc == nil || row.Takeover.ReserveAc.Inspection == nil || len(row.Takeover.ReserveAc.Inspection.ID) != 16 {
		return responsedto.FlightStepItem{}
	}

	inspectionID := row.Takeover.ReserveAc.Inspection.ID
	if h == nil || h.afterFlightInspection == nil {
		return responsedto.FlightStepItem{}
	}

	afterRow, err := h.afterFlightInspection.GetByFlightInspectionID(ctx, inspectionID)
	if err != nil || afterRow == nil {
		return responsedto.FlightStepItem{}
	}

	return responsedto.FlightStepItem{
		Phase:       "after_flight_inspection",
		Step:        3,
		ID:          idString(afterRow.ID),
		Status:      "completed",
		Complete:    true,
		CompletedAt: timeStringOrEmptyRefTime(&afterRow.UpdatedAt),
		CompletedBy: uuidStringOrEmpty(afterRow.UpdatedBy),
	}
}

func (h *FlightHandler) flightFMReportStep(ctx context.Context, row *flight.Flight) responsedto.FlightStepItem {
	if row == nil || row.Takeover == nil || h == nil || h.fmReport == nil || len(row.ID) != 16 {
		return responsedto.FlightStepItem{}
	}

	reportRow, err := h.fmReport.GetByFlightID(ctx, row.ID)
	if err != nil || reportRow == nil {
		return responsedto.FlightStepItem{}
	}

	step := responsedto.FlightStepItem{
		Phase:  "fm_report",
		Step:   4,
		ID:     idString(reportRow.ID),
		Status: "in_progress",
	}
	if reportRow.CompletedAt != nil {
		step.Status = "completed"
		step.Complete = true
		step.CompletedAt = timeStringOrEmpty(reportRow.CompletedAt)
		step.CompletedBy = uuidStringOrEmpty(reportRow.CompletedBy)
	}
	return step
}

// ResolveHelicopterIDByTakeover resolves the flown aircraft's helicopter id from
// a takeover id via takeover -> reserve_ac -> aircraft. Returns nil when it
// cannot be resolved.
func (h *FlightHandler) ResolveHelicopterIDByTakeover(ctx context.Context, takeoverID []byte) []byte {
	if h == nil || h.takeover == nil || h.reserveAc == nil || len(takeoverID) != 16 {
		return nil
	}
	takeoverRow, err := h.takeover.GetByID(ctx, takeoverID)
	if err != nil || takeoverRow == nil || len(takeoverRow.ReserveAcID) != 16 {
		return nil
	}
	reserveRow, err := h.reserveAc.GetByID(ctx, takeoverRow.ReserveAcID)
	if err != nil || reserveRow == nil || len(reserveRow.AircraftID) != 16 {
		return nil
	}
	return reserveRow.AircraftID
}

func (h *FlightHandler) ResolveHelicopterForReportCode(ctx context.Context, takeoverID []byte) ([]byte, string) {
	helicopterID := h.ResolveHelicopterIDByTakeover(ctx, takeoverID)
	if len(helicopterID) != 16 || h.helicopter == nil {
		return nil, ""
	}
	heliRow, err := h.helicopter.GetByID(ctx, helicopterID)
	if err != nil || heliRow == nil {
		return helicopterID, ""
	}
	return helicopterID, strings.TrimSpace(heliRow.Identifier)
}

func (h *FlightHandler) helicopterSummaryFromTakeover(ctx context.Context, row *flight.Flight) *responsedto.FlightHelicopterSummary {
	if row == nil || row.Takeover == nil || h == nil || h.helicopter == nil || h.reserveAc == nil {
		return nil
	}
	reserveID := row.Takeover.ReserveAcID
	if len(reserveID) != 16 {
		return nil
	}
	reserveRow, err := h.reserveAc.GetByID(ctx, reserveID)
	if err != nil || reserveRow == nil || len(reserveRow.AircraftID) != 16 {
		return nil
	}
	heliRow, err := h.helicopter.GetByID(ctx, reserveRow.AircraftID)
	if err != nil || heliRow == nil {
		return nil
	}
	return &responsedto.FlightHelicopterSummary{
		AOG:         heliRow.AirOnGround,
		Designation: strings.TrimSpace(heliRow.Designation),
		ID:          idString(heliRow.ID),
		Identifier:  strings.TrimSpace(heliRow.Identifier),
		IsActive:    heliRow.IsActive,
		Type:        strings.TrimSpace(heliRow.Type),
	}
}

func stringOrFallback(name string, id []byte) string {
	if trimmed := strings.TrimSpace(name); trimmed != "" {
		return trimmed
	}
	return uuidStringOrEmpty(id)
}

func flightTakeoverSummary(ctx context.Context, reserveAcSvc reserveac.ReserveAcService, row *flight.Flight) *responsedto.FlightTakeoverSummary {
	if row == nil || reserveAcSvc == nil {
		return nil
	}
	var reserveID []byte
	if row.Takeover != nil && len(row.Takeover.ReserveAcID) == 16 {
		reserveID = row.Takeover.ReserveAcID
	}
	if len(reserveID) != 16 {
		return nil
	}
	reserveRow, err := reserveAcSvc.GetByID(ctx, reserveID)
	if err != nil || reserveRow == nil || len(reserveRow.AircraftID) == 0 {
		return nil
	}
	out := &responsedto.FlightTakeoverSummary{}
	lastDaily, _ := reserveAcSvc.GetLastDailyInspectionByHelicopterID(ctx, reserveRow.AircraftID)
	if lastDaily != nil {
		out.DailyInspection = &responsedto.FlightDailyInspectionSummary{
			ID:        idString(lastDaily.ID),
			Name:      strings.TrimSpace(lastDaily.Name),
			ShortName: strings.TrimSpace(lastDaily.ShortName),
			Date:      lastDaily.At.UTC().Format("02.01.2006"),
			UTCTime:   lastDaily.At.UTC().Format("15:04"),
		}
	}
	return out
}

func (h *FlightHandler) takeoverRosterDetailSummary(ctx context.Context, row *flight.Flight) *responsedto.TakeoverDutyRosterDetailsResponse {
	if h == nil || h.takeover == nil || row == nil || len(row.TakeoverAcID) != 16 {
		return nil
	}

	takeoverRow := row.Takeover
	if takeoverRow == nil || len(takeoverRow.RosterCrews) == 0 && len(takeoverRow.OtherPeople) == 0 {
		loaded, err := h.takeover.GetByID(ctx, row.TakeoverAcID)
		if err != nil || loaded == nil {
			return nil
		}
		takeoverRow = loaded
	}
	if takeoverRow == nil {
		return nil
	}

	resource := responsedto.DutyRosterResource{
		Attributes: responsedto.DutyRosterAttributes{
			Detail: responsedto.DutyRosterDetailsResponse{
				Pilot:       []responsedto.DutyRosterPilotCrew{},
				Doctor:      []responsedto.DutyRosterCrew{},
				Rescuer:     []responsedto.DutyRosterCrew{},
				Helper:      []responsedto.DutyRosterCrew{},
				OtherPerson: []responsedto.DutyRosterCrew{},
			},
		},
	}
	resource = buildDutyRosterResourceFromTakeoverRows(resource, takeoverRow.RosterCrews, row)
	resource = buildDutyRosterResourceFromOtherPeople(resource, takeoverRow.OtherPeople)

	detail := takeoverDutyRosterDetailsFromRosterDetails(resource.Attributes.Detail)
	return &detail
}

func flightDutyRosterStep(row *flight.Flight) responsedto.FlightStepItem {
	if row == nil || len(row.TakeoverAcID) != 16 {
		return responsedto.FlightStepItem{
			Phase:  "duty_roster",
			ID:     "",
			Status: "unassigned",
		}
	}
	return responsedto.FlightStepItem{
		Phase:       "duty_roster",
		Status:      "completed",
		CompletedAt: row.CreatedAt.UTC().Format(time.RFC3339),
		CompletedBy: uuidStringOrEmpty(row.CreatedBy),
	}
}

func flightReserveAcStep(row *flight.Flight) responsedto.FlightStepItem {
	if row == nil {
		return responsedto.FlightStepItem{
			Phase:  "reserve_ac",
			ID:     "",
			Status: "unassigned",
		}
	}
	var reserveRow *reserveac.ReserveAc
	var reserveID []byte
	if row.Takeover != nil {
		reserveRow = row.Takeover.ReserveAc
		reserveID = row.Takeover.ReserveAcID
	}
	if len(reserveID) != 16 {
		return responsedto.FlightStepItem{
			Phase:  "reserve_ac",
			ID:     "",
			Status: "unassigned",
		}
	}
	step := responsedto.FlightStepItem{
		Phase:  "reserve_ac",
		ID:     idString(reserveID),
		Status: "in_progress",
	}
	status := ""
	if reserveRow != nil {
		status = reserveRow.Status
	}
	switch reserveacutil.NormalizeStatus(status) {
	case reserveacutil.StatusApproved:
		step.Status = "completed"
		if reserveRow != nil && !reserveRow.UpdatedAt.IsZero() {
			step.CompletedAt = reserveRow.UpdatedAt.UTC().Format(time.RFC3339)
		}
		if reserveRow != nil {
			step.CompletedBy = uuidStringOrEmpty(reserveRow.UpdatedBy)
		}
	case reserveacutil.StatusOnProgress:
		step.Status = "in_progress"
	case reserveacutil.StatusPending:
		step.Status = "pending"
	default:
		step.Status = "in_progress"
	}
	return step
}

func timeStringOrEmptyRefTime(v *time.Time) string {
	if v == nil || v.IsZero() {
		return ""
	}
	return v.UTC().Format(time.RFC3339)
}

func stringPtrOrEmpty(v *string) string {
	if v == nil {
		return ""
	}
	return strings.TrimSpace(*v)
}

func parseOptionalUUIDFlight(v *string, pointer string) ([]byte, *jsonapi.ErrorObject) {
	if v == nil {
		return nil, nil
	}
	raw := strings.TrimSpace(*v)
	if raw == "" {
		return nil, nil
	}
	id, err := uuidv7.ParseString(raw)
	if err != nil {
		return nil, &jsonapi.ErrorObject{
			Status: "422",
			Title:  "Validation error",
			Detail: strings.TrimPrefix(pointer, "/data/attributes/") + " is invalid UUID",
			Source: &jsonapi.ErrorSource{Pointer: pointer},
		}
	}
	return id, nil
}

func writeFlightErrors(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
		if strings.TrimSpace(e.Code) == "" || strings.TrimSpace(e.ErrorCode) == "" {
			code, errorCode := inferFlightErrorCode(status, e.Title, e.Detail)
			if strings.TrimSpace(e.Code) == "" {
				e.Code = code
			}
			if strings.TrimSpace(e.ErrorCode) == "" {
				e.ErrorCode = errorCode
			}
		}
		enriched = append(enriched, e)
	}
	return response.WriteErrors(c, status, enriched)
}

func inferFlightErrorCode(status int, title, detail string) (string, string) {
	lowerTitle := strings.ToLower(strings.TrimSpace(title))
	lowerDetail := strings.ToLower(strings.TrimSpace(detail))
	switch status {
	case fiber.StatusUnauthorized:
		return apperrorsx.CodeFlightUnauthorized, apperrorsx.ErrFlightUnauthorized.ErrorCode
	case fiber.StatusNotFound:
		return apperrorsx.CodeFlightNotFound, apperrorsx.ErrFlightNotFound.ErrorCode
	case fiber.StatusUnprocessableEntity:
		switch {
		case strings.Contains(lowerDetail, "date must be yyyy-mm-dd"):
			return apperrorsx.CodeFlightDateInvalid, apperrorsx.ErrFlightDateInvalid.ErrorCode
		case strings.Contains(lowerDetail, "invalid uuid"):
			return apperrorsx.CodeFlightInvalidUUID, apperrorsx.ErrFlightInvalidUUID.ErrorCode
		default:
			return apperrorsx.CodeFlightValidationFailed, apperrorsx.ErrFlightValidationFailed.ErrorCode
		}
	case fiber.StatusBadRequest:
		switch {
		case strings.Contains(lowerTitle, "invalid json"):
			return apperrorsx.CodeFlightInvalidJSON, apperrorsx.ErrFlightInvalidJSON.ErrorCode
		case strings.Contains(lowerTitle, "create failed"):
			return apperrorsx.CodeFlightCreateFailed, apperrorsx.ErrFlightCreateFailed.ErrorCode
		case strings.Contains(lowerTitle, "get failed"):
			return apperrorsx.CodeFlightGetFailed, apperrorsx.ErrFlightGetFailed.ErrorCode
		case strings.Contains(lowerTitle, "list failed"):
			return apperrorsx.CodeFlightListFailed, apperrorsx.ErrFlightListFailed.ErrorCode
		default:
			return apperrorsx.CodeFlightInvalidPayload, apperrorsx.ErrFlightInvalidPayload.ErrorCode
		}
	default:
		return apperrorsx.CodeInternalServerError, apperrorsx.ErrInternal.ErrorCode
	}
}
