package handlers

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

	mysqldriver "github.com/go-sql-driver/mysql"
	"github.com/gofiber/fiber/v2"
	"gorm.io/gorm"

	facilitydomain "wucher/internal/domain/facility"
	flightdata "wucher/internal/domain/flight_data"
	hospitaldomain "wucher/internal/domain/hospital"
	icaodomain "wucher/internal/domain/icao"
	missiondomain "wucher/internal/domain/mission"
	userdomain "wucher/internal/domain/user"
	sharedconst "wucher/internal/shared/const"
	"wucher/internal/shared/pkg/apperrorsx"
	"wucher/internal/shared/pkg/uuidv7"
	"wucher/internal/transport/http/dto"
	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 FlightDataHandler struct {
	svc      flightdata.Service
	mission  missionService
	hospital hospitalService
	icao     icaoService
	user     userService
	facility facilityService
	validate *validators.Validator
}

func NewFlightDataHandler(svc flightdata.Service) *FlightDataHandler {
	return &FlightDataHandler{
		svc:      svc,
		validate: validators.New(),
	}
}

type hospitalService interface {
	GetByID(context.Context, []byte) (*hospitaldomain.Hospital, error)
}

type missionService interface {
	GetByID(context.Context, []byte) (*missiondomain.Mission, error)
}

type icaoService interface {
	GetByID(context.Context, []byte) (*icaodomain.ICAO, error)
}

type userService interface {
	GetByID(context.Context, []byte) (*userdomain.User, error)
}

type facilityService interface {
	GetByID(context.Context, []byte) (*facilitydomain.Facility, error)
}

func (h *FlightDataHandler) WithHospitalService(svc hospitalService) *FlightDataHandler {
	if h != nil {
		h.hospital = svc
	}
	return h
}

func (h *FlightDataHandler) WithMissionService(svc missionService) *FlightDataHandler {
	if h != nil {
		h.mission = svc
	}
	return h
}

func (h *FlightDataHandler) WithICAOService(svc icaoService) *FlightDataHandler {
	if h != nil {
		h.icao = svc
	}
	return h
}

func (h *FlightDataHandler) WithUserService(svc userService) *FlightDataHandler {
	if h != nil {
		h.user = svc
	}
	return h
}

func (h *FlightDataHandler) WithFacilityService(svc facilityService) *FlightDataHandler {
	if h != nil {
		h.facility = svc
	}
	return h
}

func (h *FlightDataHandler) validateFlightDataReferences(ctx context.Context, missionID, coPilotID, fromICAOID, fromHospitalID, toICAOID, toHospitalID []byte) error {
	if len(missionID) == 16 && h != nil && h.mission != nil {
		row, err := h.mission.GetByID(ctx, missionID)
		if err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return flightdata.ErrMissionReference
			}
			return err
		}
		if row == nil {
			return flightdata.ErrMissionReference
		}
	}
	if len(coPilotID) == 16 && h != nil && h.user != nil {
		row, err := h.user.GetByID(ctx, coPilotID)
		if err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return flightdata.ErrCoPilotReference
			}
			return err
		}
		if row == nil {
			return flightdata.ErrCoPilotReference
		}
	}
	if len(fromICAOID) == 16 && h != nil && h.icao != nil {
		row, err := h.icao.GetByID(ctx, fromICAOID)
		if err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return flightdata.ErrOriginLocationReference
			}
			return err
		}
		if row == nil {
			return flightdata.ErrOriginLocationReference
		}
	}
	if len(toICAOID) == 16 && h != nil && h.icao != nil {
		row, err := h.icao.GetByID(ctx, toICAOID)
		if err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return flightdata.ErrDestinationLocationReference
			}
			return err
		}
		if row == nil {
			return flightdata.ErrDestinationLocationReference
		}
	}
	if len(fromHospitalID) == 16 && h != nil && h.hospital != nil {
		row, err := h.hospital.GetByID(ctx, fromHospitalID)
		if err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return flightdata.ErrOriginLocationReference
			}
			return err
		}
		if row == nil {
			return flightdata.ErrOriginLocationReference
		}
	}
	if len(toHospitalID) == 16 && h != nil && h.hospital != nil {
		row, err := h.hospital.GetByID(ctx, toHospitalID)
		if err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return flightdata.ErrDestinationLocationReference
			}
			return err
		}
		if row == nil {
			return flightdata.ErrDestinationLocationReference
		}
	}
	return nil
}

func (h *FlightDataHandler) validateSPOFacilityReferences(ctx context.Context, attrs requestdto.FlightDataCreateAttributes) *jsonapi.ErrorObject {
	if h == nil || h.facility == nil {
		return nil
	}

	check := func(pointer, raw, expectedCategory string) *jsonapi.ErrorObject {
		id, err := uuidv7.ParseString(strings.TrimSpace(raw))
		if err != nil || len(id) != 16 {
			return nil
		}
		row, err := h.facility.GetByID(ctx, id)
		if err != nil || row == nil || !strings.EqualFold(strings.TrimSpace(row.Category), expectedCategory) {
			return &jsonapi.ErrorObject{
				Status: "422",
				Title:  "Validation error",
				Detail: flightdata.ErrorMessageFacilityReference,
				Source: &jsonapi.ErrorSource{Pointer: pointer},
			}
		}
		return nil
	}

	if attrs.HESLO != nil {
		for i, f := range attrs.HESLO.Flights {
			if errObj := check("/data/attributes/heslo/flights/"+strconv.Itoa(i)+"/heslo_rope_label_id", f.HESLORopeLabelID, sharedconst.FlightDataFacilityCategoryHesloRopeLabel); errObj != nil {
				return errObj
			}
			for j, s := range f.Slings {
				if errObj := check("/data/attributes/heslo/flights/"+strconv.Itoa(i)+"/slings/"+strconv.Itoa(j)+"/sling_id", s.SlingID, sharedconst.FlightDataFacilityCategoryHesloRopeLength); errObj != nil {
					return errObj
				}
			}
		}
	}

	if attrs.Logging != nil {
		for i, s := range attrs.Logging.Slings {
			if errObj := check("/data/attributes/logging/slings/"+strconv.Itoa(i)+"/sling_id", s.SlingID, sharedconst.FlightDataFacilityCategoryHesloHook); errObj != nil {
				return errObj
			}
		}
	}

	if attrs.HEC != nil {
		for i, f := range attrs.HEC.Flights {
			if errObj := check("/data/attributes/hec/flights/"+strconv.Itoa(i)+"/equipment_id", f.EquipmentID, sharedconst.FlightDataFacilityCategoryHecRopeLabel); errObj != nil {
				return errObj
			}
		}
		for i, s := range attrs.HEC.Slings {
			if errObj := check("/data/attributes/hec/slings/"+strconv.Itoa(i)+"/sling_id", s.SlingID, sharedconst.FlightDataFacilityCategoryHecRopeLength); errObj != nil {
				return errObj
			}
		}
	}

	return nil
}

