package handlers

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

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

	filemanager "wucher/internal/domain/file_manager"
	flightdata "wucher/internal/domain/flight_data"
	"wucher/internal/domain/mission"
	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 MissionHandler struct {
	svc           mission.Service
	flightDataSvc flightdata.Service
	fileManager   filemanager.Service
	storage       fileManagerDownloadURLStorage
	validate      *validators.Validator
}

func NewMissionHandler(svc mission.Service) *MissionHandler {
	return &MissionHandler{
		svc:      svc,
		validate: validators.New(),
	}
}

func (h *MissionHandler) WithFlightDataService(svc flightdata.Service) *MissionHandler {
	h.flightDataSvc = svc
	return h
}

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

func (h *MissionHandler) WithFileManagerService(svc filemanager.Service) *MissionHandler {
	h.fileManager = svc
	return h
}

func (h *MissionHandler) attachMissionFiles(ctx context.Context, missionID []byte, files []requestdto.MissionFileInput, actorID []byte) {
	if len(files) == 0 || h.fileManager == nil {
		return
	}
	folderID, err := ensureMissionFileFolderPath(ctx, h.fileManager, missionID, actorID)
	if err != nil {
		return
	}
	missionRefID := idString(missionID)
	for i := range files {
		uploadIntentRaw := strings.TrimSpace(files[i].UploadIntentUUID)
		if uploadIntentRaw == "" {
			continue
		}
		uploadIntentID, parseErr := uuidv7.ParseString(uploadIntentRaw)
		if parseErr != nil {
			continue
		}
		name := strings.TrimSpace(files[i].Name)
		created, createErr := h.fileManager.CreateFileNodeFromUploadIntent(ctx, filemanager.CreateFileFromUploadIntentParams{
			UploadIntentID: uploadIntentID,
			FolderID:       folderID,
			FolderProvided: true,
			Name:           name,
			ActorID:        actorID,
		})
		if createErr != nil {
			if !errors.Is(createErr, filemanager.ErrDuplicateName) {
				continue
			}
			existing, exErr := h.fileManager.GetFileByFolderAndName(ctx, folderID, strings.ToLower(name))
			if exErr != nil || existing == nil {
				continue
			}
			created = existing
		}
		if created == nil {
			continue
		}
		attachParams := filemanager.CreateAttachmentParams{
			FileID:    created.ID,
			RefType:   "mission",
			RefID:     missionRefID,
			SortOrder: 0,
			IsPrimary: false,
			ActorID:   actorID,
		}
		// The file finishes processing (→ ready) asynchronously, shortly after
		// the upload intent is finalized above. CreateAttachment rejects a
		// not-yet-ready file, so wait briefly for readiness instead of silently
		// dropping the attachment (bounded ~10s; processing is normally ~1-2s).
		var attachment *filemanager.Attachment
		var attachErr error
		for attempt := 0; attempt < 40; attempt++ {
			attachment, attachErr = h.fileManager.CreateAttachment(ctx, attachParams)
			if attachErr == nil || !errors.Is(attachErr, filemanager.ErrFileNotReadyForAttachment) {
				break
			}
			if ctx.Err() != nil {
				break
			}
			time.Sleep(250 * time.Millisecond)
		}
		if attachErr != nil || attachment == nil {
			continue
		}
		_ = h.svc.AttachFile(ctx, missionID, attachment.ID)
	}
}

// CreateMission godoc
// @Summary Create mission
// @Description Create a mission and link it to a flight.
// @Description `type` must be one of: `HEMS`, `CAT`, `SPO`, `NCO`.
// @Description `subtype_id` is required when `type=HEMS` and must reference mission subcategory under `HEMS` category.
// @Description `subtype_id` must be empty when `type` is not `HEMS`.
// @Tags Flights - Mission
// @Accept json
// @Produce json
// @Param request body requestdto.MissionCreateRequest true "JSON:API mission create request"
// @Success 201 {object} responsedto.MissionResponse
// @Failure 422 {object} jsonapi.ErrorDocument
// @Failure 409 {object} jsonapi.ErrorDocument
// @Failure 400 {object} jsonapi.ErrorDocument
// @Router /api/v1/mission/create [post]
func (h *MissionHandler) Create(c *fiber.Ctx) error {
	var req requestdto.MissionCreateRequest
	if err := c.BodyParser(&req); err != nil {
		return writeMissionError(c, apperrorsx.ErrMissionInvalidJSON, err.Error(), nil)
	}
	if errs := h.validate.ValidateStruct(req); errs != nil {
		return writeMissionValidationErrors(c, errs)
	}

	flightID, err := uuidv7.ParseString(strings.TrimSpace(req.Data.Attributes.FlightID))
	if err != nil {
		return writeMissionError(c, apperrorsx.ErrMissionInvalidFlightUUID, mission.ErrorMessageInvalidFlightID, &jsonapi.ErrorSource{Pointer: "/data/attributes/flight_id"})
	}

	var subtypeID []byte
	subtypeRaw := strings.TrimSpace(req.Data.Attributes.SubtypeID)
	if subtypeRaw != "" {
		subtypeID, err = uuidv7.ParseString(subtypeRaw)
		if err != nil {
			return writeMissionError(c, apperrorsx.ErrMissionSubtypeInvalid, mission.ErrorMessageSubtypeInvalid, &jsonapi.ErrorSource{Pointer: "/data/attributes/subtype_id"})
		}
	}

	row := &mission.Mission{
		FlightID:  flightID,
		Type:      strings.ToUpper(strings.TrimSpace(req.Data.Attributes.Type)),
		SubtypeID: subtypeID,
		Note:      strings.TrimSpace(req.Data.Attributes.Note),
		CreatedBy: actorUserID(c),
		UpdatedBy: actorUserID(c),
	}
	if err := h.svc.Create(c.UserContext(), row); err != nil {
		return writeMissionServiceError(c, err, "/data/attributes/flight_id")
	}

	h.attachMissionFiles(c.UserContext(), row.ID, req.Data.Attributes.Files, actorUserID(c))
	if created, getErr := h.svc.GetByID(c.UserContext(), row.ID); getErr == nil && created != nil {
		row = created
	}

	doc, err := h.missionDocument(c.UserContext(), row)
	if err != nil {
		return writeMissionError(c, apperrorsx.ErrMissionCreateFailed, err.Error(), nil)
	}
	return response.Write(c, fiber.StatusCreated, doc)
}

// CreateMissionType godoc
// @Summary Create mission type
// @Description Create a mission category master record for use in mission flow.
// @Tags Flights - Mission
// @Accept json
// @Produce json
// @Param request body requestdto.MissionTypeCreateRequest true "JSON:API mission type create request"
// @Success 201 {object} responsedto.MissionTypeResource
// @Failure 422 {object} jsonapi.ErrorDocument
// @Failure 400 {object} jsonapi.ErrorDocument
// @Router /api/v1/mission-type/create [post]
func (h *MissionHandler) CreateType(c *fiber.Ctx) error {
	var req requestdto.MissionTypeCreateRequest
	if err := c.BodyParser(&req); err != nil {
		return writeMissionError(c, apperrorsx.ErrMissionInvalidJSON, err.Error(), nil)
	}
	if errs := h.validate.ValidateStruct(req); errs != nil {
		return writeMissionValidationErrors(c, errs)
	}

	row := &mission.MissionCategory{
		CodeType: strings.ToUpper(strings.TrimSpace(req.Data.Attributes.CodeType)),
		TypeName: strings.TrimSpace(req.Data.Attributes.TypeName),
	}
	if err := h.svc.CreateType(c.UserContext(), row); err != nil {
		return writeMissionServiceError(c, err, "/data/attributes/code_type")
	}

	return response.Write(c, fiber.StatusCreated, missionTypeResource(row))
}

// UpdateMission godoc
// @Summary Update mission
// @Description Update mission type/subtype/note by mission ID, and optionally attach files already uploaded via `upload-mission-file`.
// @Description `type` must be one of: `HEMS`, `CAT`, `SPO`, `NCO`.
// @Description `subtype_id` is required when `type=HEMS` and must reference mission subcategory under `HEMS` category.
// @Description `subtype_id` must be empty when `type` is not `HEMS`.
// @Description `files[]` items reference an `upload_intent_uuid` (from `upload-mission-file`) and an optional display `name`.
// @Tags Flights - Mission
// @Accept json
// @Produce json
// @Param mission_id path string true "Mission ID (UUIDv7)"
// @Param request body requestdto.MissionUpdateRequest true "JSON:API mission update request"
// @Success 200 {object} responsedto.MissionResponse
// @Failure 422 {object} jsonapi.ErrorDocument
// @Failure 404 {object} jsonapi.ErrorDocument
// @Failure 400 {object} jsonapi.ErrorDocument
// @Router /api/v1/mission/update/{mission_id} [patch]
func (h *MissionHandler) Update(c *fiber.Ctx) error {
	missionRaw := strings.TrimSpace(c.Params("mission_id"))
	missionID, err := uuidv7.ParseString(missionRaw)
	if err != nil {
		return writeMissionError(c, apperrorsx.ErrMissionInvalidMissionUUID, mission.ErrorMessageInvalidMissionID, &jsonapi.ErrorSource{Pointer: "/path/mission_id"})
	}

	var req requestdto.MissionUpdateRequest
	if err := c.BodyParser(&req); err != nil {
		return writeMissionError(c, apperrorsx.ErrMissionInvalidJSON, err.Error(), nil)
	}
	if errs := h.validate.ValidateStruct(req); errs != nil {
		return writeMissionValidationErrors(c, errs)
	}

	var subtypeID []byte
	subtypeRaw := strings.TrimSpace(req.Data.Attributes.SubtypeID)
	if subtypeRaw != "" {
		subtypeID, err = uuidv7.ParseString(subtypeRaw)
		if err != nil {
			return writeMissionError(c, apperrorsx.ErrMissionSubtypeInvalid, mission.ErrorMessageSubtypeInvalid, &jsonapi.ErrorSource{Pointer: "/data/attributes/subtype_id"})
		}
	}

	if err := h.svc.UpdateByID(
		c.UserContext(),
		missionID,
		strings.ToUpper(strings.TrimSpace(req.Data.Attributes.Type)),
		subtypeID,
		strings.TrimSpace(req.Data.Attributes.Note),
		actorUserID(c),
	); err != nil {
		return writeMissionServiceError(c, err, "/path/mission_id")
	}

	h.attachMissionFiles(c.UserContext(), missionID, req.Data.Attributes.Files, actorUserID(c))

	row, err := h.svc.GetByID(c.UserContext(), missionID)
	if err != nil {
		return writeMissionServiceError(c, err, "/path/mission_id")
	}
	if row == nil {
		return writeMissionError(c, apperrorsx.ErrMissionNotFound, mission.ErrorMessageNotFound, nil)
	}
	doc, buildErr := h.missionDocument(c.UserContext(), row)
	if buildErr != nil {
		return writeMissionError(c, apperrorsx.ErrMissionUpdateFailed, buildErr.Error(), nil)
	}
	return response.Write(c, fiber.StatusOK, doc)
}