// ListFlightData godoc
// @Summary List flight data
// @Description List flight data rows. Supports `date` for exact flight take-off date filtering (`YYYY-MM-DD`), plus `mission_id` and `flight_data_id` filters.
// @Description SPO-specific response fields (`heslo`, `logging`, and `hec`) are returned only when the related mission type is `SPO`.
// @Tags Mission - Flight Data
// @Produce json
// @Param date query string false "Filter by flight take-off date (YYYY-MM-DD)"
// @Param mission_id query string false "Filter by mission UUID"
// @Param flight_data_id query string false "Filter by flight-data UUID"
// @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.FlightDataListResponse
// @Failure 422 {object} jsonapi.ErrorDocument
// @Failure 400 {object} jsonapi.ErrorDocument
// @Router /api/v1/flight-data/get-all [get]
func (h *FlightDataHandler) List(c *fiber.Ctx) error {
	dateFilter := strings.TrimSpace(c.Query("date"))
	if dateFilter != "" {
		if _, err := time.Parse("2006-01-02", dateFilter); err != nil {
			return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
				Status: "422",
				Title:  "Validation error",
				Detail: "date must be YYYY-MM-DD",
				Source: &jsonapi.ErrorSource{Pointer: "/date"},
			}})
		}
	}
	missionQuery := c.Query("mission_id")
	missionID, errObj := parseOptionalUUIDFlightData(&missionQuery, "/query/mission_id")
	if errObj != nil {
		return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
	}
	flightDataQuery := c.Query("flight_data_id")
	flightDataID, errObj := parseOptionalUUIDFlightData(&flightDataQuery, "/query/flight_data_id")
	if errObj != nil {
		return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
	}

	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.List(c.UserContext(), dateFilter, missionID, flightDataID, 0, 0)
	if err != nil {
		return writeFlightDataErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "List failed",
			Detail: safeInternalDetail(),
		}})
	}

	data := make([]responsedto.FlightDataResource, 0, len(rows))
	for i := range rows {
		resource, buildErr := h.buildFlightDataResource(c, &rows[i])
		if buildErr != nil {
			return writeFlightDataErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
				Status: "400",
				Title:  "List failed",
				Detail: buildErr.Error(),
			}})
		}
		data = append(data, resource)
	}

	return response.WriteWithMeta(c, fiber.StatusOK, data, map[string]any{
		"total": total,
	})
}

// ListFlightDataDatatable godoc
// @Summary List flight data (datatable)
// @Description Datatable-friendly list of flight data rows. Supports `date` for exact flight take-off date filtering (`YYYY-MM-DD`), plus `mission_id` and `flight_data_id` filters.
// @Description Same payload shape as `get-all`, but with DataTables paging params (`start`, `length`, `draw`).
// @Description SPO-specific response fields (`heslo`, `logging`, and `hec`) are returned only when the related mission type is `SPO`.
// @Tags Mission - Flight Data
// @Produce json
// @Param draw query int false "DataTables draw counter"
// @Param start query int false "Paging start offset"
// @Param length query int false "Paging length"
// @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"
// @Param date query string false "Filter by flight take-off date (YYYY-MM-DD)"
// @Param mission_id query string false "Filter by mission UUID"
// @Param flight_data_id query string false "Filter by flight-data UUID"
// @Success 200 {object} responsedto.FlightDataListResponse
// @Failure 422 {object} jsonapi.ErrorDocument
// @Failure 400 {object} jsonapi.ErrorDocument
// @Router /api/v1/flight-data/get-all/dt [get]
func (h *FlightDataHandler) ListDatatable(c *fiber.Ctx) error {
	dateFilter := strings.TrimSpace(c.Query("date"))
	if dateFilter != "" {
		if _, err := time.Parse("2006-01-02", dateFilter); err != nil {
			return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
				Status: "422",
				Title:  "Validation error",
				Detail: "date must be YYYY-MM-DD",
				Source: &jsonapi.ErrorSource{Pointer: "/date"},
			}})
		}
	}
	missionQuery := c.Query("mission_id")
	missionID, errObj := parseOptionalUUIDFlightData(&missionQuery, "/query/mission_id")
	if errObj != nil {
		return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
	}
	flightDataQuery := c.Query("flight_data_id")
	flightDataID, errObj := parseOptionalUUIDFlightData(&flightDataQuery, "/query/flight_data_id")
	if errObj != nil {
		return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
	}

	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
	}

	if start := parseIntDefault(c.Query("start"), -1); start >= 0 && c.Query("length") != "" {
		pageSize = parseIntDefault(c.Query("length"), pageSize)
		if pageSize < 1 {
			pageSize = 20
		}
		pageNumber = (start / pageSize) + 1
	}

	offset := (pageNumber - 1) * pageSize
	rows, total, err := h.svc.List(c.UserContext(), dateFilter, missionID, flightDataID, pageSize, offset)
	if err != nil {
		return writeFlightDataErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "List failed",
			Detail: safeInternalDetail(),
		}})
	}

	data := make([]responsedto.FlightDataResource, 0, len(rows))
	for i := range rows {
		resource, buildErr := h.buildFlightDataResource(c, &rows[i])
		if buildErr != nil {
			return writeFlightDataErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
				Status: "400",
				Title:  "List failed",
				Detail: buildErr.Error(),
			}})
		}
		data = append(data, resource)
	}

	return response.WriteWithMeta(c, fiber.StatusOK, data, map[string]any{
		"page_number": pageNumber,
		"page_size":   pageSize,
		"total":       total,
	})
}

// CreateFlightData godoc
// @Summary Create flight data
// @Description Create a flight data row.
// @Description Required field: `mission_id`. `co_pilot_id` is optional.
// @Description Swagger request example shows the complete common payload for an `SPO` mission. `SPO` is the default example shape, and for other mission types (`CAT` and `NCO`) you do not need to send the `hec`, `heslo`, or `logging` blocks. `rotor_brake_cycle` is optional for `SPO`, `CAT`, and `NCO`.
// @Description SPO facility references must point to facilities with categories: `heslo_rope_label_id -> heslo-rope-label`, `heslo.slings -> heslo-rope-length`, `logging.slings -> heslo-hook`, `hec.flights -> hec-rope-label`, and `hec.slings -> hec-rope-length`.
// @Description SPO-specific response fields (`heslo`, `logging`, and `hec`) are returned only when the related mission type is `SPO`.
// @Description Example request: {"data":{"type":"flight_data_create","attributes":{"mission_id":"019d6774-98e8-7fba-9ef4-3795c0da8a13"}}}
// @Tags  Mission - Flight Data
// @Accept json
// @Produce json
// @Param request body requestdto.FlightDataCreateRequest true "JSON:API flight data create request. Example is for SPO; CAT/NCO can omit SPO-only blocks and other optional fields." SchemaExample({"data":{"type":"flight_data_create","attributes":{"mission_id":"019d6774-98e8-7fba-9ef4-3795c0da8a13","flight_take_off":"2026-04-20T08:00:00Z","flight_landing":"2026-04-20T09:15:00Z","from_icao_id":"019d6774-98e8-7fba-9ef4-3795c0da8a13","to_icao_id":"019d6774-98e8-7fba-9ef4-3795c0da8a13","hook_releases":0,"ticket_no":"SPO-001","engine":"AIRBUS H145","delivery_note_number":"DN-2026-0001","customer_name":"Patient Transfer","other_information":"SPO mission sample","landing_count":1,"heslo":{"flights":[{"id":"string","rot":0,"heslo_rope_label_id":"string","heslo_rope_label_name":"string"}],"slings":[{"id":"string","heslo_flight_id":"string","sling_id":"string","sling_name":"string"}]},"logging":{"slings":[{"id":"string","sling_id":"string","sling_name":"string"}]},"hec":{"flights":[{"equipment_id":"string","equipment_name":"string","hec_cycle":0,"id":"string","rot":0}],"loads":[{"id":"string","load_category":"string","quantity":0}],"slings":[{"id":"string","sling_id":"string","sling_name":"string"}]}}}})
// @Success 201 {object} responsedto.FlightDataResponse
// @Failure 422 {object} jsonapi.ErrorDocument
// @Failure 400 {object} jsonapi.ErrorDocument
// @Router /api/v1/flight-data/create [post]
func (h *FlightDataHandler) Create(c *fiber.Ctx) error {
	var req requestdto.FlightDataCreateRequest
	if err := c.BodyParser(&req); err != nil {
		return writeFlightDataErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "Invalid JSON",
			Detail: safeInternalDetail(),
		}})
	}
	if errs := h.validate.ValidateStruct(req); errs != nil {
		return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, errs)
	}

	attrs := req.Data.Attributes

	missionID, errObj := parseRequiredUUID(attrs.MissionID, "/data/attributes/mission_id")
	if errObj != nil {
		return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
	}
	coPilotID, errObj := parseOptionalUUIDFlightData(attrs.CoPilotID, "/data/attributes/co_pilot_id")
	if errObj != nil {
		return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
	}

	fromICAOID, errObj := parseOptionalUUIDFlightData(attrs.FromICAOID, "/data/attributes/from_icao_id")
	if errObj != nil {
		return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
	}
	fromHospitalID, errObj := parseOptionalUUIDFlightData(attrs.FromHospitalID, "/data/attributes/from_hospital_id")
	if errObj != nil {
		return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
	}
	toICAOID, errObj := parseOptionalUUIDFlightData(attrs.ToICAOID, "/data/attributes/to_icao_id")
	if errObj != nil {
		return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
	}
	toHospitalID, errObj := parseOptionalUUIDFlightData(attrs.ToHospitalID, "/data/attributes/to_hospital_id")
	if errObj != nil {
		return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
	}
	if err := h.validateFlightDataReferences(c.UserContext(), missionID, coPilotID, fromICAOID, fromHospitalID, toICAOID, toHospitalID); err != nil {
		return writeFlightDataServiceError(c, err, missionID, coPilotID, fromICAOID, fromHospitalID, toICAOID, toHospitalID)
	}

	row := &flightdata.FlightData{
		MissionID:          missionID,
		CoPilotID:          coPilotID,
		FromICAOID:         fromICAOID,
		FromHospitalID:     fromHospitalID,
		ToICAOID:           toICAOID,
		ToHospitalID:       toHospitalID,
		CreatedBy:          actorUserID(c),
		UpdatedBy:          actorUserID(c),
		TicketNo:           stringPtrOrEmpty(attrs.TicketNo),
		Engine:             stringPtrOrEmpty(attrs.Engine),
		DeliveryNoteNumber: stringPtrOrEmpty(attrs.DeliveryNoteNumber),
		CustomerName:       stringPtrOrEmpty(attrs.CustomerName),
		OtherInformation:   stringPtrOrEmpty(attrs.OtherInformation),
	}
	if attrs.FlightTakeOff != nil {
		v, err := parseRFC3339(*attrs.FlightTakeOff, "/data/attributes/flight_take_off")
		if err != nil {
			return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*err})
		}
		row.FlightTakeOff = v
	}
	if attrs.FlightLanding != nil {
		v, err := parseRFC3339(*attrs.FlightLanding, "/data/attributes/flight_landing")
		if err != nil {
			return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*err})
		}
		row.FlightLanding = v
	}
	if attrs.FlightDurationSecs != nil {
		row.FlightDuration = time.Duration(*attrs.FlightDurationSecs) * time.Second
	}
	if attrs.FlightREDSecs != nil {
		row.FlightRED = time.Duration(*attrs.FlightREDSecs) * time.Second
	}
	if attrs.MaxN1 != nil {
		row.MaxN1 = *attrs.MaxN1
	}
	if attrs.MaxN2 != nil {
		row.MaxN2 = *attrs.MaxN2
	}
	if attrs.PaxCount != nil {
		row.PaxCount = *attrs.PaxCount
	}
	if attrs.LandingCount != nil {
		row.LandingCount = *attrs.LandingCount
	}
	if attrs.RotorBrakeCycle != nil {
		row.RotorBrakeCycle = *attrs.RotorBrakeCycle
	}
	if attrs.HookReleases != nil {
		row.HookReleases = *attrs.HookReleases
	}
	if attrs.FlightPlanDistance != nil {
		row.FlightPlanDistance = *attrs.FlightPlanDistance
	}
	if attrs.FlightPlanTimeSecs != nil {
		row.FlightPlanTime = time.Duration(*attrs.FlightPlanTimeSecs) * time.Second
	}
	if attrs.FlightPlanTrueCourse != nil {
		row.FlightPlanTrueCourse = *attrs.FlightPlanTrueCourse
	}
	if attrs.FuelBeforeFlight != nil {
		row.FuelBeforeFlight = *attrs.FuelBeforeFlight
	}
	if attrs.FuelUpload != nil {
		row.FuelUpload = *attrs.FuelUpload
	}
	if attrs.FuelAfterFlight != nil {
		row.FuelAfterFlight = *attrs.FuelAfterFlight
	}
	if attrs.FuelPlanning != nil {
		row.FuelPlanning = *attrs.FuelPlanning
	}
	if attrs.FlightPositioning != nil {
		row.FlightPositioning = *attrs.FlightPositioning
	}

	if err := h.svc.Create(c.UserContext(), row); err != nil {
		return writeFlightDataServiceError(c, err, row.MissionID, row.CoPilotID, row.FromICAOID, row.FromHospitalID, row.ToICAOID, row.ToHospitalID)
	}

	if spoData := parseSPOUpsertData(attrs); spoData != nil {
		if errObj := h.validateSPOFacilityReferences(c.UserContext(), attrs); errObj != nil {
			return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
		}
		if err := h.svc.UpsertSPODetails(c.UserContext(), row.ID, spoData); err != nil {
			return writeFlightDataErrors(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 {
		resource, buildErr := h.buildFlightDataResource(c, created)
		if buildErr != nil {
			return writeFlightDataErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
				Status: "400",
				Title:  "Create failed",
				Detail: buildErr.Error(),
			}})
		}
		return response.Write(c, fiber.StatusCreated, resource)
	}
	if rowByMission, lookupErr := h.svc.GetByMissionID(c.UserContext(), row.MissionID); lookupErr == nil && rowByMission != nil {
		resource, buildErr := h.buildFlightDataResource(c, rowByMission)
		if buildErr != nil {
			return writeFlightDataErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
				Status: "400",
				Title:  "Create failed",
				Detail: buildErr.Error(),
			}})
		}
		return response.Write(c, fiber.StatusCreated, resource)
	}
	resource, buildErr := h.buildFlightDataResource(c, row)
	if buildErr != nil {
		return writeFlightDataErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "Create failed",
			Detail: buildErr.Error(),
		}})
	}
	return response.Write(c, fiber.StatusCreated, resource)
}

// GetFlightData godoc
// @Summary Get flight data by ID
// @Description Get flight data detail by ID. Path `id` can be mission UUID, flight-data UUID, or flight UUID.
// @Description SPO-specific response fields (`heslo`, `logging`, and `hec`) are returned only when the related mission type is `SPO`.
// @Tags  Mission - Flight Data
// @Produce json
// @Param id path string true "Mission UUID / Flight Data UUID / Flight UUID (UUIDv7)"
// @Success 200 {object} responsedto.FlightDataResponse
// @Failure 422 {object} jsonapi.ErrorDocument
// @Failure 404 {object} jsonapi.ErrorDocument
// @Failure 400 {object} jsonapi.ErrorDocument
// @Router /api/v1/flight-data/get/{id} [get]
func (h *FlightDataHandler) Get(c *fiber.Ctx) error {
	rawID := strings.TrimSpace(c.Params("id"))
	parsedID, err := uuidv7.ParseString(rawID)
	if err != nil {
		return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
			Status: "422",
			Title:  "Validation error",
			Detail: flightdata.ErrorMessageInvalidPathUUID,
			Source: &jsonapi.ErrorSource{Pointer: "/path/id"},
		}})
	}

	// Primary flow: mission UUID -> flight_data by mission relation.
	row, err := h.svc.GetByMissionID(c.UserContext(), parsedID)
	if err != nil {
		return writeFlightDataErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "Get failed",
			Detail: safeInternalDetail(),
		}})
	}
	if row == nil {
		// Backward compatibility for older consumers.
		row, err = h.svc.GetByID(c.UserContext(), parsedID)
	}
	if err != nil {
		return writeFlightDataErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "Get failed",
			Detail: safeInternalDetail(),
		}})
	}
	if row == nil {
		row, err = h.svc.GetByFlightID(c.UserContext(), parsedID)
	}
	if err != nil {
		return writeFlightDataErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "Get failed",
			Detail: safeInternalDetail(),
		}})
	}
	if row == nil {
		return writeFlightDataErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{
			Status: "404",
			Title:  "Not found",
			Detail: flightdata.ErrorMessageNotFound,
		}})
	}
	resource, buildErr := h.buildFlightDataResource(c, row)
	if buildErr != nil {
		return writeFlightDataErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "Get failed",
			Detail: buildErr.Error(),
		}})
	}
	return response.Write(c, fiber.StatusOK, resource)
}