// RemoveFile godoc
// @Summary Remove a file from a mission
// @Description Detaches the file (by its attachment id) from the mission and deletes the attachment.
// @Tags Flights - Mission
// @Produce json
// @Param mission_id path string true "Mission UUID"
// @Param attachment_id path string true "Attachment UUID (the file's attachment_id)"
// @Success 200 {object} responsedto.MissionResponse
// @Failure 404 {object} jsonapi.ErrorDocument
// @Failure 400 {object} jsonapi.ErrorDocument
// @Router /api/v1/mission/file/{mission_id}/{attachment_id} [delete]
func (h *MissionHandler) RemoveFile(c *fiber.Ctx) error {
	missionID, err := uuidv7.ParseString(strings.TrimSpace(c.Params("mission_id")))
	if err != nil {
		return writeMissionError(c, apperrorsx.ErrMissionInvalidMissionUUID, mission.ErrorMessageInvalidMissionID, &jsonapi.ErrorSource{Pointer: "/path/mission_id"})
	}
	attachmentID, err := uuidv7.ParseString(strings.TrimSpace(c.Params("attachment_id")))
	if err != nil {
		return writeMissionError(c, apperrorsx.ErrMissionInvalidMissionUUID, "invalid attachment id", &jsonapi.ErrorSource{Pointer: "/path/attachment_id"})
	}

	if err := h.svc.DetachFile(c.UserContext(), missionID, attachmentID); err != nil {
		return writeMissionServiceError(c, err, "/path/attachment_id")
	}
	if h.fileManager != nil {
		// Best-effort: the junction row is already gone; an orphaned attachment
		// is harmless and is cleaned up by lifecycle jobs.
		_ = h.fileManager.DeleteAttachment(c.UserContext(), attachmentID)
	}

	row, err := h.svc.GetByID(c.UserContext(), missionID)
	if err != nil {
		return writeMissionServiceError(c, err, "/path/mission_id")
	}
	if row == nil {
		return writeMissionError(c, apperrorsx.ErrMissionNotFound, mission.ErrorMessageNotFound, nil)
	}
	doc, buildErr := h.missionDocument(c.UserContext(), row)
	if buildErr != nil {
		return writeMissionError(c, apperrorsx.ErrMissionUpdateFailed, buildErr.Error(), nil)
	}
	return response.Write(c, fiber.StatusOK, doc)
}

// DeleteMission godoc
// @Summary Delete mission
// @Description Soft delete mission by linked flight ID.
// @Tags Flights - Mission
// @Produce json
// @Param flight_id path string true "Flight ID (UUIDv7)"
// @Success 200 {object} dto.GenericDeleteResponse
// @Failure 422 {object} jsonapi.ErrorDocument
// @Failure 404 {object} jsonapi.ErrorDocument
// @Failure 400 {object} jsonapi.ErrorDocument
// @Router /api/v1/mission/delete/{flight_id} [delete]
func (h *MissionHandler) Delete(c *fiber.Ctx) error {
	flightRaw := strings.TrimSpace(c.Params("flight_id"))
	flightID, err := uuidv7.ParseString(flightRaw)
	if err != nil {
		return writeMissionError(c, apperrorsx.ErrMissionInvalidFlightUUID, mission.ErrorMessageInvalidFlightID, &jsonapi.ErrorSource{Pointer: "/path/flight_id"})
	}

	if err := h.svc.DeleteByFlightID(c.UserContext(), flightID, actorUserID(c)); err != nil {
		return writeMissionServiceError(c, err, "/path/flight_id")
	}

	return response.Write(c, fiber.StatusOK, dto.NewGenericDeleteResponse("mission_delete"))
}