// UpdateFlightData godoc
// @Summary Update flight data by ID
// @Description Update a flight data row by UUID.
// @Description Required field: `mission_id`. `co_pilot_id` is optional.
// @Description Swagger request example uses the same SPO-shaped payload as create, but this endpoint updates an existing flight-data row.
// @Description SPO facility references must point to facilities with categories: `heslo_rope_label_id -> heslo-rope-label`, `heslo.slings -> heslo-rope-length`, `logging.slings -> heslo-hook`, `hec.flights -> hec-rope-label`, and `hec.slings -> hec-rope-length`.
// @Description For frontend usage, send only the fields that need to change; omitted fields stay unchanged. `SPO` is the default example shape, and for `CAT`/`NCO` you do not need to send the `hec`, `heslo`, or `logging` blocks. `rotor_brake_cycle` is optional for `SPO`, `CAT`, and `NCO`.
// @Description SPO-specific response fields (`heslo`, `logging`, and `hec`) are returned only when the related mission type is `SPO`.
// @Description Example request: {"data":{"type":"flight_data_create","attributes":{"mission_id":"019d6774-98e8-7fba-9ef4-3795c0da8a13"}}}
// @Tags Mission - Flight Data
// @Accept json
// @Produce json
// @Param id path string true "Flight Data UUID (UUIDv7)"
// @Param request body requestdto.FlightDataUpdateRequest true "JSON:API flight data update request. Send only fields you want to change; omit the rest." SchemaExample({"data":{"type":"flight_data_update","attributes":{"mission_id":"019d6774-98e8-7fba-9ef4-3795c0da8a13","flight_take_off":"2026-04-20T08:00:00Z","flight_landing":"2026-04-20T09:15:00Z","from_icao_id":"019d6774-98e8-7fba-9ef4-3795c0da8a13","to_icao_id":"019d6774-98e8-7fba-9ef4-3795c0da8a13","hook_releases":0,"ticket_no":"SPO-001","engine":"AIRBUS H145","delivery_note_number":"DN-2026-0001","customer_name":"Patient Transfer","other_information":"SPO mission sample","landing_count":1,"heslo":{"flights":[{"id":"string","rot":0,"heslo_rope_label_id":"string","heslo_rope_label_name":"string"}],"slings":[{"id":"string","heslo_flight_id":"string","sling_id":"string","sling_name":"string"}]},"logging":{"slings":[{"id":"string","sling_id":"string","sling_name":"string"}]},"hec":{"flights":[{"equipment_id":"string","equipment_name":"string","hec_cycle":0,"id":"string","rot":0}],"loads":[{"id":"string","load_category":"string","quantity":0}],"slings":[{"id":"string","sling_id":"string","sling_name":"string"}]}}}})
// @Success 200 {object} responsedto.FlightDataResponse
// @Failure 422 {object} jsonapi.ErrorDocument
// @Failure 404 {object} jsonapi.ErrorDocument
// @Failure 400 {object} jsonapi.ErrorDocument
// @Router /api/v1/flight-data/update/{id} [patch]
func (h *FlightDataHandler) Update(c *fiber.Ctx) error {
	rawID := strings.TrimSpace(c.Params("id"))
	id, err := uuidv7.ParseString(rawID)
	if err != nil {
		return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
			Status: "422",
			Title:  "Validation error",
			Detail: flightdata.ErrorMessageInvalidPathUUID,
			Source: &jsonapi.ErrorSource{Pointer: "/path/id"},
		}})
	}

	var req requestdto.FlightDataUpdateRequest
	if err := c.BodyParser(&req); err != nil {
		return writeFlightDataErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "Invalid JSON",
			Detail: safeInternalDetail(),
		}})
	}
	if errs := h.validate.ValidateStruct(req); errs != nil {
		return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, errs)
	}

	attrs := req.Data.Attributes
	missionID, errObj := parseRequiredUUID(attrs.MissionID, "/data/attributes/mission_id")
	if errObj != nil {
		return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
	}
	coPilotID, errObj := parseOptionalUUIDFlightData(attrs.CoPilotID, "/data/attributes/co_pilot_id")
	if errObj != nil {
		return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
	}

	fromICAOID, errObj := parseOptionalUUIDFlightData(attrs.FromICAOID, "/data/attributes/from_icao_id")
	if errObj != nil {
		return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
	}
	fromHospitalID, errObj := parseOptionalUUIDFlightData(attrs.FromHospitalID, "/data/attributes/from_hospital_id")
	if errObj != nil {
		return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
	}
	toICAOID, errObj := parseOptionalUUIDFlightData(attrs.ToICAOID, "/data/attributes/to_icao_id")
	if errObj != nil {
		return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
	}
	toHospitalID, errObj := parseOptionalUUIDFlightData(attrs.ToHospitalID, "/data/attributes/to_hospital_id")
	if errObj != nil {
		return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
	}
	if err := h.validateFlightDataReferences(c.UserContext(), missionID, coPilotID, fromICAOID, fromHospitalID, toICAOID, toHospitalID); err != nil {
		return writeFlightDataServiceError(c, err, missionID, coPilotID, fromICAOID, fromHospitalID, toICAOID, toHospitalID)
	}

	row, err := h.svc.GetByID(c.UserContext(), id)
	if err != nil {
		return writeFlightDataErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "Get failed",
			Detail: safeInternalDetail(),
		}})
	}
	if row == nil {
		return writeFlightDataErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{
			Status: "404",
			Title:  "Not found",
			Detail: flightdata.ErrorMessageNotFound,
		}})
	}

	row.MissionID = missionID
	if len(coPilotID) > 0 {
		row.CoPilotID = coPilotID
	}
	row.FromICAOID = fromICAOID
	row.FromHospitalID = fromHospitalID
	row.ToICAOID = toICAOID
	row.ToHospitalID = toHospitalID
	row.UpdatedBy = actorUserID(c)
	if attrs.TicketNo != nil {
		row.TicketNo = stringPtrOrEmpty(attrs.TicketNo)
	}
	if attrs.Engine != nil {
		row.Engine = stringPtrOrEmpty(attrs.Engine)
	}
	if attrs.DeliveryNoteNumber != nil {
		row.DeliveryNoteNumber = stringPtrOrEmpty(attrs.DeliveryNoteNumber)
	}
	if attrs.CustomerName != nil {
		row.CustomerName = stringPtrOrEmpty(attrs.CustomerName)
	}
	if attrs.OtherInformation != nil {
		row.OtherInformation = stringPtrOrEmpty(attrs.OtherInformation)
	}
	if attrs.FlightTakeOff != nil {
		v, err := parseRFC3339(*attrs.FlightTakeOff, "/data/attributes/flight_take_off")
		if err != nil {
			return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*err})
		}
		row.FlightTakeOff = v
	}
	if attrs.FlightLanding != nil {
		v, err := parseRFC3339(*attrs.FlightLanding, "/data/attributes/flight_landing")
		if err != nil {
			return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*err})
		}
		row.FlightLanding = v
	}
	if attrs.FlightDurationSecs != nil {
		row.FlightDuration = time.Duration(*attrs.FlightDurationSecs) * time.Second
	}
	if attrs.FlightREDSecs != nil {
		row.FlightRED = time.Duration(*attrs.FlightREDSecs) * time.Second
	}
	if attrs.MaxN1 != nil {
		row.MaxN1 = *attrs.MaxN1
	}
	if attrs.MaxN2 != nil {
		row.MaxN2 = *attrs.MaxN2
	}
	if attrs.PaxCount != nil {
		row.PaxCount = *attrs.PaxCount
	}
	if attrs.LandingCount != nil {
		row.LandingCount = *attrs.LandingCount
	}
	if attrs.RotorBrakeCycle != nil {
		row.RotorBrakeCycle = *attrs.RotorBrakeCycle
	}
	if attrs.HookReleases != nil {
		row.HookReleases = *attrs.HookReleases
	}
	if attrs.FlightPlanDistance != nil {
		row.FlightPlanDistance = *attrs.FlightPlanDistance
	}
	if attrs.FlightPlanTimeSecs != nil {
		row.FlightPlanTime = time.Duration(*attrs.FlightPlanTimeSecs) * time.Second
	}
	if attrs.FlightPlanTrueCourse != nil {
		row.FlightPlanTrueCourse = *attrs.FlightPlanTrueCourse
	}
	if attrs.FuelBeforeFlight != nil {
		row.FuelBeforeFlight = *attrs.FuelBeforeFlight
	}
	if attrs.FuelUpload != nil {
		row.FuelUpload = *attrs.FuelUpload
	}
	if attrs.FuelAfterFlight != nil {
		row.FuelAfterFlight = *attrs.FuelAfterFlight
	}
	if attrs.FuelPlanning != nil {
		row.FuelPlanning = *attrs.FuelPlanning
	}
	if attrs.FlightPositioning != nil {
		row.FlightPositioning = *attrs.FlightPositioning
	}

	if err := h.svc.Update(c.UserContext(), row); err != nil {
		return writeFlightDataServiceError(c, err, row.MissionID, row.CoPilotID, row.FromICAOID, row.FromHospitalID, row.ToICAOID, row.ToHospitalID)
	}

	if spoData := parseSPOUpsertData(attrs); spoData != nil {
		if errObj := h.validateSPOFacilityReferences(c.UserContext(), attrs); errObj != nil {
			return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
		}
		if err := h.svc.UpsertSPODetails(c.UserContext(), row.ID, spoData); err != nil {
			return writeFlightDataErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
				Status: "400",
				Title:  "Update failed",
				Detail: safeInternalDetail(),
			}})
		}
	}

	updated, err := h.svc.GetByID(c.UserContext(), row.ID)
	if err == nil && updated != nil {
		resource, buildErr := h.buildFlightDataResource(c, updated)
		if buildErr != nil {
			return writeFlightDataErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
				Status: "400",
				Title:  "Update failed",
				Detail: buildErr.Error(),
			}})
		}
		return response.Write(c, fiber.StatusOK, resource)
	}
	resource, buildErr := h.buildFlightDataResource(c, row)
	if buildErr != nil {
		return writeFlightDataErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "Update failed",
			Detail: buildErr.Error(),
		}})
	}
	return response.Write(c, fiber.StatusOK, resource)
}

// DeleteFlightData godoc
// @Summary Delete flight data by ID
// @Description Soft delete flight data by UUID.
// @Tags Mission - Flight Data
// @Produce json
// @Param id path string true "Flight Data UUID (UUIDv7)"
// @Success 200 {object} dto.GenericDeleteResponse
// @Failure 422 {object} jsonapi.ErrorDocument
// @Failure 404 {object} jsonapi.ErrorDocument
// @Failure 400 {object} jsonapi.ErrorDocument
// @Router /api/v1/flight-data/delete/{id} [delete]
func (h *FlightDataHandler) Delete(c *fiber.Ctx) error {
	rawID := strings.TrimSpace(c.Params("id"))
	id, err := uuidv7.ParseString(rawID)
	if err != nil {
		return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
			Status: "422",
			Title:  "Validation error",
			Detail: flightdata.ErrorMessageInvalidPathUUID,
			Source: &jsonapi.ErrorSource{Pointer: "/path/id"},
		}})
	}
	if err := h.svc.Delete(c.UserContext(), id, actorUserID(c)); err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return writeFlightDataErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{
				Status: "404",
				Title:  "Not found",
				Detail: "flight data not found",
			}})
		}
		return writeFlightDataServiceError(c, err)
	}
	return response.Write(c, fiber.StatusOK, dto.NewGenericDeleteResponse("flight_data_delete"))
}

func flightDataResource(row *flightdata.FlightData) responsedto.FlightDataResource {
	resource := responsedto.FlightDataResource{
		Type:       sharedconst.FlightDataResourceType,
		ID:         idString(row.ID),
		Attributes: buildBaseFlightDataAttributes(row),
	}

	return resource
}

func (h *FlightDataHandler) buildFlightDataResource(c *fiber.Ctx, row *flightdata.FlightData) (responsedto.FlightDataResource, error) {
	return buildFlightDataResourceWithService(c.UserContext(), h.svc, row)
}

func (h *FlightDataHandler) buildSPODetails(c *fiber.Ctx, row *flightdata.FlightData) (
	*responsedto.FlightDataSPOHESLODetails,
	*responsedto.FlightDataSPOLoggingDetails,
	*responsedto.FlightDataSPOHECDetails,
	error,
) {
	return buildFlightDataSPODetails(c.UserContext(), h.svc, row)
}

func missionRefFlightData(row *flightdata.FlightData) *responsedto.FlightDataMissionRef {
	if row == nil {
		return nil
	}
	if row.Mission == nil && len(row.MissionID) == 0 {
		return nil
	}
	out := &responsedto.FlightDataMissionRef{
		ID: idString(row.MissionID),
	}
	if row.Mission != nil {
		out.ID = idString(row.Mission.ID)
		out.Type = strings.TrimSpace(row.Mission.Type)
	}
	return out
}

func coPilotRefFlightData(row *flightdata.FlightData) *responsedto.FlightDataCoPilotRef {
	if row == nil {
		return nil
	}
	if row.CoPilot == nil && len(row.CoPilotID) == 0 {
		return nil
	}
	out := &responsedto.FlightDataCoPilotRef{
		ID: idString(row.CoPilotID),
	}
	if row.CoPilot != nil {
		out.ID = idString(row.CoPilot.ID)
		out.FirstName = strings.TrimSpace(row.CoPilot.FirstName)
		out.LastName = strings.TrimSpace(row.CoPilot.LastName)
		out.Email = strings.TrimSpace(row.CoPilot.Email)
	}
	return out
}

func fromLocationRefFlightData(row *flightdata.FlightData) *responsedto.FlightDataLocationRef {
	if row == nil {
		return nil
	}
	if row.FromICAO != nil {
		code := strings.TrimSpace(row.FromICAO.ICAOCode)
		return &responsedto.FlightDataLocationRef{
			Type:  sharedconst.FlightDataLocationTypeICAO,
			ID:    idString(row.FromICAO.ID),
			Code:  code,
			Label: code,
		}
	}
	if row.FromHospital != nil {
		name := strings.TrimSpace(row.FromHospital.Name)
		return &responsedto.FlightDataLocationRef{
			Type:  sharedconst.FlightDataLocationTypeHospital,
			ID:    idString(row.FromHospital.ID),
			Name:  name,
			Label: name,
		}
	}
	if len(row.FromICAOID) > 0 {
		return &responsedto.FlightDataLocationRef{Type: sharedconst.FlightDataLocationTypeICAO, ID: idString(row.FromICAOID)}
	}
	if len(row.FromHospitalID) > 0 {
		return &responsedto.FlightDataLocationRef{Type: sharedconst.FlightDataLocationTypeHospital, ID: idString(row.FromHospitalID)}
	}
	return nil
}