// DeleteByIDMission godoc
// @Summary Delete mission by mission ID
// @Description Soft delete mission by its mission UUID. This also soft deletes flight data rows linked to the mission.
// @Tags Flights - Mission
// @Produce json
// @Param mission_id path string true "Mission ID (UUIDv7)"
// @Success 200 {object} dto.GenericDeleteResponse
// @Failure 422 {object} jsonapi.ErrorDocument
// @Failure 404 {object} jsonapi.ErrorDocument
// @Failure 400 {object} jsonapi.ErrorDocument
// @Router /api/v1/mission/delete-by-id/{mission_id} [delete]
func (h *MissionHandler) DeleteByID(c *fiber.Ctx) error {
	missionRaw := strings.TrimSpace(c.Params("mission_id"))
	missionID, err := uuidv7.ParseString(missionRaw)
	if err != nil {
		return writeMissionError(c, apperrorsx.ErrMissionInvalidMissionUUID, mission.ErrorMessageInvalidMissionID, &jsonapi.ErrorSource{Pointer: "/path/mission_id"})
	}

	if err := h.svc.DeleteByID(c.UserContext(), missionID, actorUserID(c)); err != nil {
		return writeMissionServiceError(c, err, "/path/mission_id")
	}

	return response.Write(c, fiber.StatusOK, dto.NewGenericDeleteResponse("mission_delete"))
}

// GetMissionByFlightID godoc
// @Summary Get mission by flight ID
// @Description Get mission detail by linked flight ID.
// @Tags Flights - Mission
// @Produce json
// @Param flight_id path string true "Flight ID (UUIDv7)"
// @Success 200 {object} responsedto.MissionResponse
// @Failure 422 {object} jsonapi.ErrorDocument
// @Failure 404 {object} jsonapi.ErrorDocument
// @Failure 400 {object} jsonapi.ErrorDocument
// @Router /api/v1/mission/get/{flight_id} [get]
func (h *MissionHandler) GetByFlightID(c *fiber.Ctx) error {
	flightRaw := strings.TrimSpace(c.Params("flight_id"))
	if flightRaw == "" {
		flightRaw = strings.TrimSpace(c.Params("flight_uuid"))
	}
	flightID, err := uuidv7.ParseString(flightRaw)
	if err != nil {
		return writeMissionError(c, apperrorsx.ErrMissionInvalidFlightUUID, mission.ErrorMessageInvalidFlight, &jsonapi.ErrorSource{Pointer: "/path/flight_id"})
	}

	rows, err := h.svc.ListByFlightID(c.UserContext(), flightID)
	if err != nil {
		return writeMissionServiceError(c, err, "/path/flight_id")
	}
	if len(rows) == 0 {
		return writeMissionError(c, apperrorsx.ErrMissionNotFound, mission.ErrorMessageNotFound, nil)
	}

	doc, buildErr := h.missionDocuments(c.UserContext(), rows)
	if buildErr != nil {
		return writeMissionError(c, apperrorsx.ErrMissionGetFailed, buildErr.Error(), nil)
	}
	return response.Write(c, fiber.StatusOK, doc)
}

// ListMissions godoc
// @Summary List missions
// @Description Supports `search`, `filter[search]`, `flight_id`, `sort`, `page[number]`, `page[size]`, `page`, `size`, and `limit`.
// @Tags Flights - Mission
// @Produce json
// @Param search query string false "Search text"
// @Param filter[search] query string false "Search text (JSON:API style)"
// @Param flight_id query string false "Filter by flight UUID"
// @Param flight_data_status query string false "Filter missions by flight data status: draft, in_progress, or completed"
// @Param sort query string false "Sort field: type|-type|flight_date|-flight_date|created_at|-created_at|updated_at|-updated_at"
// @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.MissionListResponse
// @Failure 400 {object} jsonapi.ErrorDocument
// @Router /api/v1/mission/get-all [get]
func (h *MissionHandler) List(c *fiber.Ctx) error {
	filter := c.Query("filter[search]")
	if filter == "" {
		filter = c.Query("search")
	}
	flightQuery := strings.TrimSpace(c.Query("flight_id"))
	var flightID []byte
	if flightQuery != "" {
		parsedFlightID, err := uuidv7.ParseString(flightQuery)
		if err != nil {
			return writeMissionError(c, apperrorsx.ErrMissionInvalidFlightUUID, mission.ErrorMessageInvalidFlightID, &jsonapi.ErrorSource{Pointer: "/query/flight_id"})
		}
		flightID = parsedFlightID
	}
	flightDataStatus := strings.TrimSpace(c.Query("flight_data_status"))
	if flightDataStatus != "" &&
		flightDataStatus != mission.StatusDraft &&
		flightDataStatus != mission.StatusInProgress &&
		flightDataStatus != mission.StatusCompleted {
		return writeMissionError(c, apperrorsx.ErrMissionInvalidPayload, "flight_data_status must be one of: draft, in_progress, completed", &jsonapi.ErrorSource{Pointer: "/query/flight_data_status"})
	}

	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"), sharedconst.MissionDefaultPageSize)
	}
	if pageSize > sharedconst.MissionMaxPageSize {
		pageSize = sharedconst.MissionMaxPageSize
	}
	if pageSize < 1 {
		pageSize = sharedconst.MissionDefaultPageSize
	}
	if pageNumber < 1 {
		pageNumber = 1
	}

	offset := (pageNumber - 1) * pageSize
	_ = offset
	sort := c.Query("sort")
	rows, total, err := h.svc.List(c.UserContext(), filter, sort, flightID, flightDataStatus, 0, 0)
	if err != nil {
		return writeMissionError(c, apperrorsx.ErrMissionListFailed, err.Error(), nil)
	}

	data := make([]responsedto.MissionResource, 0, len(rows))
	for i := range rows {
		resource, buildErr := h.missionResource(c.UserContext(), &rows[i])
		if buildErr != nil {
			return writeMissionError(c, apperrorsx.ErrMissionListFailed, buildErr.Error(), nil)
		}
		data = append(data, resource)
	}

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

// ListMissionDatatable godoc
// @Summary List missions (datatable)
// @Description Supports `search`, `filter[search]`, `date_from`, `date_to`, `mission_type`, `pilot_id`, `helicopter_id`, `sort`, `start`, `length`, `draw`, `page[number]`, `page[size]`, `page`, `size`, and `limit`.
// @Description `date_from` and `date_to` filter flights by their mission date range (`YYYY-MM-DD`).
// @Description `mission_type` accepts `HEMS`, `CAT`, `SPO`, or `NCO`.
// @Description `pilot_id` filters missions where the pilot is assigned on the linked flight roster.
// @Description `helicopter_id` filters missions by the helicopter used in the linked flight takeover.
// @Tags Flights - Mission
// @Produce json
// @Param search query string false "Search text"
// @Param filter[search] query string false "Search text (JSON:API style)"
// @Param flight_id query string false "Filter by linked flight UUID"
// @Param date_from query string false "Mission date lower bound (YYYY-MM-DD)"
// @Param date_to query string false "Mission date upper bound (YYYY-MM-DD)"
// @Param mission_type query string false "Mission type: HEMS|CAT|SPO|NCO"
// @Param pilot_id query string false "Pilot user UUID"
// @Param helicopter_id query string false "Helicopter UUID"
// @Param sort query string false "Sort field: type|-type|flight_date|-flight_date|created_at|-created_at|updated_at|-updated_at"
// @Param start query int false "DataTables start index"
// @Param length query int false "DataTables page size"
// @Param draw query int false "DataTables draw counter"
// @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.MissionDataTableResponse
// @Failure 422 {object} jsonapi.ErrorDocument
// @Failure 400 {object} jsonapi.ErrorDocument
// @Router /api/v1/mission/get-all/dt [get]
func (h *MissionHandler) ListDatatable(c *fiber.Ctx) error {
	filter, errObj := missionListFilterFromQuery(c)
	if errObj != nil {
		return writeMissionError(c, apperrorsx.ErrMissionInvalidPayload, errObj.Detail, errObj.Source)
	}

	draw := parseIntDefault(c.Query("draw"), 1)
	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"), sharedconst.MissionDefaultPageSize)
	}
	if pageSize > sharedconst.MissionMaxPageSize {
		pageSize = sharedconst.MissionMaxPageSize
	}
	if pageSize < 1 {
		pageSize = sharedconst.MissionDefaultPageSize
	}
	if pageNumber < 1 {
		pageNumber = 1
	}

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

	offset := (pageNumber - 1) * pageSize
	rows, total, filtered, err := h.svc.ListDatatable(c.UserContext(), filter, pageSize, offset)
	if err != nil {
		return writeMissionError(c, apperrorsx.ErrMissionListFailed, err.Error(), nil)
	}

	data := make([]responsedto.MissionResource, 0, len(rows))
	for i := range rows {
		resource, buildErr := h.missionResource(c.UserContext(), &rows[i])
		if buildErr != nil {
			return writeMissionError(c, apperrorsx.ErrMissionListFailed, buildErr.Error(), nil)
		}
		data = append(data, resource)
	}

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

// ListMissionTypes godoc
// @Summary List mission types with subtypes
// @Description Returns all mission categories and their available subtypes.
// @Tags Flights - Mission
// @Produce json
// @Success 200 {object} responsedto.MissionTypeListResponse
// @Failure 400 {object} jsonapi.ErrorDocument
// @Router /api/v1/mission-type/get-all [get]
func (h *MissionHandler) ListTypes(c *fiber.Ctx) error {
	rows, err := h.svc.ListTypes(c.UserContext())
	if err != nil {
		return writeMissionError(c, apperrorsx.ErrMissionListFailed, err.Error(), nil)
	}

	data := make([]responsedto.MissionTypeResource, 0, len(rows))
	for i := range rows {
		data = append(data, missionTypeResource(&rows[i]))
	}
	return response.Write(c, fiber.StatusOK, data)
}