func toLocationRefFlightData(row *flightdata.FlightData) *responsedto.FlightDataLocationRef {
	if row == nil {
		return nil
	}
	if row.ToICAO != nil {
		code := strings.TrimSpace(row.ToICAO.ICAOCode)
		return &responsedto.FlightDataLocationRef{
			Type:  sharedconst.FlightDataLocationTypeICAO,
			ID:    idString(row.ToICAO.ID),
			Code:  code,
			Label: code,
		}
	}
	if row.ToHospital != nil {
		name := strings.TrimSpace(row.ToHospital.Name)
		return &responsedto.FlightDataLocationRef{
			Type:  sharedconst.FlightDataLocationTypeHospital,
			ID:    idString(row.ToHospital.ID),
			Name:  name,
			Label: name,
		}
	}
	if len(row.ToICAOID) > 0 {
		return &responsedto.FlightDataLocationRef{Type: sharedconst.FlightDataLocationTypeICAO, ID: idString(row.ToICAOID)}
	}
	if len(row.ToHospitalID) > 0 {
		return &responsedto.FlightDataLocationRef{Type: sharedconst.FlightDataLocationTypeHospital, ID: idString(row.ToHospitalID)}
	}
	return nil
}

func parseRequiredUUID(raw, pointer string) ([]byte, *jsonapi.ErrorObject) {
	trimmed := strings.TrimSpace(raw)
	if trimmed == "" {
		return nil, &jsonapi.ErrorObject{
			Status: "422",
			Title:  "Validation error",
			Detail: flightdata.ErrorMessageFieldRequired,
			Source: &jsonapi.ErrorSource{Pointer: pointer},
		}
	}
	v, err := uuidv7.ParseString(trimmed)
	if err != nil {
		return nil, &jsonapi.ErrorObject{
			Status: "422",
			Title:  "Validation error",
			Detail: flightdata.ErrorMessageInvalidUUID,
			Source: &jsonapi.ErrorSource{Pointer: pointer},
		}
	}
	return v, nil
}

func parseOptionalUUIDFlightData(raw *string, pointer string) ([]byte, *jsonapi.ErrorObject) {
	if raw == nil {
		return nil, nil
	}
	trimmed := strings.TrimSpace(*raw)
	if trimmed == "" {
		return nil, nil
	}
	v, err := uuidv7.ParseString(trimmed)
	if err != nil {
		return nil, &jsonapi.ErrorObject{
			Status: "422",
			Title:  "Validation error",
			Detail: flightdata.ErrorMessageInvalidUUID,
			Source: &jsonapi.ErrorSource{Pointer: pointer},
		}
	}
	return v, nil
}

func parseRFC3339(raw, pointer string) (time.Time, *jsonapi.ErrorObject) {
	v, err := time.Parse(time.RFC3339, strings.TrimSpace(raw))
	if err != nil {
		return time.Time{}, &jsonapi.ErrorObject{
			Status: "422",
			Title:  "Validation error",
			Detail: flightdata.ErrorMessageInvalidRFC3339,
			Source: &jsonapi.ErrorSource{Pointer: pointer},
		}
	}
	return v.UTC(), nil
}

func writeFlightDataServiceError(c *fiber.Ctx, err error, refs ...[]byte) error {
	var (
		fromICAOID     []byte
		fromHospitalID []byte
		toICAOID       []byte
		toHospitalID   []byte
	)
	if len(refs) > 2 {
		fromICAOID = refs[2]
	}
	if len(refs) > 3 {
		fromHospitalID = refs[3]
	}
	if len(refs) > 4 {
		toICAOID = refs[4]
	}
	if len(refs) > 5 {
		toHospitalID = refs[5]
	}

	switch {
	case errors.Is(err, flightdata.ErrMissionReference):
		return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
			Status: "422",
			Title:  "Validation error",
			Detail: flightdata.ErrorMessageMissionReference,
			Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/mission_id"},
		}})
	case errors.Is(err, flightdata.ErrOriginLocationReference):
		pointer := "/data/attributes/from_icao_id"
		if len(fromICAOID) == 0 && len(fromHospitalID) > 0 {
			pointer = "/data/attributes/from_hospital_id"
		}
		return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
			Status: "422",
			Title:  "Validation error",
			Detail: flightdata.ErrorMessageOriginLocationReference,
			Source: &jsonapi.ErrorSource{Pointer: pointer},
		}})
	case errors.Is(err, flightdata.ErrDestinationLocationReference):
		pointer := "/data/attributes/to_icao_id"
		if len(toICAOID) == 0 && len(toHospitalID) > 0 {
			pointer = "/data/attributes/to_hospital_id"
		}
		return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
			Status: "422",
			Title:  "Validation error",
			Detail: flightdata.ErrorMessageDestinationLocationReference,
			Source: &jsonapi.ErrorSource{Pointer: pointer},
		}})
	case errors.Is(err, flightdata.ErrCoPilotReference):
		return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
			Status: "422",
			Title:  "Validation error",
			Detail: flightdata.ErrorMessageCoPilotReference,
			Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/co_pilot_id"},
		}})
	case errors.Is(err, flightdata.ErrCreateBlocked):
		return writeFlightDataErrors(c, fiber.StatusConflict, []jsonapi.ErrorObject{{
			Status: "409",
			Title:  "Conflict",
			Detail: flightdata.ErrorMessageCreateBlocked,
		}})
	case errors.Is(err, flightdata.ErrLocked):
		return writeFlightDataErrors(c, fiber.StatusLocked, []jsonapi.ErrorObject{{
			Status: "423",
			Title:  "Locked",
			Detail: flightdata.ErrorMessageLocked,
		}})
	case errors.Is(err, flightdata.ErrLockedAfterFlightInspection):
		return writeFlightDataErrors(c, fiber.StatusLocked, []jsonapi.ErrorObject{{
			Status: "423",
			Title:  "Locked",
			Detail: flightdata.ErrorMessageLockedAfterFlightInspection,
		}})
	case errors.Is(err, flightdata.ErrRequired):
		return writeFlightDataErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "Create failed",
			Detail: flightdata.ErrorMessageRequired,
		}})
	case flightDataFKError(err) != nil:
		obj := flightDataFKError(err)
		return writeFlightDataErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*obj})
	default:
		return writeFlightDataErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "Create failed",
			Detail: safeInternalDetail(),
		}})
	}
}

func flightDataFKError(err error) *jsonapi.ErrorObject {
	if err == nil {
		return nil
	}
	var mysqlErr *mysqldriver.MySQLError
	if !errors.As(err, &mysqlErr) || mysqlErr.Number != 1452 {
		return nil
	}

	if _, errs, ok := mapOperationRelationError(err, map[string]relationFieldError{
		"(`mission_id`)":       {Pointer: "/data/attributes/mission_id", Detail: flightdata.ErrorMessageMissionReference},
		"(`from_icao_id`)":     {Pointer: "/data/attributes/from_icao_id", Detail: flightdata.ErrorMessageOriginLocationReference},
		"(`from_hospital_id`)": {Pointer: "/data/attributes/from_hospital_id", Detail: flightdata.ErrorMessageOriginLocationReference},
		"(`to_icao_id`)":       {Pointer: "/data/attributes/to_icao_id", Detail: flightdata.ErrorMessageDestinationLocationReference},
		"(`to_hospital_id`)":   {Pointer: "/data/attributes/to_hospital_id", Detail: flightdata.ErrorMessageDestinationLocationReference},
		"(`pilot_id`)":         {Pointer: "/data/attributes/co_pilot_id", Detail: flightdata.ErrorMessageCoPilotReference},
		"(`co_pilot_id`)":      {Pointer: "/data/attributes/co_pilot_id", Detail: flightdata.ErrorMessageCoPilotReference},
	}); ok && len(errs) > 0 {
		return &errs[0]
	}

	return &jsonapi.ErrorObject{
		Status: "422",
		Title:  "Validation error",
		Detail: "referenced relation data was not found",
	}
}