func missionListFilterFromQuery(c *fiber.Ctx) (mission.ListFilter, *jsonapi.ErrorObject) {
	q := func(keys ...string) string {
		for _, key := range keys {
			if value := strings.TrimSpace(c.Query(key)); value != "" {
				return value
			}
		}
		return ""
	}

	filter := mission.ListFilter{
		Search: q("filter[search]", "search"),
		Sort:   strings.TrimSpace(c.Query("sort")),
	}

	if flightRaw := q("flight_id"); flightRaw != "" {
		flightID, err := uuidv7.ParseString(flightRaw)
		if err != nil {
			return filter, &jsonapi.ErrorObject{
				Status: "422",
				Title:  "Validation error",
				Detail: "flight_id is invalid UUID",
				Source: &jsonapi.ErrorSource{Pointer: "/query/flight_id"},
			}
		}
		filter.FlightID = flightID
	}

	if dateFromRaw := q("date_from", "start_date", "from"); dateFromRaw != "" {
		dateFrom, err := time.Parse("2006-01-02", dateFromRaw)
		if err != nil {
			return filter, &jsonapi.ErrorObject{
				Status: "422",
				Title:  "Validation error",
				Detail: "date_from must be YYYY-MM-DD",
				Source: &jsonapi.ErrorSource{Pointer: "/query/date_from"},
			}
		}
		filter.StartDate = &dateFrom
	}

	if dateToRaw := q("date_to", "end_date", "to"); dateToRaw != "" {
		dateTo, err := time.Parse("2006-01-02", dateToRaw)
		if err != nil {
			return filter, &jsonapi.ErrorObject{
				Status: "422",
				Title:  "Validation error",
				Detail: "date_to must be YYYY-MM-DD",
				Source: &jsonapi.ErrorSource{Pointer: "/query/date_to"},
			}
		}
		filter.EndDate = &dateTo
	}

	if filter.StartDate != nil && filter.EndDate != nil && filter.EndDate.Before(*filter.StartDate) {
		return filter, &jsonapi.ErrorObject{
			Status: "422",
			Title:  "Validation error",
			Detail: "date_to must be >= date_from",
			Source: &jsonapi.ErrorSource{Pointer: "/query/date_to"},
		}
	}

	filter.MissionType = strings.ToUpper(strings.TrimSpace(q("mission_type", "type")))
	switch filter.MissionType {
	case "", sharedconst.MissionTypeHEMS, sharedconst.MissionTypeCAT, sharedconst.MissionTypeSPO, sharedconst.MissionTypeNCO:
	default:
		return filter, &jsonapi.ErrorObject{
			Status: "422",
			Title:  "Validation error",
			Detail: "mission_type must be one of: HEMS, CAT, SPO, NCO",
			Source: &jsonapi.ErrorSource{Pointer: "/query/mission_type"},
		}
	}

	if pilotRaw := q("pilot_id"); pilotRaw != "" {
		pilotID, err := uuidv7.ParseString(pilotRaw)
		if err != nil {
			return filter, &jsonapi.ErrorObject{
				Status: "422",
				Title:  "Validation error",
				Detail: "pilot_id is invalid UUID",
				Source: &jsonapi.ErrorSource{Pointer: "/query/pilot_id"},
			}
		}
		filter.PilotID = pilotID
	}

	if heliRaw := q("helicopter_id"); heliRaw != "" {
		helicopterID, err := uuidv7.ParseString(heliRaw)
		if err != nil {
			return filter, &jsonapi.ErrorObject{
				Status: "422",
				Title:  "Validation error",
				Detail: "helicopter_id is invalid UUID",
				Source: &jsonapi.ErrorSource{Pointer: "/query/helicopter_id"},
			}
		}
		filter.HelicopterID = helicopterID
	}

	return filter, nil
}

func (h *MissionHandler) missionDocument(ctx context.Context, row *mission.Mission) (responsedto.MissionResponse, error) {
	resource, err := h.missionResource(ctx, row)
	if err != nil {
		return responsedto.MissionResponse{}, err
	}
	return responsedto.MissionResponse{Data: resource}, nil
}

func (h *MissionHandler) missionDocuments(ctx context.Context, rows []mission.Mission) (responsedto.MissionListResponse, error) {
	data := make([]responsedto.MissionResource, 0, len(rows))
	for i := range rows {
		resource, err := h.missionResource(ctx, &rows[i])
		if err != nil {
			return responsedto.MissionListResponse{}, err
		}
		data = append(data, resource)
	}
	return responsedto.MissionListResponse{Data: data}, nil
}

func (h *MissionHandler) missionResource(ctx context.Context, row *mission.Mission) (responsedto.MissionResource, error) {
	missionType := strings.ToUpper(strings.TrimSpace(row.Type))
	flightDataRows, flightDataResources, err := h.missionFlightDataResources(ctx, row)
	if err != nil {
		return responsedto.MissionResource{}, err
	}
	flightDataStatus := mission.StatusDraft
	if len(flightDataRows) > 0 {
		flightDataStatus = mission.StatusInProgress
		allComplete := true
		for i := range flightDataRows {
			if !flightdata.IsComplete(&flightDataRows[i]) {
				allComplete = false
				break
			}
		}
		if allComplete {
			flightDataStatus = mission.StatusCompleted
		}
	}
	formStates := mission.BuildForms(missionType, mission.FormFacts{
		mission.FormKeyFlightData: flightDataStatus,
	})

	formData := &responsedto.MissionForms{}
	if len(flightDataResources) > 0 {
		formData.FlightData = flightDataResources
	}

	return responsedto.MissionResource{
		Type: sharedconst.MissionResourceType,
		ID:   idString(row.ID),
		Attributes: responsedto.MissionAttributes{
			Flight:      missionFlightRef(row),
			Type:        missionType,
			MissionCode: strings.TrimSpace(row.Code),
			Subtypes:    missionSubtypeRef(row),
			Note:        strings.TrimSpace(row.Note),
			StartTime:   normalizeMissionClockDisplay(row.StartTime),
			EndTime:     normalizeMissionClockDisplay(row.EndTime),
			Status:      mission.ComputeStatus(formStates),
			Forms:       formData,
			Files:       h.missionFiles(ctx, row),
			CreatedAt:   row.CreatedAt.UTC().Format(time.RFC3339),
			CreatedBy:   uuidStringOrEmpty(row.CreatedBy),
			UpdatedAt:   row.UpdatedAt.UTC().Format(time.RFC3339),
			UpdatedBy:   uuidStringOrEmpty(row.UpdatedBy),
		},
	}, nil
}

func (h *MissionHandler) missionFiles(ctx context.Context, row *mission.Mission) []responsedto.MissionFileItem {
	if row == nil || len(row.Files) == 0 {
		return nil
	}
	files := make([]responsedto.MissionFileItem, 0, len(row.Files))
	for i := range row.Files {
		f := &row.Files[i]
		item := responsedto.MissionFileItem{
			ID:           idString(f.FileAttachmentID),
			AttachmentID: idString(f.FileAttachmentID),
		}
		if f.FileAttachment != nil && f.FileAttachment.File != nil {
			// Expose the underlying file uuid as the item id so it matches the
			// `file_id` carried by `file.updated` SSE events — letting the
			// frontend resolve per-file processing readiness in real time.
			item.ID = idString(f.FileAttachment.File.ID)
			item.FileName = strings.TrimSpace(f.FileAttachment.File.Name)
			urls := resolveFileManagerDownloadURLs(ctx, h.storage, f.FileAttachment.File)
			item.DownloadURL = urls.OriginalURL
			item.ThumbnailURL = urls.ThumbnailURL
		}
		files = append(files, item)
	}
	return files
}

func missionSubtypeRef(row *mission.Mission) *responsedto.MissionSubtypeRef {
	if row == nil || len(row.SubtypeID) == 0 {
		return nil
	}
	subtype := ""
	subtypeName := ""
	if row.MissionSubCategory != nil {
		subtype = row.MissionSubCategory.SubCodeType
		subtypeName = row.MissionSubCategory.SubTypeName
	}
	return &responsedto.MissionSubtypeRef{
		ID:          idString(row.SubtypeID),
		Subtype:     strings.TrimSpace(subtype),
		SubtypeName: strings.TrimSpace(subtypeName),
	}
}

func normalizeMissionClockDisplay(raw string) string {
	trimmed := strings.TrimSpace(raw)
	if trimmed == "" {
		return ""
	}
	if len(trimmed) >= 5 {
		return trimmed[:5]
	}
	return trimmed
}

func missionFlightRef(row *mission.Mission) *responsedto.MissionFlightRef {
	if row == nil {
		return nil
	}
	if row.Flight == nil && len(row.FlightID) == 0 {
		return nil
	}
	out := &responsedto.MissionFlightRef{ID: idString(row.FlightID)}
	if row.Flight != nil {
		out.ID = idString(row.Flight.ID)
		out.Code = strings.TrimSpace(stringPtrOrEmpty(row.Flight.MissionCode))
		if !row.Flight.Date.IsZero() {
			out.Date = row.Flight.Date.UTC().Format("2006-01-02")
		}
	}
	return out
}

func missionTypeResource(row *mission.MissionCategory) responsedto.MissionTypeResource {
	subtypes := make([]responsedto.MissionTypeSubtypeItem, 0, len(row.SubCategory))
	for i := range row.SubCategory {
		subtypes = append(subtypes, responsedto.MissionTypeSubtypeItem{
			ID:       idString(row.SubCategory[i].ID),
			Code:     strings.TrimSpace(row.SubCategory[i].SubCodeType),
			Name:     strings.TrimSpace(row.SubCategory[i].SubTypeName),
			TaskName: strings.TrimSpace(row.SubCategory[i].TaskName),
		})
	}
	return responsedto.MissionTypeResource{
		Type: "mission_type",
		ID:   idString(row.ID),
		Attributes: responsedto.MissionTypeAttributes{
			Code:     strings.TrimSpace(row.CodeType),
			Name:     strings.TrimSpace(row.TypeName),
			Subtypes: subtypes,
		},
	}
}