func writeFlightDataErrors(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 := inferFlightDataDef(status, e.Title, e.Detail, e.Source)
		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 inferFlightDataDef(status int, title, detail string, source *jsonapi.ErrorSource) apperrorsx.Def {
	lowerTitle := strings.ToLower(strings.TrimSpace(title))
	lowerDetail := strings.ToLower(strings.TrimSpace(detail))
	pointer := ""
	if source != nil {
		pointer = strings.ToLower(strings.TrimSpace(source.Pointer))
	}

	switch status {
	case fiber.StatusNotFound:
		return apperrorsx.ErrFlightDataNotFound
	case fiber.StatusLocked:
		switch {
		case strings.Contains(lowerDetail, "after flight inspection already exists"):
			return apperrorsx.ErrFlightDataLockedAfterFlightInspection
		case strings.Contains(lowerDetail, "fm report is completed"),
			strings.Contains(lowerDetail, "flight data is locked because fm report is completed"):
			return apperrorsx.ErrFlightDataLocked
		default:
			return apperrorsx.ErrFlightDataLocked
		}
	case fiber.StatusUnprocessableEntity:
		switch {
		case strings.Contains(lowerDetail, "invalid path uuid"), pointer == "/path/uuid":
			return apperrorsx.ErrFlightDataInvalidPathUUID
		case strings.Contains(lowerDetail, "field is required"), pointer == "/data/attributes/mission_id":
			return apperrorsx.ErrFlightDataMissionIDRequired
		case strings.Contains(lowerDetail, "mission reference"):
			return apperrorsx.ErrFlightDataMissionReference
		case strings.Contains(lowerDetail, "field is required"), pointer == "/data/attributes/co_pilot_id", pointer == "/data/attributes/pilot_id":
			return apperrorsx.ErrFlightDataPilotIDRequired
		case strings.Contains(lowerDetail, "pilot must reference an existing user"):
			return apperrorsx.ErrFlightDataPilotIDRequired
		case strings.Contains(lowerDetail, "invalid uuid"):
			return apperrorsx.ErrFlightDataInvalidUUID
		case strings.Contains(lowerDetail, "invalid rfc3339"):
			return apperrorsx.ErrFlightDataInvalidRFC3339
		case strings.Contains(lowerDetail, "origin location reference"):
			return apperrorsx.ErrFlightDataOriginLocationReference
		case strings.Contains(lowerDetail, "destination location reference"):
			return apperrorsx.ErrFlightDataDestinationLocationReference
		case strings.Contains(lowerDetail, "pilot reference"):
			return apperrorsx.ErrFlightDataPilotReference
		case strings.Contains(lowerDetail, "facility reference"):
			return apperrorsx.ErrFlightDataFacilityReference
		case strings.Contains(lowerDetail, "cannot be created"), strings.Contains(lowerDetail, "already exists"):
			return apperrorsx.ErrFlightDataCreateBlocked
		default:
			return apperrorsx.ErrFlightDataInvalidPayload
		}
	case fiber.StatusBadRequest:
		switch {
		case strings.Contains(lowerTitle, "invalid json"):
			return apperrorsx.ErrFlightDataInvalidJSON
		case strings.Contains(lowerTitle, "create failed"):
			if strings.Contains(lowerDetail, "required data is missing") {
				return apperrorsx.ErrFlightDataRequired
			}
			return apperrorsx.ErrFlightDataCreateFailed
		case strings.Contains(lowerTitle, "get failed"):
			return apperrorsx.ErrFlightDataGetFailed
		default:
			return apperrorsx.ErrInvalidRequest
		}
	default:
		return apperrorsx.ErrInternal
	}
}

func inferFlightDataErrorCode(status int, title, detail string, source *jsonapi.ErrorSource) (string, string) {
	def := inferFlightDataDef(status, title, detail, source)
	return def.Code, def.ErrorCode
}

func parseSPOUpsertData(attrs requestdto.FlightDataCreateAttributes) *flightdata.SPOUpsertData {
	if attrs.HESLO == nil && attrs.Logging == nil && attrs.HEC == nil {
		return nil
	}
	data := &flightdata.SPOUpsertData{}

	if attrs.HESLO != nil {
		heslo := &flightdata.SPOHESLOUpsert{
			Flights: make([]flightdata.SPOHESLOFlightUpsert, 0, len(attrs.HESLO.Flights)),
		}
		for _, f := range attrs.HESLO.Flights {
			fu := flightdata.SPOHESLOFlightUpsert{ROT: f.ROT}
			if id, err := uuidv7.ParseString(f.HESLORopeLabelID); err == nil {
				fu.FuelTruckID = id
			}
			for _, s := range f.Slings {
				if id, err := uuidv7.ParseString(s.SlingID); err == nil {
					fu.Slings = append(fu.Slings, flightdata.SPOHESLOSlingUpsert{SlingID: id})
				}
			}
			heslo.Flights = append(heslo.Flights, fu)
		}
		data.HESLO = heslo
	}

	if attrs.Logging != nil {
		logging := &flightdata.SPOLoggingUpsert{
			Slings: make([]flightdata.SPOLoggingSlingUpsert, 0, len(attrs.Logging.Slings)),
		}
		for _, s := range attrs.Logging.Slings {
			if id, err := uuidv7.ParseString(s.SlingID); err == nil {
				logging.Slings = append(logging.Slings, flightdata.SPOLoggingSlingUpsert{SlingID: id})
			}
		}
		data.Logging = logging
	}

	if attrs.HEC != nil {
		hec := &flightdata.SPOHECUpsert{
			Flights: make([]flightdata.SPOHECFlightUpsert, 0, len(attrs.HEC.Flights)),
			Slings:  make([]flightdata.SPOHECSlingUpsert, 0, len(attrs.HEC.Slings)),
			Loads:   make([]flightdata.SPOHECLoadUpsert, 0, len(attrs.HEC.Loads)),
		}
		for _, f := range attrs.HEC.Flights {
			fu := flightdata.SPOHECFlightUpsert{ROT: f.ROT, HECCycle: f.HECCycle}
			if id, err := uuidv7.ParseString(f.EquipmentID); err == nil {
				fu.EquipmentID = id
			}
			hec.Flights = append(hec.Flights, fu)
		}
		for _, s := range attrs.HEC.Slings {
			if id, err := uuidv7.ParseString(s.SlingID); err == nil {
				hec.Slings = append(hec.Slings, flightdata.SPOHECSlingUpsert{SlingID: id})
			}
		}
		for _, l := range attrs.HEC.Loads {
			hec.Loads = append(hec.Loads, flightdata.SPOHECLoadUpsert{
				LoadCategory: l.LoadCategory,
				Quantity:     l.Quantity,
			})
		}
		data.HEC = hec
	}

	return data
}

// --- Response building (flight data → JSON:API resource) ---

func buildFlightDataResourceWithService(ctx context.Context, svc flightdata.Service, row *flightdata.FlightData) (responsedto.FlightDataResource, error) {
	resource := responsedto.FlightDataResource{
		Type:       sharedconst.FlightDataResourceType,
		ID:         idString(row.ID),
		Attributes: buildBaseFlightDataAttributes(row),
	}

	heslo, logging, hec, err := buildFlightDataSPODetails(ctx, svc, row)
	if err != nil {
		return responsedto.FlightDataResource{}, err
	}
	resource.Attributes.HESLO = heslo
	resource.Attributes.Logging = logging
	resource.Attributes.HEC = hec
	return resource, nil
}

func buildBaseFlightDataAttributes(row *flightdata.FlightData) responsedto.FlightDataAttributes {
	return responsedto.FlightDataAttributes{
		Status: func() string {
			if status := strings.TrimSpace(row.Status); status != "" {
				return status
			}
			return flightdata.StatusInProgress
		}(),
		Mission:               missionRefFlightData(row),
		RosterDetails:         rosterDetailsFromFlightData(row),
		CoPilot:               coPilotRefFlightData(row),
		From:                  fromLocationRefFlightData(row),
		To:                    toLocationRefFlightData(row),
		FlightTakeOff:         formatTimeOrEmpty(row.FlightTakeOff),
		FlightLanding:         formatTimeOrEmpty(row.FlightLanding),
		FlightDuration:        int64(row.FlightDuration.Seconds()),
		FlightRED:             int64(row.FlightRED.Seconds()),
		MaxN1:                 row.MaxN1,
		MaxN2:                 row.MaxN2,
		PaxCount:              row.PaxCount,
		LandingCount:          row.LandingCount,
		RotorBrakeCycle:       row.RotorBrakeCycle,
		HookReleases:          row.HookReleases,
		TicketNo:              row.TicketNo,
		Engine:                row.Engine,
		DeliveryNoteNumber:    row.DeliveryNoteNumber,
		CustomerName:          row.CustomerName,
		FlightPlanDistance:    row.FlightPlanDistance,
		FlightPlanTimeSeconds: int64(row.FlightPlanTime.Seconds()),
		FlightPlanTrueCourse:  row.FlightPlanTrueCourse,
		FuelBeforeFlight:      row.FuelBeforeFlight,
		FuelUpload:            row.FuelUpload,
		FuelAfterFlight:       row.FuelAfterFlight,
		FuelPlanning:          row.FuelPlanning,
		FlightPositioning:     row.FlightPositioning,
		OtherInformation:      row.OtherInformation,
		CreatedAt:             formatTimeOrEmpty(row.CreatedAt),
		CreatedBy:             uuidStringOrEmpty(row.CreatedBy),
		UpdatedAt:             formatTimeOrEmpty(row.UpdatedAt),
		UpdatedBy:             uuidStringOrEmpty(row.UpdatedBy),
	}
}

func rosterDetailsFromFlightData(row *flightdata.FlightData) *responsedto.TakeoverDutyRosterDetailsResponse {
	if row == nil || row.Mission == nil || row.Mission.Flight == nil || row.Mission.Flight.Takeover == nil {
		return nil
	}
	takeoverRow := row.Mission.Flight.Takeover
	if len(takeoverRow.RosterCrews) == 0 && len(takeoverRow.OtherPeople) == 0 {
		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.Mission.Flight)
	resource = buildDutyRosterResourceFromOtherPeople(resource, takeoverRow.OtherPeople)
	detail := takeoverDutyRosterDetailsFromRosterDetails(resource.Attributes.Detail)
	return &detail
}

func formatTimeOrEmpty(value time.Time) string {
	if value.IsZero() {
		return ""
	}
	return value.UTC().Format(time.RFC3339)
}

func buildFlightDataSPODetails(
	ctx context.Context,
	svc flightdata.Service,
	row *flightdata.FlightData,
) (
	*responsedto.FlightDataSPOHESLODetails,
	*responsedto.FlightDataSPOLoggingDetails,
	*responsedto.FlightDataSPOHECDetails,
	error,
) {
	if svc == nil || row == nil || row.Mission == nil || strings.ToUpper(strings.TrimSpace(row.Mission.Type)) != "SPO" {
		return nil, nil, nil, nil
	}
	if len(row.ID) != 16 {
		return nil, nil, nil, nil
	}

	details, err := svc.GetSPODetailsByFlightDataID(ctx, row.ID)
	if err != nil || details == nil {
		return nil, nil, nil, err
	}

	heslo := &responsedto.FlightDataSPOHESLODetails{
		Flights: make([]responsedto.FlightDataSPOHESLOFlight, 0, len(details.HESLO.Flights)),
	}
	logging := &responsedto.FlightDataSPOLoggingDetails{
		Slings: make([]responsedto.FlightDataSPOLoggingSling, 0, len(details.Logging.Slings)),
	}
	hec := &responsedto.FlightDataSPOHECDetails{
		Flights: make([]responsedto.FlightDataSPOHECFlight, 0, len(details.HEC.Flights)),
		Slings:  make([]responsedto.FlightDataSPOHECSling, 0, len(details.HEC.Slings)),
		Loads:   make([]responsedto.FlightDataSPOHECLoad, 0, len(details.HEC.Loads)),
	}

	slingsByFlightID := make(map[string][]responsedto.FlightDataSPOHESLOSling, len(details.HESLO.Slings))
	for i := range details.HESLO.Slings {
		flightID := idString(details.HESLO.Slings[i].HESLOFlightID)
		slingsByFlightID[flightID] = append(slingsByFlightID[flightID], responsedto.FlightDataSPOHESLOSling{
			ID:        idString(details.HESLO.Slings[i].ID),
			SlingID:   idString(details.HESLO.Slings[i].SlingID),
			SlingName: strings.TrimSpace(details.HESLO.Slings[i].SlingName),
		})
	}

	for i := range details.HESLO.Flights {
		flightID := idString(details.HESLO.Flights[i].ID)
		heslo.Flights = append(heslo.Flights, responsedto.FlightDataSPOHESLOFlight{
			ID:                 flightID,
			ROT:                details.HESLO.Flights[i].ROT,
			HESLORopeLabelID:   idString(details.HESLO.Flights[i].FuelTruckID),
			HESLORopeLabelName: strings.TrimSpace(details.HESLO.Flights[i].FuelTruckName),
			Slings:             slingsByFlightID[flightID],
		})
	}
	for i := range details.Logging.Slings {
		logging.Slings = append(logging.Slings, responsedto.FlightDataSPOLoggingSling{
			ID:        idString(details.Logging.Slings[i].ID),
			SlingID:   idString(details.Logging.Slings[i].SlingID),
			SlingName: strings.TrimSpace(details.Logging.Slings[i].SlingName),
		})
	}
	for i := range details.HEC.Flights {
		hec.Flights = append(hec.Flights, responsedto.FlightDataSPOHECFlight{
			ID:            idString(details.HEC.Flights[i].ID),
			ROT:           details.HEC.Flights[i].ROT,
			HECCycle:      details.HEC.Flights[i].HECCycle,
			EquipmentID:   idString(details.HEC.Flights[i].EquipmentID),
			EquipmentName: strings.TrimSpace(details.HEC.Flights[i].EquipmentName),
		})
	}
	for i := range details.HEC.Slings {
		hec.Slings = append(hec.Slings, responsedto.FlightDataSPOHECSling{
			ID:        idString(details.HEC.Slings[i].ID),
			SlingID:   idString(details.HEC.Slings[i].SlingID),
			SlingName: strings.TrimSpace(details.HEC.Slings[i].SlingName),
		})
	}
	for i := range details.HEC.Loads {
		hec.Loads = append(hec.Loads, responsedto.FlightDataSPOHECLoad{
			ID:           idString(details.HEC.Loads[i].ID),
			LoadCategory: strings.TrimSpace(details.HEC.Loads[i].LoadCategory),
			Quantity:     details.HEC.Loads[i].Quantity,
		})
	}

	return heslo, logging, hec, nil
}