func (h *MissionHandler) missionFlightDataResources(ctx context.Context, row *mission.Mission) ([]flightdata.FlightData, []responsedto.FlightDataResource, error) {
	if h == nil || h.flightDataSvc == nil || row == nil || len(row.ID) != 16 {
		return make([]flightdata.FlightData, 0), make([]responsedto.FlightDataResource, 0), nil
	}

	rows, err := h.flightDataSvc.ListByMissionID(ctx, row.ID)
	if err != nil {
		return nil, nil, err
	}
	if len(rows) == 0 {
		return make([]flightdata.FlightData, 0), make([]responsedto.FlightDataResource, 0), nil
	}
	flightDataResources := make([]responsedto.FlightDataResource, 0, len(rows))
	for i := range rows {
		resource, err := buildFlightDataResourceWithService(ctx, h.flightDataSvc, &rows[i])
		if err != nil {
			return nil, nil, err
		}
		flightDataResources = append(flightDataResources, resource)
	}
	return rows, flightDataResources, nil
}

func writeMissionError(c *fiber.Ctx, def apperrorsx.Def, detail string, source *jsonapi.ErrorSource) error {
	return response.WriteErrors(c, def.Status, []jsonapi.ErrorObject{{
		Code:   def.Code,
		Detail: detail,
		Source: source,
	}})
}

func writeMissionValidationErrors(c *fiber.Ctx, errs []jsonapi.ErrorObject) error {
	for i := range errs {
		if errs[i].Code == "" {
			errs[i].Code = apperrorsx.CodeMissionInvalidPayload
		}
	}
	return response.WriteErrors(c, fiber.StatusUnprocessableEntity, errs)
}

func writeMissionServiceError(c *fiber.Ctx, err error, pointer string) error {
	switch {
	case errors.Is(err, mission.ErrTypeInvalid):
		return writeMissionError(c, apperrorsx.ErrMissionTypeInvalid, mission.ErrorMessageTypeInvalid, &jsonapi.ErrorSource{Pointer: "/data/attributes/type"})
	case errors.Is(err, mission.ErrFlightIDRequired):
		return writeMissionError(c, apperrorsx.ErrMissionFlightIDRequired, mission.ErrorMessageFlightIDRequired, &jsonapi.ErrorSource{Pointer: pointer})
	case errors.Is(err, mission.ErrMissionIDRequired):
		return writeMissionError(c, apperrorsx.ErrMissionMissionIDRequired, mission.ErrorMessageMissionIDRequired, &jsonapi.ErrorSource{Pointer: pointer})
	case errors.Is(err, mission.ErrSubtypeRequired):
		return writeMissionError(c, apperrorsx.ErrMissionSubtypeRequired, mission.ErrorMessageSubtypeRequired, &jsonapi.ErrorSource{Pointer: "/data/attributes/subtype_id"})
	case errors.Is(err, mission.ErrSubtypeInvalid):
		return writeMissionError(c, apperrorsx.ErrMissionSubtypeInvalid, mission.ErrorMessageSubtypeInvalid, &jsonapi.ErrorSource{Pointer: "/data/attributes/subtype_id"})
	case errors.Is(err, mission.ErrSubtypeForbidden):
		return writeMissionError(c, apperrorsx.ErrMissionSubtypeForbidden, mission.ErrorMessageSubtypeForbidden, &jsonapi.ErrorSource{Pointer: "/data/attributes/subtype_id"})
	case errors.Is(err, mission.ErrRequired):
		return writeMissionError(c, apperrorsx.ErrMissionCreateFailed, mission.ErrorMessageRequired, nil)
	case errors.Is(err, mission.ErrDuplicateFlight):
		return writeMissionError(c, apperrorsx.ErrMissionDuplicateFlight, mission.ErrorMessageDuplicateFlight, &jsonapi.ErrorSource{Pointer: "/data/attributes/flight_id"})
	case errors.Is(err, mission.ErrLockedAfterFlightInspection):
		return writeMissionError(c, apperrorsx.ErrMissionLockedAfterFlightInspection, mission.ErrorMessageLockedAfterFlightInspection, &jsonapi.ErrorSource{Pointer: pointer})
	case errors.Is(err, mission.ErrScheduleMissing):
		return writeMissionError(c, apperrorsx.ErrMissionInvalidPayload, mission.ErrorMessageScheduleMissing, &jsonapi.ErrorSource{Pointer: pointer})
	case errors.Is(err, mission.ErrBaseMissing):
		return writeMissionError(c, apperrorsx.ErrMissionInvalidPayload, mission.ErrorMessageBaseMissing, &jsonapi.ErrorSource{Pointer: pointer})
	case errors.Is(err, mission.ErrTwilightMissing):
		return writeMissionError(c, apperrorsx.ErrMissionInvalidPayload, mission.ErrorMessageTwilightMissing, &jsonapi.ErrorSource{Pointer: pointer})
	case errors.Is(err, mission.ErrCoordinatesInvalid):
		return writeMissionError(c, apperrorsx.ErrMissionInvalidPayload, mission.ErrorMessageCoordinatesInvalid, &jsonapi.ErrorSource{Pointer: pointer})
	case errors.Is(err, gorm.ErrRecordNotFound):
		return writeMissionError(c, apperrorsx.ErrMissionNotFound, mission.ErrorMessageNotFound, &jsonapi.ErrorSource{Pointer: pointer})
	default:
		return writeMissionError(c, apperrorsx.ErrMissionDeleteFailed, err.Error(), nil)
	}
}
