package handlers

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

	"github.com/gofiber/fiber/v2"

	authdomain "wucher/internal/domain/auth"
	"wucher/internal/domain/base"
	"wucher/internal/domain/dul"
	filemanager "wucher/internal/domain/file_manager"
	"wucher/internal/domain/helicopter"
	helicopterfile "wucher/internal/domain/helicopter_file"
	missiondomain "wucher/internal/domain/mission"
	takeoverdomain "wucher/internal/domain/takeover"
	"wucher/internal/domain/user"
	"wucher/internal/service"
	"wucher/internal/shared/pkg/appctx"
	"wucher/internal/shared/pkg/apperrorsx"
	"wucher/internal/shared/pkg/uuidv7"
	dto "wucher/internal/transport/http/dto"
	requestdto "wucher/internal/transport/http/dto/request"
	responsedto "wucher/internal/transport/http/dto/response"
	shareddto "wucher/internal/transport/http/dto/shared"
	"wucher/internal/transport/http/jsonapi"
	"wucher/internal/transport/http/response"
	"wucher/internal/transport/http/validators"
)

type FileManagerFileHandler struct {
	svc                  filemanager.Service
	wopi                 *service.WOPIService
	heliFile             helicopterfile.Service
	aircraft             helicopter.Service
	base                 base.Service
	dul                  dul.Service
	takeover             takeoverFileService
	mission              missionFileService
	user                 user.Service
	fleetStatusFile      *service.FleetStatusFileService
	helicopterFileEvents helicopterFileEventPublisher
	auth                 fileManagerAuthService
	validate             *validators.Validator
	storage              filemanager.ObjectStorage
}

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

type takeoverFileService interface {
	GetByID(context.Context, []byte) (*takeoverdomain.TakeoverAc, error)
	List(context.Context, int, int) ([]takeoverdomain.TakeoverAc, int64, error)
	AttachFile(context.Context, []byte, []byte) error
}

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

const aircraftImageFolderName = "aircraft"
const baseImageFolderName = "bases"
const dulImageFolderName = "duls"
const takeoverFileFolderName = "takeovers"
const missionFileFolderName = "missions"
const userImageFolderName = "user"

var (
	_ = dto.ErrorResponse{}
	_ = dto.GenericDeleteResponse{}
)

func NewFileManagerFileHandler(svc filemanager.Service, storage filemanager.ObjectStorage) *FileManagerFileHandler {
	return &FileManagerFileHandler{
		svc:      svc,
		validate: validators.New(),
		storage:  storage,
	}
}

func (h *FileManagerFileHandler) WithWOPIService(wopiSvc *service.WOPIService) *FileManagerFileHandler {
	h.wopi = wopiSvc
	return h
}

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

func (h *FileManagerFileHandler) WithAircraftService(aircraftSvc helicopter.Service) *FileManagerFileHandler {
	h.aircraft = aircraftSvc
	return h
}

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

func (h *FileManagerFileHandler) WithDULService(dulSvc dul.Service) *FileManagerFileHandler {
	h.dul = dulSvc
	return h
}

func (h *FileManagerFileHandler) WithTakeoverService(takeoverSvc takeoverFileService) *FileManagerFileHandler {
	h.takeover = takeoverSvc
	return h
}

func (h *FileManagerFileHandler) WithMissionService(missionSvc missionFileService) *FileManagerFileHandler {
	h.mission = missionSvc
	return h
}

func (h *FileManagerFileHandler) WithAuthService(authSvc fileManagerAuthService) *FileManagerFileHandler {
	h.auth = authSvc
	return h
}

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

func (h *FileManagerFileHandler) takeoverUserHasGlobalAccessForUserID(ctx context.Context, userID []byte) bool {
	if h == nil || h.auth == nil {
		return false
	}
	if len(userID) != 16 {
		return false
	}
	user, err := h.auth.GetUserByID(ctx, userID)
	if err != nil || user == nil {
		return false
	}
	for i := range user.Roles {
		if strings.EqualFold(strings.TrimSpace(user.Roles[i].Name), "admin") {
			return true
		}
	}
	return false
}

func (h *FileManagerFileHandler) WithUserService(userSvc user.Service) *FileManagerFileHandler {
	h.user = userSvc
	return h
}

func (h *FileManagerFileHandler) WithFleetStatusFileService(svc *service.FleetStatusFileService) *FileManagerFileHandler {
	h.fleetStatusFile = svc
	return h
}

func (h *FileManagerFileHandler) WithHelicopterFileEvents(publisher helicopterFileEventPublisher) *FileManagerFileHandler {
	h.helicopterFileEvents = publisher
	return h
}

// CreateFile godoc
// @Summary Create file record from uploaded object
// @ID FileManagerCreateFileFromUploadedObject
// @Description Final public control-plane step after direct PUT upload to S3 (bulk contract).
// @Description Verifies uploaded object per item from upload intent, then persists metadata+outbox durably and emits lifecycle status.
// @Description This endpoint does not upload binary data.
// @Description Public flow: POST /files/upload (intent+presign) -> PUT to S3 -> POST /files/create.
// @Description folder_id is optional; null/omitted means create in root folder.
// @Description Canonical JSON:API data.type is "file_manager_file_create" (legacy "file_manager_file" remains accepted for backward compatibility).
// @Description Bulk payload accepts one or many items.
// @Tags File Manager
// @Accept json
// @Produce json
// @Param request body requestdto.FileManagerFileUserBulkCreateRequest true "Bulk create payload for user profile image"
// @Success 200 {object} responsedto.FileManagerFileBulkResponse
// @Success 207 {object} responsedto.FileManagerFileBulkResponse
// @Success 201 {object} responsedto.FileManagerFileBulkResponse
// @Failure 422 {object} dto.ErrorResponse
// @Failure 400 {object} dto.ErrorResponse
// @Router /api/v1/file-manager/files/create [post]
func (h *FileManagerFileHandler) CreateFile(c *fiber.Ctx) error {
	isBulk, err := isJSONAPIDataArrayPayload(c.Body())
	if err != nil {
		return writeFileManagerError(c, fiber.StatusBadRequest, "Invalid JSON", err.Error(), nil)
	}
	if !isBulk {
		return writeFileManagerError(c, fiber.StatusUnprocessableEntity, "Validation error", "data must be an array", &jsonapi.ErrorSource{Pointer: "/data"})
	}
	return h.createFilesBulk(c)
}

// UploadFile godoc
// @Summary Request upload intents + presigned PUT URLs
// @ID FileManagerRequestUploadIntent
// @Description Control-plane endpoint to create upload intents and presigned PUT URLs for direct upload to S3 (bulk contract).
// @Description Binary does not flow through backend upload endpoint.
// @Description After successful PUT to S3, call POST /api/v1/file-manager/files/create with upload_intent_uuid.
// @Description Presigned URL TTL is controlled by backend storage config.
// @Description Bulk payload accepts one or many items. Response is item-based and supports partial success.
// @Tags File Manager
// @Accept json
// @Produce json
// @Param request body requestdto.FileManagerFileUploadIntentBulkRequest true "Bulk upload intent request"
// @Success 200 {object} responsedto.FileManagerFileUploadIntentBulkResponse
// @Success 207 {object} responsedto.FileManagerFileUploadIntentBulkResponse
// @Success 201 {object} responsedto.FileManagerFileUploadIntentBulkResponse
// @Failure 422 {object} dto.ErrorResponse
// @Failure 400 {object} dto.ErrorResponse
// @Router /api/v1/file-manager/files/upload [post]
func (h *FileManagerFileHandler) UploadFile(c *fiber.Ctx) error {
	isBulk, err := isJSONAPIDataArrayPayload(c.Body())
	if err != nil {
		return writeFileManagerError(c, fiber.StatusBadRequest, "Invalid JSON", err.Error(), nil)
	}
	if !isBulk {
		return writeFileManagerError(c, fiber.StatusUnprocessableEntity, "Validation error", "data must be an array", &jsonapi.ErrorSource{Pointer: "/data"})
	}

	var req requestdto.FileManagerFileUploadIntentBulkRequest
	if err := c.BodyParser(&req); err != nil {
		return writeFileManagerError(c, fiber.StatusBadRequest, "Invalid JSON", err.Error(), nil)
	}
	if len(req.Data) == 0 {
		return writeFileManagerError(c, fiber.StatusUnprocessableEntity, "Validation error", "data must contain at least one item", &jsonapi.ErrorSource{Pointer: "/data"})
	}

	actorID := actorUserID(c)
	items := make([]responsedto.FileManagerFileUploadIntentBulkItem, len(req.Data))

	for i := range req.Data {
		item := req.Data[i]
		typ := strings.TrimSpace(item.Type)
		if typ != "file_manager_file_upload_intent" {
			items[i] = fileManagerUploadIntentBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "type must be file_manager_file_upload_intent")
			continue
		}

		name := strings.TrimSpace(item.Attributes.Name)
		if name == "" {
			items[i] = fileManagerUploadIntentBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "name is required")
			continue
		}

		contentType := strings.TrimSpace(item.Attributes.ContentType)
		if contentType == "" {
			items[i] = fileManagerUploadIntentBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "content_type is required")
			continue
		}

		if item.Attributes.SizeBytes < 0 {
			items[i] = fileManagerUploadIntentBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "size_bytes must be greater than or equal to 0")
			continue
		}

		grant, grantErr := h.svc.RequestFileUploadIntent(c.Context(), filemanager.RequestFileUploadIntentParams{
			Name:        name,
			ContentType: contentType,
			SizeBytes:   item.Attributes.SizeBytes,
			ActorID:     actorID,
		})
		if grantErr != nil {
			items[i] = fileManagerUploadIntentBulkActionErrorItem(i, "Upload intent failed", grantErr)
			continue
		}

		resource := fileManagerFileUploadIntentResource(grant)
		items[i] = responsedto.FileManagerFileUploadIntentBulkItem{
			Index:    i,
			Success:  true,
			Resource: &resource,
		}
	}

	return writeFileManagerUploadIntentBulkResponse(c, items)
}

// UploadTakeoverFile godoc
// @Summary Request upload intents for takeover files
// @Description Dedicated upload-intent endpoint for takeover file flow.
// @Description Use together with /api/v1/file-manager/files/create-takeover-file to persist the takeover file relation.
// @Tags File Manager
// @Accept json
// @Produce json
// @Param request body requestdto.FileManagerFileUploadIntentBulkRequest true "Bulk upload intent request"
// @Success 200 {object} responsedto.FileManagerFileUploadIntentBulkResponse
// @Success 207 {object} responsedto.FileManagerFileUploadIntentBulkResponse
// @Success 201 {object} responsedto.FileManagerFileUploadIntentBulkResponse
// @Failure 422 {object} dto.ErrorResponse
// @Failure 400 {object} dto.ErrorResponse
// @Router /api/v1/file-manager/files/upload-takeover-file [post]
func (h *FileManagerFileHandler) UploadTakeoverFile(c *fiber.Ctx) error {
	return h.UploadFile(c)
}

// UploadMissionFile godoc
// @Summary Request upload intents for mission files
// @ID FileManagerRequestMissionFileUploadIntent
// @Description Dedicated upload-intent endpoint for mission file flow.
// @Description Use together with /api/v1/file-manager/files/create-mission-file to attach the file to a mission.
// @Tags File Manager
// @Accept json
// @Produce json
// @Param request body requestdto.FileManagerFileUploadIntentBulkRequest true "Bulk upload intent request"
// @Success 200 {object} responsedto.FileManagerFileUploadIntentBulkResponse
// @Success 207 {object} responsedto.FileManagerFileUploadIntentBulkResponse
// @Success 201 {object} responsedto.FileManagerFileUploadIntentBulkResponse
// @Failure 422 {object} dto.ErrorResponse
// @Failure 400 {object} dto.ErrorResponse
// @Router /api/v1/file-manager/files/upload-mission-file [post]
func (h *FileManagerFileHandler) UploadMissionFile(c *fiber.Ctx) error {
	return h.UploadFile(c)
}

// CreateMissionFile godoc
// @Summary Create mission file record from uploaded object
// @ID FileManagerCreateMissionFileFromUploadedObject
// @Description Materializes uploaded objects into folder "missions/<mission-uuid>", creates attachments (ref_type=mission), and links them to the mission so they appear in GET mission.
// @Description mission_uuid is required per item.
// @Description Possible error code: MISSION_FILE_INVALID_JSON, MISSION_FILE_VALIDATION, MISSION_FILE_INVALID_MISSION_UUID, MISSION_FILE_MISSION_NOT_FOUND, MISSION_FILE_SERVICE_UNAVAILABLE, MISSION_FILE_ACTION_FAILED, FILE_MANAGER_UPLOAD_INTENT_NOT_FOUND, FILE_MANAGER_STORAGE_UNAVAILABLE, FILE_MANAGER_ACTION_FAILED.
// @Tags File Manager
// @Accept json
// @Produce json
// @Param request body requestdto.FileManagerFileMissionBulkCreateRequest true "Bulk create payload for mission file"
// @Success 200 {object} responsedto.FileManagerFileBulkResponse
// @Success 207 {object} responsedto.FileManagerFileBulkResponse
// @Success 201 {object} responsedto.FileManagerFileBulkResponse
// @Failure 422 {object} dto.ErrorResponse
// @Failure 400 {object} dto.ErrorResponse
// @Router /api/v1/file-manager/files/create-mission-file [post]
func (h *FileManagerFileHandler) CreateMissionFile(c *fiber.Ctx) error {
	isBulk, err := isJSONAPIDataArrayPayload(c.Body())
	if err != nil {
		return writeMissionFileError(c, apperrorsx.ErrMissionFileInvalidJSON, "mission file request body is not valid JSON", nil)
	}
	if !isBulk {
		return writeMissionFileError(c, apperrorsx.ErrMissionFileValidation, "data must be an array", &jsonapi.ErrorSource{Pointer: "/data"})
	}

	var req requestdto.FileManagerFileMissionBulkCreateRequest
	if err := c.BodyParser(&req); err != nil {
		return writeMissionFileError(c, apperrorsx.ErrMissionFileInvalidJSON, "mission file request body is not valid JSON", nil)
	}
	if len(req.Data) == 0 {
		return writeMissionFileError(c, apperrorsx.ErrMissionFileValidation, "data must contain at least one item", &jsonapi.ErrorSource{Pointer: "/data"})
	}

	actorID := actorUserID(c)
	if h.mission == nil {
		return writeMissionFileError(c, apperrorsx.ErrMissionFileServiceUnavailable, "mission service is not configured", nil)
	}

	items := make([]responsedto.FileManagerFileBulkItem, len(req.Data))
	validParams := make([]filemanager.CreateFileFromUploadIntentParams, 0, len(req.Data))
	validIndexes := make([]int, 0, len(req.Data))

	for i := range req.Data {
		item := req.Data[i]
		typ := strings.TrimSpace(item.Type)
		if typ != "file_manager_file_create" && typ != "file_manager_file" {
			items[i] = missionFileBulkErrorItem(i, apperrorsx.ErrMissionFileValidation, "type must be file_manager_file_create or file_manager_file")
			continue
		}

		uploadIntentUUID := strings.TrimSpace(item.Attributes.UploadIntentUUID)
		uploadIntentID, parseUploadErr := uuidv7.ParseString(uploadIntentUUID)
		if parseUploadErr != nil {
			items[i] = missionFileBulkErrorItem(i, apperrorsx.ErrMissionFileValidation, "upload_intent_uuid is invalid UUID")
			continue
		}

		name := strings.TrimSpace(item.Attributes.Name)
		if name == "" {
			items[i] = missionFileBulkErrorItem(i, apperrorsx.ErrMissionFileValidation, "name is required")
			continue
		}

		missionUUID := strings.TrimSpace(item.Attributes.MissionUUID)
		missionID, parseMissionErr := uuidv7.ParseString(missionUUID)
		if parseMissionErr != nil {
			items[i] = missionFileBulkErrorItem(i, apperrorsx.ErrMissionFileInvalidMissionUUID, "mission_uuid is invalid UUID")
			continue
		}

		missionRow, missionErr := h.mission.GetByID(c.UserContext(), missionID)
		if missionErr != nil {
			items[i] = missionFileBulkErrorItem(i, apperrorsx.ErrMissionFileActionFailed, "failed to load mission")
			continue
		}
		if missionRow == nil {
			items[i] = missionFileBulkErrorItem(i, apperrorsx.ErrMissionFileMissionNotFound, "mission not found")
			continue
		}

		folderID, folderErr := h.ensureMissionFileFolder(c.UserContext(), actorID, missionID)
		if folderErr != nil {
			items[i] = fileManagerBulkActionErrorItem(i, "Create failed", folderErr)
			continue
		}

		validParams = append(validParams, filemanager.CreateFileFromUploadIntentParams{
			UploadIntentID: uploadIntentID,
			FolderID:       folderID,
			FolderProvided: true,
			Name:           name,
			ActorID:        actorID,
		})
		validIndexes = append(validIndexes, i)
	}

	for i := range validParams {
		idx := validIndexes[i]
		created, createErr := h.svc.CreateFileNodeFromUploadIntent(c.UserContext(), validParams[i])
		if createErr != nil {
			items[idx] = fileManagerBulkActionErrorItem(idx, "Create failed", createErr)
			continue
		}
		missionUUID := strings.TrimSpace(req.Data[idx].Attributes.MissionUUID)
		missionID, parseMissionErr := uuidv7.ParseString(missionUUID)
		if parseMissionErr != nil {
			items[idx] = missionFileBulkErrorItem(idx, apperrorsx.ErrMissionFileInvalidMissionUUID, "mission_uuid is invalid UUID")
			continue
		}
		attachment, attachErr := h.svc.CreateAttachment(c.UserContext(), filemanager.CreateAttachmentParams{
			FileID:    created.ID,
			RefType:   "mission",
			RefID:     missionUUID,
			SortOrder: 0,
			IsPrimary: false,
			ActorID:   actorID,
		})
		if attachErr != nil {
			items[idx] = fileManagerBulkActionErrorItem(idx, "Create failed", attachErr)
			continue
		}
		if err := h.mission.AttachFile(c.UserContext(), missionID, attachment.ID); err != nil {
			items[idx] = fileManagerBulkActionErrorItem(idx, "Create failed", err)
			continue
		}
		items[idx] = fileManagerBulkSuccessItem(idx, "created", created)
	}

	return writeFileManagerBulkResponse(c, items)
}

func (h *FileManagerFileHandler) ensureMissionFileFolder(ctx context.Context, actorID []byte, missionID []byte) ([]byte, error) {
	return ensureMissionFileFolderPath(ctx, h.svc, missionID, actorID)
}

// UploadAircraftImage godoc
// @Summary Request upload intents for aircraft drag-drop images
// @ID FileManagerRequestAircraftImageUploadIntent
// @Description Same flow as file upload intent, but dedicated for aircraft image drag-drop flow.
// @Description Use together with /api/v1/file-manager/files/create-aircraft-image to force target folder "aircraft".
// @Tags File Manager
// @Accept json
// @Produce json
// @Param request body requestdto.FileManagerFileUploadIntentBulkRequest true "Bulk upload intent request"
// @Success 200 {object} responsedto.FileManagerFileUploadIntentBulkResponse
// @Success 207 {object} responsedto.FileManagerFileUploadIntentBulkResponse
// @Success 201 {object} responsedto.FileManagerFileUploadIntentBulkResponse
// @Failure 422 {object} dto.ErrorResponse
// @Failure 400 {object} dto.ErrorResponse
// @Router /api/v1/file-manager/files/upload-aircraft-image [post]
func (h *FileManagerFileHandler) UploadAircraftImage(c *fiber.Ctx) error {
	isBulk, err := isJSONAPIDataArrayPayload(c.Body())
	if err != nil {
		return writeFileManagerError(c, fiber.StatusBadRequest, "Invalid JSON", err.Error(), nil)
	}
	if !isBulk {
		return writeFileManagerError(c, fiber.StatusUnprocessableEntity, "Validation error", "data must be an array", &jsonapi.ErrorSource{Pointer: "/data"})
	}

	var req requestdto.FileManagerFileUploadIntentBulkRequest
	if err := c.BodyParser(&req); err != nil {
		return writeFileManagerError(c, fiber.StatusBadRequest, "Invalid JSON", err.Error(), nil)
	}
	if len(req.Data) == 0 {
		return writeFileManagerError(c, fiber.StatusUnprocessableEntity, "Validation error", "data must contain at least one item", &jsonapi.ErrorSource{Pointer: "/data"})
	}

	actorID := actorUserID(c)
	items := make([]responsedto.FileManagerFileUploadIntentBulkItem, len(req.Data))
	for i := range req.Data {
		item := req.Data[i]
		typ := strings.TrimSpace(item.Type)
		if typ != "file_manager_file_upload_intent" {
			items[i] = fileManagerUploadIntentBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "type must be file_manager_file_upload_intent")
			continue
		}

		name := strings.TrimSpace(item.Attributes.Name)
		if name == "" {
			items[i] = fileManagerUploadIntentBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "name is required")
			continue
		}

		contentType := strings.TrimSpace(item.Attributes.ContentType)
		if contentType == "" {
			items[i] = fileManagerUploadIntentBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "content_type is required")
			continue
		}
		if !strings.HasPrefix(strings.ToLower(contentType), "image/") {
			items[i] = fileManagerUploadIntentBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "content_type must be image/* for aircraft image upload")
			continue
		}

		if item.Attributes.SizeBytes < 0 {
			items[i] = fileManagerUploadIntentBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "size_bytes must be greater than or equal to 0")
			continue
		}

		grant, grantErr := h.svc.RequestFileUploadIntent(c.Context(), filemanager.RequestFileUploadIntentParams{
			Name:        name,
			ContentType: contentType,
			SizeBytes:   item.Attributes.SizeBytes,
			ActorID:     actorID,
		})
		if grantErr != nil {
			items[i] = fileManagerUploadIntentBulkActionErrorItem(i, "Upload intent failed", grantErr)
			continue
		}

		resource := fileManagerFileUploadIntentResource(grant)
		items[i] = responsedto.FileManagerFileUploadIntentBulkItem{
			Index:    i,
			Success:  true,
			Resource: &resource,
		}
	}

	return writeFileManagerUploadIntentBulkResponse(c, items)
}

// UploadBaseImage godoc
// @Summary Request upload intents for base images
// @ID FileManagerRequestBaseImageUploadIntent
// @Description Same flow as file upload intent, but dedicated for base image flow.
// @Description Use together with /api/v1/file-manager/files/create-base-image to force target folder under bases hierarchy.
// @Tags File Manager
// @Accept json
// @Produce json
// @Param request body requestdto.FileManagerFileUploadIntentBulkRequest true "Bulk upload intent request"
// @Success 200 {object} responsedto.FileManagerFileUploadIntentBulkResponse
// @Success 207 {object} responsedto.FileManagerFileUploadIntentBulkResponse
// @Success 201 {object} responsedto.FileManagerFileUploadIntentBulkResponse
// @Failure 422 {object} dto.ErrorResponse
// @Failure 400 {object} dto.ErrorResponse
// @Router /api/v1/file-manager/files/upload-base-image [post]
func (h *FileManagerFileHandler) UploadBaseImage(c *fiber.Ctx) error {
	isBulk, err := isJSONAPIDataArrayPayload(c.Body())
	if err != nil {
		return writeFileManagerError(c, fiber.StatusBadRequest, "Invalid JSON", err.Error(), nil)
	}
	if !isBulk {
		return writeFileManagerError(c, fiber.StatusUnprocessableEntity, "Validation error", "data must be an array", &jsonapi.ErrorSource{Pointer: "/data"})
	}

	var req requestdto.FileManagerFileUploadIntentBulkRequest
	if err := c.BodyParser(&req); err != nil {
		return writeFileManagerError(c, fiber.StatusBadRequest, "Invalid JSON", err.Error(), nil)
	}
	if len(req.Data) == 0 {
		return writeFileManagerError(c, fiber.StatusUnprocessableEntity, "Validation error", "data must contain at least one item", &jsonapi.ErrorSource{Pointer: "/data"})
	}

	actorID := actorUserID(c)
	items := make([]responsedto.FileManagerFileUploadIntentBulkItem, len(req.Data))
	for i := range req.Data {
		item := req.Data[i]
		typ := strings.TrimSpace(item.Type)
		if typ != "file_manager_file_upload_intent" {
			items[i] = fileManagerUploadIntentBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "type must be file_manager_file_upload_intent")
			continue
		}

		name := strings.TrimSpace(item.Attributes.Name)
		if name == "" {
			items[i] = fileManagerUploadIntentBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "name is required")
			continue
		}

		contentType := strings.TrimSpace(item.Attributes.ContentType)
		if contentType == "" {
			items[i] = fileManagerUploadIntentBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "content_type is required")
			continue
		}
		if !strings.HasPrefix(strings.ToLower(contentType), "image/") {
			items[i] = fileManagerUploadIntentBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "content_type must be image/* for base image upload")
			continue
		}

		if item.Attributes.SizeBytes < 0 {
			items[i] = fileManagerUploadIntentBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "size_bytes must be greater than or equal to 0")
			continue
		}

		grant, grantErr := h.svc.RequestFileUploadIntent(c.Context(), filemanager.RequestFileUploadIntentParams{
			Name:        name,
			ContentType: contentType,
			SizeBytes:   item.Attributes.SizeBytes,
			ActorID:     actorID,
		})
		if grantErr != nil {
			items[i] = fileManagerUploadIntentBulkActionErrorItem(i, "Upload intent failed", grantErr)
			continue
		}

		resource := fileManagerFileUploadIntentResource(grant)
		items[i] = responsedto.FileManagerFileUploadIntentBulkItem{
			Index:    i,
			Success:  true,
			Resource: &resource,
		}
	}

	return writeFileManagerUploadIntentBulkResponse(c, items)
}

// UploadDULImage godoc
// @Summary Request upload intents for DUL images
// @Description Dedicated upload-intent endpoint for DUL image flow.
// @Description Possible status: 201 (all success), 207 (partial success), 400 (invalid json), 401 (unauthorized), 403 (forbidden), 422 (validation), 503 (storage/queue/upload-intent service unavailable).
// @Description Possible error code: INVALID_REQUEST, USER_UNAUTHORIZED, FORBIDDEN_ACCESS, FILE_MANAGER_VALIDATION_FAILED, FILE_MANAGER_STORAGE_UNAVAILABLE, FILE_MANAGER_QUEUE_UNAVAILABLE, FILE_MANAGER_UPLOAD_INTENT_UNAVAILABLE, FILE_MANAGER_ACTION_FAILED.
// @Tags File Manager
// @Accept json
// @Produce json
// @Param request body requestdto.FileManagerFileUploadIntentBulkRequest true "Bulk upload intent request"
// @Success 200 {object} responsedto.FileManagerFileUploadIntentBulkResponse
// @Success 207 {object} responsedto.FileManagerFileUploadIntentBulkResponse
// @Success 201 {object} responsedto.FileManagerFileUploadIntentBulkResponse
// @Failure 401 {object} dto.ErrorResponse
// @Failure 403 {object} dto.ErrorResponse
// @Failure 422 {object} dto.ErrorResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 503 {object} dto.ErrorResponse
// @Router /api/v1/file-manager/files/upload-dul-image [post]
func (h *FileManagerFileHandler) UploadDULImage(c *fiber.Ctx) error {
	return h.UploadBaseImage(c)
}

// UploadUserImage godoc
// @Summary Request upload intents for user profile images
// @ID FileManagerRequestUserImageUploadIntent
// @Description Same flow as file upload intent, but dedicated for user profile image flow.
// @Description Use together with /api/v1/file-manager/files/create-user-image to force target folder under user hierarchy.
// @Tags File Manager
// @Accept json
// @Produce json
// @Param request body requestdto.FileManagerFileUploadIntentBulkRequest true "Bulk upload intent request"
// @Success 200 {object} responsedto.FileManagerFileUploadIntentBulkResponse
// @Success 207 {object} responsedto.FileManagerFileUploadIntentBulkResponse
// @Success 201 {object} responsedto.FileManagerFileUploadIntentBulkResponse
// @Failure 422 {object} dto.ErrorResponse
// @Failure 400 {object} dto.ErrorResponse
// @Router /api/v1/file-manager/files/upload-user-image [post]
func (h *FileManagerFileHandler) UploadUserImage(c *fiber.Ctx) error {
	isBulk, err := isJSONAPIDataArrayPayload(c.Body())
	if err != nil {
		return writeFileManagerError(c, fiber.StatusBadRequest, "Invalid JSON", err.Error(), nil)
	}
	if !isBulk {
		return writeFileManagerError(c, fiber.StatusUnprocessableEntity, "Validation error", "data must be an array", &jsonapi.ErrorSource{Pointer: "/data"})
	}

	var req requestdto.FileManagerFileUploadIntentBulkRequest
	if err := c.BodyParser(&req); err != nil {
		return writeFileManagerError(c, fiber.StatusBadRequest, "Invalid JSON", err.Error(), nil)
	}
	if len(req.Data) == 0 {
		return writeFileManagerError(c, fiber.StatusUnprocessableEntity, "Validation error", "data must contain at least one item", &jsonapi.ErrorSource{Pointer: "/data"})
	}

	actorID := actorUserID(c)
	items := make([]responsedto.FileManagerFileUploadIntentBulkItem, len(req.Data))
	for i := range req.Data {
		item := req.Data[i]
		if strings.TrimSpace(item.Type) != "file_manager_file_upload_intent" {
			items[i] = fileManagerUploadIntentBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "type must be file_manager_file_upload_intent")
			continue
		}
		name := strings.TrimSpace(item.Attributes.Name)
		if name == "" {
			items[i] = fileManagerUploadIntentBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "name is required")
			continue
		}
		contentType := strings.TrimSpace(item.Attributes.ContentType)
		if contentType == "" {
			items[i] = fileManagerUploadIntentBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "content_type is required")
			continue
		}
		if !strings.HasPrefix(strings.ToLower(contentType), "image/") {
			items[i] = fileManagerUploadIntentBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "content_type must be image/* for user image upload")
			continue
		}
		if item.Attributes.SizeBytes < 0 {
			items[i] = fileManagerUploadIntentBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "size_bytes must be greater than or equal to 0")
			continue
		}
		grant, grantErr := h.svc.RequestFileUploadIntent(c.Context(), filemanager.RequestFileUploadIntentParams{
			Name:        name,
			ContentType: contentType,
			SizeBytes:   item.Attributes.SizeBytes,
			ActorID:     actorID,
		})
		if grantErr != nil {
			items[i] = fileManagerUploadIntentBulkActionErrorItem(i, "Upload intent failed", grantErr)
			continue
		}
		resource := fileManagerFileUploadIntentResource(grant)
		items[i] = responsedto.FileManagerFileUploadIntentBulkItem{Index: i, Success: true, Resource: &resource}
	}
	return writeFileManagerUploadIntentBulkResponse(c, items)
}

// CancelUpload godoc
// @Summary Cancel upload intent (bulk)
// @ID FileManagerCancelUploadIntentBulk
// @Description Cancel pending upload intents before file entity creation.
// @Description This endpoint is intended for pre-create cleanup when FE uploaded to object storage but user cancels flow.
// @Description Bulk payload accepts one or many items.
// @Tags File Manager
// @Accept json
// @Produce json
// @Param request body requestdto.FileManagerFileCancelUploadBulkRequest true "Bulk cancel upload intent request"
// @Success 200 {object} responsedto.FileManagerFileCancelUploadBulkResponse
// @Failure 422 {object} dto.ErrorResponse
// @Failure 400 {object} dto.ErrorResponse
// @Router /api/v1/file-manager/files/cancel-upload [delete]
func (h *FileManagerFileHandler) CancelUpload(c *fiber.Ctx) error {
	isBulk, err := isJSONAPIDataArrayPayload(c.Body())
	if err != nil {
		return writeFileManagerError(c, fiber.StatusBadRequest, "Invalid JSON", err.Error(), nil)
	}
	if !isBulk {
		return writeFileManagerError(c, fiber.StatusUnprocessableEntity, "Validation error", "data must be an array", &jsonapi.ErrorSource{Pointer: "/data"})
	}

	var req requestdto.FileManagerFileCancelUploadBulkRequest
	if err := c.BodyParser(&req); err != nil {
		return writeFileManagerError(c, fiber.StatusBadRequest, "Invalid JSON", err.Error(), nil)
	}
	if len(req.Data) == 0 {
		return writeFileManagerError(c, fiber.StatusUnprocessableEntity, "Validation error", "data must contain at least one item", &jsonapi.ErrorSource{Pointer: "/data"})
	}

	items := make([]responsedto.FileManagerFileCancelUploadBulkItem, len(req.Data))
	for i := range req.Data {
		item := req.Data[i]
		if strings.TrimSpace(item.Type) != "file_manager_file_cancel_upload" {
			items[i] = fileManagerCancelUploadBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "type must be file_manager_file_cancel_upload")
			continue
		}

		uploadIntentID, err := uuidv7.ParseString(strings.TrimSpace(item.Attributes.UploadIntentUUID))
		if err != nil {
			items[i] = fileManagerCancelUploadBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "upload_intent_uuid is invalid UUID")
			continue
		}

		if err := h.svc.CancelFileUploadIntent(c.UserContext(), filemanager.CancelFileUploadIntentParams{
			UploadIntentID: uploadIntentID,
			ActorID:        actorUserID(c),
		}); err != nil {
			items[i] = fileManagerCancelUploadBulkActionErrorItem(i, "Cancel upload failed", err)
			continue
		}

		items[i] = fileManagerCancelUploadBulkSuccessItem(i, uploadIntentID)
	}

	return writeFileManagerCancelUploadBulkResponse(c, items)
}

// CompleteUploadFile completes a direct upload intent.
// Deprecated: this method is kept for internal compatibility and is not exposed in public routes.
func (h *FileManagerFileHandler) CompleteUploadFile(c *fiber.Ctx) error {
	var req requestdto.FileManagerFileUploadCompleteRequest
	if err := c.BodyParser(&req); err != nil {
		return writeFileManagerError(c, fiber.StatusBadRequest, "Invalid JSON", err.Error(), nil)
	}
	if errs := h.validate.ValidateStruct(req); errs != nil {
		return response.WriteErrors(c, fiber.StatusUnprocessableEntity, errs)
	}

	uploadIntentID, err := uuidv7.ParseString(strings.TrimSpace(req.Data.Attributes.UploadIntentUUID))
	if err != nil {
		return writeFileManagerError(c, fiber.StatusUnprocessableEntity, "Validation error", "upload_intent_uuid is invalid UUID", &jsonapi.ErrorSource{Pointer: "/data/attributes/upload_intent_uuid"})
	}

	uploaded, err := h.svc.CompleteFileUploadIntent(c.UserContext(), filemanager.CompleteFileUploadIntentParams{
		UploadIntentID: uploadIntentID,
		ActorID:        actorUserID(c),
	})
	if err != nil {
		return writeFileManagerActionError(c, "Complete upload failed", err)
	}
	return response.Write(c, fiber.StatusOK, fileManagerFileResource(uploaded))
}

func (h *FileManagerFileHandler) createFilesBulk(c *fiber.Ctx) error {
	var req requestdto.FileManagerFileBulkCreateRequest
	if err := c.BodyParser(&req); err != nil {
		return writeFileManagerError(c, fiber.StatusBadRequest, "Invalid JSON", err.Error(), nil)
	}
	if len(req.Data) == 0 {
		return writeFileManagerError(c, fiber.StatusUnprocessableEntity, "Validation error", "data must contain at least one item", &jsonapi.ErrorSource{Pointer: "/data"})
	}

	items := make([]responsedto.FileManagerFileBulkItem, len(req.Data))
	validParams := make([]filemanager.CreateFileFromUploadIntentParams, 0, len(req.Data))
	validIndexes := make([]int, 0, len(req.Data))

	for i := range req.Data {
		item := req.Data[i]
		typ := strings.TrimSpace(item.Type)
		if typ != "file_manager_file_create" && typ != "file_manager_file" {
			items[i] = fileManagerBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "type must be file_manager_file_create or file_manager_file")
			continue
		}

		uploadIntentUUID := strings.TrimSpace(item.Attributes.UploadIntentUUID)
		uploadIntentID, err := uuidv7.ParseString(uploadIntentUUID)
		if err != nil {
			items[i] = fileManagerBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "upload_intent_uuid is invalid UUID")
			continue
		}

		var folderID []byte
		folderProvided := false
		if item.Attributes.FolderID != nil {
			folderProvided = true
			folderUUID := strings.TrimSpace(*item.Attributes.FolderID)
			if folderUUID != "" {
				parsed, parseErr := uuidv7.ParseString(folderUUID)
				if parseErr != nil {
					items[i] = fileManagerBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "folder_id is invalid UUID")
					continue
				}
				folderID = parsed
			}
		}

		name := strings.TrimSpace(item.Attributes.Name)
		if name == "" {
			items[i] = fileManagerBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "name is required")
			continue
		}

		validParams = append(validParams, filemanager.CreateFileFromUploadIntentParams{
			UploadIntentID: uploadIntentID,
			FolderID:       folderID,
			FolderProvided: folderProvided,
			Name:           name,
			ActorID:        actorUserID(c),
		})
		validIndexes = append(validIndexes, i)
	}

	for i := range validParams {
		idx := validIndexes[i]
		created, err := h.svc.CreateFileNodeFromUploadIntent(c.UserContext(), validParams[i])
		if err != nil {
			items[idx] = fileManagerBulkActionErrorItem(idx, "Create failed", err)
			continue
		}
		items[idx] = fileManagerBulkSuccessItem(idx, "created", created)
	}

	return writeFileManagerBulkResponse(c, items)
}

func (h *FileManagerFileHandler) UploadFleetStatusFile(c *fiber.Ctx) error {
	return h.UploadFile(c)
}

func (h *FileManagerFileHandler) CreateFleetStatusFile(c *fiber.Ctx) error {
	return h.createFilesBulk(c)
}

// CreateAircraftImage godoc
// @Summary Create aircraft image file record from uploaded object
// @ID FileManagerCreateAircraftImageFromUploadedObject
// @Description Same as bulk create, but folder is resolved using hierarchy "__system_root_files__" -> "aircraft" -> "<aircraft designation>".
// @Description aircraft_uuid is required per item to resolve designation.
// @Tags File Manager
// @Accept json
// @Produce json
// @Param request body requestdto.FileManagerFileAircraftBulkCreateRequest true "Bulk create payload for aircraft image"
// @Success 200 {object} responsedto.FileManagerFileBulkResponse
// @Success 207 {object} responsedto.FileManagerFileBulkResponse
// @Success 201 {object} responsedto.FileManagerFileBulkResponse
// @Failure 422 {object} dto.ErrorResponse
// @Failure 400 {object} dto.ErrorResponse
// @Router /api/v1/file-manager/files/create-aircraft-image [post]
func (h *FileManagerFileHandler) CreateAircraftImage(c *fiber.Ctx) error {
	isBulk, err := isJSONAPIDataArrayPayload(c.Body())
	if err != nil {
		return writeFileManagerError(c, fiber.StatusBadRequest, "Invalid JSON", err.Error(), nil)
	}
	if !isBulk {
		return writeFileManagerError(c, fiber.StatusUnprocessableEntity, "Validation error", "data must be an array", &jsonapi.ErrorSource{Pointer: "/data"})
	}

	var req requestdto.FileManagerFileAircraftBulkCreateRequest
	if err := c.BodyParser(&req); err != nil {
		return writeFileManagerError(c, fiber.StatusBadRequest, "Invalid JSON", err.Error(), nil)
	}
	if len(req.Data) == 0 {
		return writeFileManagerError(c, fiber.StatusUnprocessableEntity, "Validation error", "data must contain at least one item", &jsonapi.ErrorSource{Pointer: "/data"})
	}

	actorID := actorUserID(c)
	if h.aircraft == nil {
		return writeFileManagerError(c, fiber.StatusServiceUnavailable, "Aircraft service unavailable", "aircraft service is not configured", nil)
	}

	items := make([]responsedto.FileManagerFileBulkItem, len(req.Data))
	validParams := make([]filemanager.CreateFileFromUploadIntentParams, 0, len(req.Data))
	validIndexes := make([]int, 0, len(req.Data))
	for i := range req.Data {
		item := req.Data[i]
		typ := strings.TrimSpace(item.Type)
		if typ != "file_manager_file_create" && typ != "file_manager_file" {
			items[i] = fileManagerBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "type must be file_manager_file_create or file_manager_file")
			continue
		}

		uploadIntentUUID := strings.TrimSpace(item.Attributes.UploadIntentUUID)
		uploadIntentID, err := uuidv7.ParseString(uploadIntentUUID)
		if err != nil {
			items[i] = fileManagerBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "upload_intent_uuid is invalid UUID")
			continue
		}

		name := strings.TrimSpace(item.Attributes.Name)
		if name == "" {
			items[i] = fileManagerBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "name is required")
			continue
		}

		aircraftUUID := strings.TrimSpace(item.Attributes.AircraftUUID)
		aircraftID, parseAircraftErr := uuidv7.ParseString(aircraftUUID)
		if parseAircraftErr != nil {
			items[i] = fileManagerBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "aircraft_uuid is invalid UUID")
			continue
		}

		aircraftRow, getAircraftErr := h.aircraft.GetByID(c.UserContext(), aircraftID)
		if getAircraftErr != nil {
			items[i] = fileManagerBulkSystemErrorItem(i, fiber.StatusBadRequest, "Create failed", getAircraftErr.Error())
			continue
		}
		if aircraftRow == nil {
			items[i] = fileManagerBulkSystemErrorItem(i, fiber.StatusNotFound, "Not found", "aircraft not found")
			continue
		}

		designation := strings.TrimSpace(aircraftRow.Designation)
		if designation == "" {
			items[i] = fileManagerBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "aircraft designation cannot be empty")
			continue
		}

		aircraftDesignationFolderID, resolveErr := h.ensureAircraftDesignationFolder(c.UserContext(), actorID, designation)
		if resolveErr != nil {
			items[i] = fileManagerBulkActionErrorItem(i, "Create failed", resolveErr)
			continue
		}

		validParams = append(validParams, filemanager.CreateFileFromUploadIntentParams{
			UploadIntentID: uploadIntentID,
			FolderID:       aircraftDesignationFolderID,
			FolderProvided: true,
			Name:           name,
			ActorID:        actorID,
		})
		validIndexes = append(validIndexes, i)
	}

	for i := range validParams {
		idx := validIndexes[i]
		existingRows, _, listErr := h.svc.ListFilesByFolder(c.UserContext(), validParams[i].FolderID, "name ASC", 0, 0)
		if listErr != nil {
			items[idx] = fileManagerBulkActionErrorItem(idx, "Replace failed", listErr)
			continue
		}
		replaceFailed := false
		for j := range existingRows {
			if len(existingRows[j].ID) != 16 {
				continue
			}
			now := time.Now().UTC()
			if deleteErr := h.svc.DeleteFile(c.UserContext(), existingRows[j].ID, actorID, &now); deleteErr != nil {
				items[idx] = fileManagerBulkActionErrorItem(idx, "Replace failed", deleteErr)
				replaceFailed = true
				break
			}
			if purgeErr := h.svc.PurgeFileNode(c.UserContext(), existingRows[j].ID); purgeErr != nil {
				items[idx] = fileManagerBulkActionErrorItem(idx, "Replace failed", purgeErr)
				replaceFailed = true
				break
			}
		}
		if replaceFailed {
			continue
		}

		created, err := h.svc.CreateFileNodeFromUploadIntent(c.UserContext(), validParams[i])
		if err != nil {
			items[idx] = fileManagerBulkActionErrorItem(idx, "Create failed", err)
			continue
		}
		items[idx] = fileManagerBulkSuccessItem(idx, "created", created)
	}

	return writeFileManagerBulkResponse(c, items)
}

// CreateBaseImage godoc
// @Summary Create base image file record from uploaded object
// @ID FileManagerCreateBaseImageFromUploadedObject
// @Description Same as bulk create, but folder is resolved using hierarchy "__system_root_files__" -> "bases" -> "<base-type>" -> "<base-name>".
// @Description base_uuid is required per item.
// @Tags File Manager
// @Accept json
// @Produce json
// @Param request body requestdto.FileManagerFileBaseBulkCreateRequest true "Bulk create payload for base image"
// @Success 200 {object} responsedto.FileManagerFileBulkResponse
// @Success 207 {object} responsedto.FileManagerFileBulkResponse
// @Success 201 {object} responsedto.FileManagerFileBulkResponse
// @Failure 422 {object} dto.ErrorResponse
// @Failure 400 {object} dto.ErrorResponse
// @Router /api/v1/file-manager/files/create-base-image [post]
func (h *FileManagerFileHandler) CreateBaseImage(c *fiber.Ctx) error {
	isBulk, err := isJSONAPIDataArrayPayload(c.Body())
	if err != nil {
		return writeFileManagerError(c, fiber.StatusBadRequest, "Invalid JSON", err.Error(), nil)
	}
	if !isBulk {
		return writeFileManagerError(c, fiber.StatusUnprocessableEntity, "Validation error", "data must be an array", &jsonapi.ErrorSource{Pointer: "/data"})
	}

	var req requestdto.FileManagerFileBaseBulkCreateRequest
	if err := c.BodyParser(&req); err != nil {
		return writeFileManagerError(c, fiber.StatusBadRequest, "Invalid JSON", err.Error(), nil)
	}
	if len(req.Data) == 0 {
		return writeFileManagerError(c, fiber.StatusUnprocessableEntity, "Validation error", "data must contain at least one item", &jsonapi.ErrorSource{Pointer: "/data"})
	}

	actorID := actorUserID(c)
	if h.base == nil {
		return writeFileManagerError(c, fiber.StatusServiceUnavailable, "Base service unavailable", "base service is not configured", nil)
	}

	items := make([]responsedto.FileManagerFileBulkItem, len(req.Data))
	validParams := make([]filemanager.CreateFileFromUploadIntentParams, 0, len(req.Data))
	validIndexes := make([]int, 0, len(req.Data))
	for i := range req.Data {
		item := req.Data[i]
		typ := strings.TrimSpace(item.Type)
		if typ != "file_manager_file_create" && typ != "file_manager_file" {
			items[i] = fileManagerBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "type must be file_manager_file_create or file_manager_file")
			continue
		}

		uploadIntentUUID := strings.TrimSpace(item.Attributes.UploadIntentUUID)
		uploadIntentID, parseUploadErr := uuidv7.ParseString(uploadIntentUUID)
		if parseUploadErr != nil {
			items[i] = fileManagerBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "upload_intent_uuid is invalid UUID")
			continue
		}

		name := strings.TrimSpace(item.Attributes.Name)
		if name == "" {
			items[i] = fileManagerBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "name is required")
			continue
		}

		baseUUID := strings.TrimSpace(item.Attributes.BaseUUID)
		baseID, parseBaseErr := uuidv7.ParseString(baseUUID)
		if parseBaseErr != nil {
			items[i] = fileManagerBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "base_uuid is invalid UUID")
			continue
		}

		baseRow, baseType, getBaseErr := h.getBaseByIDAnyType(c.UserContext(), baseID)
		if getBaseErr != nil {
			items[i] = fileManagerBulkSystemErrorItem(i, fiber.StatusBadRequest, "Create failed", getBaseErr.Error())
			continue
		}
		if baseRow == nil {
			items[i] = fileManagerBulkSystemErrorItem(i, fiber.StatusNotFound, "Not found", "base not found")
			continue
		}

		baseName := strings.TrimSpace(baseRow.BaseName)
		if baseName == "" {
			items[i] = fileManagerBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "base name cannot be empty")
			continue
		}

		baseFolderID, resolveErr := h.ensureBaseImageFolder(c.UserContext(), actorID, baseType, baseName)
		if resolveErr != nil {
			items[i] = fileManagerBulkActionErrorItem(i, "Create failed", resolveErr)
			continue
		}

		validParams = append(validParams, filemanager.CreateFileFromUploadIntentParams{
			UploadIntentID: uploadIntentID,
			FolderID:       baseFolderID,
			FolderProvided: true,
			Name:           name,
			ActorID:        actorID,
		})
		validIndexes = append(validIndexes, i)
	}

	for i := range validParams {
		idx := validIndexes[i]
		existingRows, _, listErr := h.svc.ListFilesByFolder(c.UserContext(), validParams[i].FolderID, "name ASC", 0, 0)
		if listErr != nil {
			items[idx] = fileManagerBulkActionErrorItem(idx, "Replace failed", listErr)
			continue
		}

		replaceFailed := false
		for j := range existingRows {
			if len(existingRows[j].ID) != 16 {
				continue
			}
			now := time.Now().UTC()
			if deleteErr := h.svc.DeleteFile(c.UserContext(), existingRows[j].ID, actorID, &now); deleteErr != nil {
				items[idx] = fileManagerBulkActionErrorItem(idx, "Replace failed", deleteErr)
				replaceFailed = true
				break
			}
			if purgeErr := h.svc.PurgeFileNode(c.UserContext(), existingRows[j].ID); purgeErr != nil {
				items[idx] = fileManagerBulkActionErrorItem(idx, "Replace failed", purgeErr)
				replaceFailed = true
				break
			}
		}
		if replaceFailed {
			continue
		}

		created, createErr := h.svc.CreateFileNodeFromUploadIntent(c.UserContext(), validParams[i])
		if createErr != nil {
			items[idx] = fileManagerBulkActionErrorItem(idx, "Create failed", createErr)
			continue
		}
		items[idx] = fileManagerBulkSuccessItem(idx, "created", created)
	}

	return writeFileManagerBulkResponse(c, items)
}

// CreateDULImage godoc
// @Summary Create DUL image file record from uploaded object
// @Description Finalize uploaded DUL image file(s) from upload intent.
// @Description If dul_uuid is provided, file is created under DUL folder hierarchy.
// @Description If dul_uuid is omitted, file is still created (unassigned), then can be attached during /api/v1/dul/create via file_uuid.
// @Description Possible status: 201 (all success), 207 (partial success), 400 (invalid json/create failed), 401 (unauthorized), 403 (forbidden), 404 (dul or file resources not found), 409 (upload-intent conflict), 422 (validation), 503 (DUL or file-manager service unavailable).
// @Description Possible error code: INVALID_REQUEST, USER_UNAUTHORIZED, FORBIDDEN_ACCESS, FILE_MANAGER_VALIDATION_FAILED, FILE_MANAGER_FILE_NOT_FOUND, FILE_MANAGER_UPLOAD_INTENT_NOT_FOUND, FILE_MANAGER_UPLOAD_INTENT_CONFLICT, DUL_NOT_FOUND, FILE_MANAGER_STORAGE_UNAVAILABLE, FILE_MANAGER_QUEUE_UNAVAILABLE, FILE_MANAGER_ATTACHMENT_SERVICE_UNAVAILABLE, FILE_MANAGER_ACTION_FAILED.
// @Tags File Manager
// @Accept json
// @Produce json
// @Param request body requestdto.FileManagerFileDULBulkCreateRequest true "Bulk create payload for DUL image"
// @Success 200 {object} responsedto.FileManagerFileBulkResponse
// @Success 207 {object} responsedto.FileManagerFileBulkResponse
// @Success 201 {object} responsedto.FileManagerFileBulkResponse
// @Failure 401 {object} dto.ErrorResponse
// @Failure 403 {object} dto.ErrorResponse
// @Failure 404 {object} dto.ErrorResponse
// @Failure 409 {object} dto.ErrorResponse
// @Failure 422 {object} dto.ErrorResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 503 {object} dto.ErrorResponse
// @Router /api/v1/file-manager/files/create-dul-image [post]
func (h *FileManagerFileHandler) CreateDULImage(c *fiber.Ctx) error {
	isBulk, err := isJSONAPIDataArrayPayload(c.Body())
	if err != nil {
		return writeFileManagerError(c, fiber.StatusBadRequest, "Invalid JSON", err.Error(), nil)
	}
	if !isBulk {
		return writeFileManagerError(c, fiber.StatusUnprocessableEntity, "Validation error", "data must be an array", &jsonapi.ErrorSource{Pointer: "/data"})
	}

	var req requestdto.FileManagerFileDULBulkCreateRequest
	if err := c.BodyParser(&req); err != nil {
		return writeFileManagerError(c, fiber.StatusBadRequest, "Invalid JSON", err.Error(), nil)
	}
	if len(req.Data) == 0 {
		return writeFileManagerError(c, fiber.StatusUnprocessableEntity, "Validation error", "data must contain at least one item", &jsonapi.ErrorSource{Pointer: "/data"})
	}
	actorID := actorUserID(c)
	items := make([]responsedto.FileManagerFileBulkItem, len(req.Data))
	validParams := make([]filemanager.CreateFileFromUploadIntentParams, 0, len(req.Data))
	validIndexes := make([]int, 0, len(req.Data))

	for i := range req.Data {
		item := req.Data[i]
		if strings.TrimSpace(item.Type) != "file_manager_file_create" && strings.TrimSpace(item.Type) != "file_manager_file" {
			items[i] = fileManagerBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "type must be file_manager_file_create or file_manager_file")
			continue
		}
		uploadIntentID, parseUploadErr := uuidv7.ParseString(strings.TrimSpace(item.Attributes.UploadIntentUUID))
		if parseUploadErr != nil {
			items[i] = fileManagerBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "upload_intent_uuid is invalid UUID")
			continue
		}
		name := strings.TrimSpace(item.Attributes.Name)
		if name == "" {
			items[i] = fileManagerBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "name is required")
			continue
		}

		rawDULUUID := strings.TrimSpace(item.Attributes.DULUUID)
		if rawDULUUID == "" {
			validParams = append(validParams, filemanager.CreateFileFromUploadIntentParams{
				UploadIntentID: uploadIntentID,
				Name:           name,
				FolderProvided: false,
				ActorID:        actorID,
			})
			validIndexes = append(validIndexes, i)
			continue
		}

		dulID, parseDULErr := uuidv7.ParseString(rawDULUUID)
		if parseDULErr != nil {
			items[i] = fileManagerBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "dul_uuid is invalid UUID")
			continue
		}
		if h.dul == nil {
			items[i] = fileManagerBulkSystemErrorItem(i, fiber.StatusServiceUnavailable, "DUL service unavailable", "dul service is not configured")
			continue
		}
		dulRow, getErr := h.dul.GetByID(c.UserContext(), dulID)
		if getErr != nil {
			items[i] = fileManagerBulkSystemErrorItem(i, fiber.StatusBadRequest, "Create failed", getErr.Error())
			continue
		}
		if dulRow == nil {
			items[i] = fileManagerBulkSystemErrorItem(i, fiber.StatusNotFound, "Not found", "dul not found")
			continue
		}
		folderID, folderErr := h.ensureDULImageFolder(c.UserContext(), actorID, strings.TrimSpace(dulRow.Name), dulID)
		if folderErr != nil {
			items[i] = fileManagerBulkActionErrorItem(i, "Create failed", folderErr)
			continue
		}
		validParams = append(validParams, filemanager.CreateFileFromUploadIntentParams{
			UploadIntentID: uploadIntentID,
			FolderID:       folderID,
			FolderProvided: true,
			Name:           name,
			ActorID:        actorID,
		})
		validIndexes = append(validIndexes, i)
	}

	for i := range validParams {
		idx := validIndexes[i]
		created, createErr := h.svc.CreateFileNodeFromUploadIntent(c.UserContext(), validParams[i])
		if createErr != nil {
			items[idx] = fileManagerBulkActionErrorItem(idx, "Create failed", createErr)
			continue
		}
		if _, _, sanitizeErr := h.sanitizeStoredXLSXObject(c.UserContext(), created, created.ObjectKey, created.Name, created.MimeType, "create-takeover-file"); sanitizeErr != nil {
			items[idx] = fileManagerBulkActionErrorItem(idx, "Create failed", sanitizeErr)
			continue
		}
		items[idx] = fileManagerBulkSuccessItem(idx, "created", created)
	}
	return writeFileManagerBulkResponse(c, items)
}

// CreateTakeoverFile godoc
// @Summary Create takeover file node from uploaded object
// @Description Finalize uploaded takeover file(s) from upload intent. The takeover relation is attached later from file_id during takeover create/update.
// @Tags File Manager
// @Accept json
// @Produce json
// @Param request body requestdto.FileManagerFileTakeoverBulkCreateRequest true "Bulk create payload for takeover file"
// @Success 200 {object} responsedto.FileManagerFileBulkResponse
// @Success 207 {object} responsedto.FileManagerFileBulkResponse
// @Success 201 {object} responsedto.FileManagerFileBulkResponse
// @Failure 422 {object} dto.ErrorResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 404 {object} dto.ErrorResponse
// @Router /api/v1/file-manager/files/create-takeover-file [post]
func (h *FileManagerFileHandler) CreateTakeoverFile(c *fiber.Ctx) error {
	isBulk, err := isJSONAPIDataArrayPayload(c.Body())
	if err != nil {
		return writeFileManagerError(c, fiber.StatusBadRequest, "Invalid JSON", err.Error(), nil)
	}
	if !isBulk {
		return writeFileManagerError(c, fiber.StatusUnprocessableEntity, "Validation error", "data must be an array", &jsonapi.ErrorSource{Pointer: "/data"})
	}

	var req requestdto.FileManagerFileTakeoverBulkCreateRequest
	if err := c.BodyParser(&req); err != nil {
		return writeFileManagerError(c, fiber.StatusBadRequest, "Invalid JSON", err.Error(), nil)
	}
	if len(req.Data) == 0 {
		return writeFileManagerError(c, fiber.StatusUnprocessableEntity, "Validation error", "data must contain at least one item", &jsonapi.ErrorSource{Pointer: "/data"})
	}

	actorID := actorUserID(c)

	items := make([]responsedto.FileManagerFileBulkItem, len(req.Data))
	validParams := make([]filemanager.CreateFileFromUploadIntentParams, 0, len(req.Data))
	validIndexes := make([]int, 0, len(req.Data))

	for i := range req.Data {
		item := req.Data[i]
		typ := strings.TrimSpace(item.Type)
		if typ != "file_manager_file_create" && typ != "file_manager_file" {
			items[i] = fileManagerBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "type must be file_manager_file_create or file_manager_file")
			continue
		}

		uploadIntentUUID := strings.TrimSpace(item.Attributes.UploadIntentUUID)
		uploadIntentID, parseUploadErr := uuidv7.ParseString(uploadIntentUUID)
		if parseUploadErr != nil {
			items[i] = fileManagerBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "upload_intent_uuid is invalid UUID")
			continue
		}

		name := strings.TrimSpace(item.Attributes.Name)
		if name == "" {
			items[i] = fileManagerBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "name is required")
			continue
		}

		validParams = append(validParams, filemanager.CreateFileFromUploadIntentParams{
			UploadIntentID: uploadIntentID,
			FolderProvided: false,
			Name:           name,
			ActorID:        actorID,
		})
		validIndexes = append(validIndexes, i)
	}

	for i := range validParams {
		idx := validIndexes[i]
		created, createErr := h.svc.CreateFileNodeFromUploadIntent(c.UserContext(), validParams[i])
		if createErr != nil {
			items[idx] = fileManagerBulkActionErrorItem(idx, "Create failed", createErr)
			continue
		}
		items[idx] = fileManagerBulkSuccessItem(idx, "created", created)
	}

	return writeFileManagerBulkResponse(c, items)
}

// CreateUserImage godoc
// @Summary Create user profile image file record from uploaded object
// @ID FileManagerCreateUserImageFromUploadedObject
// @Description Same as bulk create, but folder is resolved using hierarchy "__system_root_files__" -> "user" -> "<username>-<user-uuid>".
// @Description user_uuid is required per item to resolve target user folder.
// @Tags File Manager
// @Accept json
// @Produce json
// @Param request body requestdto.FileManagerFileUserBulkCreateRequest true "Bulk create payload for user profile image"
// @Success 200 {object} responsedto.FileManagerFileBulkResponse
// @Success 207 {object} responsedto.FileManagerFileBulkResponse
// @Success 201 {object} responsedto.FileManagerFileBulkResponse
// @Failure 422 {object} dto.ErrorResponse
// @Failure 400 {object} dto.ErrorResponse
// @Router /api/v1/file-manager/files/create-user-image [post]
func (h *FileManagerFileHandler) CreateUserImage(c *fiber.Ctx) error {
	isBulk, err := isJSONAPIDataArrayPayload(c.Body())
	if err != nil {
		return writeFileManagerError(c, fiber.StatusBadRequest, "Invalid JSON", err.Error(), nil)
	}
	if !isBulk {
		return writeFileManagerError(c, fiber.StatusUnprocessableEntity, "Validation error", "data must be an array", &jsonapi.ErrorSource{Pointer: "/data"})
	}
	var req requestdto.FileManagerFileUserBulkCreateRequest
	if err := c.BodyParser(&req); err != nil {
		return writeFileManagerError(c, fiber.StatusBadRequest, "Invalid JSON", err.Error(), nil)
	}
	if len(req.Data) == 0 {
		return writeFileManagerError(c, fiber.StatusUnprocessableEntity, "Validation error", "data must contain at least one item", &jsonapi.ErrorSource{Pointer: "/data"})
	}
	actorID := actorUserID(c)
	if h.user == nil {
		return writeFileManagerError(c, fiber.StatusServiceUnavailable, "User service unavailable", "user service is not configured", nil)
	}

	items := make([]responsedto.FileManagerFileBulkItem, len(req.Data))
	validParams := make([]filemanager.CreateFileFromUploadIntentParams, 0, len(req.Data))
	validIndexes := make([]int, 0, len(req.Data))
	for i := range req.Data {
		item := req.Data[i]
		typ := strings.TrimSpace(item.Type)
		if typ != "file_manager_file_create" && typ != "file_manager_file" {
			items[i] = fileManagerBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "type must be file_manager_file_create or file_manager_file")
			continue
		}
		uploadIntentID, parseUploadErr := uuidv7.ParseString(strings.TrimSpace(item.Attributes.UploadIntentUUID))
		if parseUploadErr != nil {
			items[i] = fileManagerBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "upload_intent_uuid is invalid UUID")
			continue
		}
		name := strings.TrimSpace(item.Attributes.Name)
		if name == "" {
			items[i] = fileManagerBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "name is required")
			continue
		}
		userID, parseUserErr := uuidv7.ParseString(strings.TrimSpace(item.Attributes.UserUUID))
		if parseUserErr != nil {
			items[i] = fileManagerBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "user_uuid is invalid UUID")
			continue
		}
		userRow, userErr := h.user.GetByID(c.UserContext(), userID)
		if userErr != nil {
			items[i] = fileManagerBulkSystemErrorItem(i, fiber.StatusBadRequest, "Create failed", userErr.Error())
			continue
		}
		if userRow == nil {
			items[i] = fileManagerBulkSystemErrorItem(i, fiber.StatusNotFound, "Not found", "user not found")
			continue
		}
		username := strings.TrimSpace(userRow.Username)
		if username == "" {
			username = strings.TrimSpace(userRow.FirstName + "-" + userRow.LastName)
		}
		if username == "" {
			username = "user"
		}
		userFolderID, resolveErr := h.ensureUserImageFolder(c.UserContext(), actorID, username, userID)
		if resolveErr != nil {
			items[i] = fileManagerBulkActionErrorItem(i, "Create failed", resolveErr)
			continue
		}
		validParams = append(validParams, filemanager.CreateFileFromUploadIntentParams{
			UploadIntentID: uploadIntentID,
			FolderID:       userFolderID,
			FolderProvided: true,
			Name:           name,
			ActorID:        actorID,
		})
		validIndexes = append(validIndexes, i)
	}
	for i := range validParams {
		idx := validIndexes[i]
		existingRows, _, listErr := h.svc.ListFilesByFolder(c.UserContext(), validParams[i].FolderID, "name ASC", 0, 0)
		if listErr != nil {
			items[idx] = fileManagerBulkActionErrorItem(idx, "Replace failed", listErr)
			continue
		}
		replaceFailed := false
		for j := range existingRows {
			if len(existingRows[j].ID) != 16 {
				continue
			}
			now := time.Now().UTC()
			if deleteErr := h.svc.DeleteFile(c.UserContext(), existingRows[j].ID, actorID, &now); deleteErr != nil {
				items[idx] = fileManagerBulkActionErrorItem(idx, "Replace failed", deleteErr)
				replaceFailed = true
				break
			}
			if purgeErr := h.svc.PurgeFileNode(c.UserContext(), existingRows[j].ID); purgeErr != nil {
				items[idx] = fileManagerBulkActionErrorItem(idx, "Replace failed", purgeErr)
				replaceFailed = true
				break
			}
		}
		if replaceFailed {
			continue
		}
		created, createErr := h.svc.CreateFileNodeFromUploadIntent(c.UserContext(), validParams[i])
		if createErr != nil {
			items[idx] = fileManagerBulkActionErrorItem(idx, "Create failed", createErr)
			continue
		}
		items[idx] = fileManagerBulkSuccessItem(idx, "created", created)
	}
	return writeFileManagerBulkResponse(c, items)
}

func isJSONAPIDataArrayPayload(body []byte) (bool, error) {
	var envelope struct {
		Data json.RawMessage `json:"data"`
	}
	if err := json.Unmarshal(body, &envelope); err != nil {
		return false, err
	}
	data := bytes.TrimSpace(envelope.Data)
	if len(data) == 0 {
		return false, nil
	}
	return data[0] == '[', nil
}

// isFileManagerCreateBulkPayload is kept for backward compatibility with existing tests/helpers.
func isFileManagerCreateBulkPayload(body []byte) (bool, error) {
	return isJSONAPIDataArrayPayload(body)
}

func parseUploadNameOverrides(formValues map[string][]string, filesCount int) ([]string, error) {
	nameOverrides := formValues["names"]
	if len(nameOverrides) == 0 {
		nameOverrides = formValues["name"]
	}
	if len(nameOverrides) > 0 && len(nameOverrides) != filesCount {
		return nil, errors.New("names count must match files count")
	}
	return nameOverrides, nil
}

func (h *FileManagerFileHandler) ensureAircraftDesignationFolder(ctx context.Context, actorID []byte, designation string) ([]byte, error) {
	rootFolder, err := h.svc.GetOrCreateSystemRootFolder(ctx)
	if err != nil {
		return nil, err
	}
	if rootFolder == nil || len(rootFolder.ID) != 16 {
		return nil, filemanager.ErrFolderNotFound
	}

	aircraftFolderID, err := h.ensureFolderByParentAndName(ctx, rootFolder.ID, aircraftImageFolderName, actorID)
	if err != nil {
		return nil, err
	}

	return h.ensureFolderByParentAndName(ctx, aircraftFolderID, designation, actorID)
}

func (h *FileManagerFileHandler) ensureBaseImageFolder(ctx context.Context, actorID []byte, baseType, baseName string) ([]byte, error) {
	rootFolder, err := h.svc.GetOrCreateSystemRootFolder(ctx)
	if err != nil {
		return nil, err
	}
	if rootFolder == nil || len(rootFolder.ID) != 16 {
		return nil, filemanager.ErrFolderNotFound
	}

	basesFolderID, err := h.ensureFolderByParentAndName(ctx, rootFolder.ID, baseImageFolderName, actorID)
	if err != nil {
		return nil, err
	}
	baseTypeFolderID, err := h.ensureFolderByParentAndName(ctx, basesFolderID, strings.TrimSpace(baseType), actorID)
	if err != nil {
		return nil, err
	}
	return h.ensureFolderByParentAndName(ctx, baseTypeFolderID, strings.TrimSpace(baseName), actorID)
}

func (h *FileManagerFileHandler) ensureUserImageFolder(ctx context.Context, actorID []byte, username string, userID []byte) ([]byte, error) {
	rootFolder, err := h.svc.GetOrCreateSystemRootFolder(ctx)
	if err != nil {
		return nil, err
	}
	if rootFolder == nil || len(rootFolder.ID) != 16 {
		return nil, filemanager.ErrFolderNotFound
	}
	usersFolderID, err := h.ensureFolderByParentAndName(ctx, rootFolder.ID, userImageFolderName, actorID)
	if err != nil {
		return nil, err
	}
	userUUID := idString(userID)
	folderName := fmt.Sprintf("%s-%s", sanitizeFolderName(username), userUUID)
	return h.ensureFolderByParentAndName(ctx, usersFolderID, folderName, actorID)
}

func (h *FileManagerFileHandler) ensureDULImageFolder(ctx context.Context, actorID []byte, dulName string, dulID []byte) ([]byte, error) {
	rootFolder, err := h.svc.GetOrCreateSystemRootFolder(ctx)
	if err != nil {
		return nil, err
	}
	dulsFolderID, err := h.ensureFolderByParentAndName(ctx, rootFolder.ID, dulImageFolderName, actorID)
	if err != nil {
		return nil, err
	}
	folderName := strings.TrimSpace(dulName)
	if folderName == "" {
		folderName = "dul"
	}
	if id := idString(dulID); id != "" {
		folderName += "-" + id
	}
	return h.ensureFolderByParentAndName(ctx, dulsFolderID, folderName, actorID)
}

func (h *FileManagerFileHandler) ensureTakeoverFileFolder(ctx context.Context, actorID []byte, takeoverID []byte) ([]byte, error) {
	rootFolder, err := h.svc.GetOrCreateSystemRootFolder(ctx)
	if err != nil {
		return nil, err
	}
	if rootFolder == nil || len(rootFolder.ID) != 16 {
		return nil, filemanager.ErrFolderNotFound
	}
	takeoversFolderID, err := h.ensureFolderByParentAndName(ctx, rootFolder.ID, takeoverFileFolderName, actorID)
	if err != nil {
		return nil, err
	}
	return h.ensureFolderByParentAndName(ctx, takeoversFolderID, idString(takeoverID), actorID)
}

func sanitizeFolderName(in string) string {
	name := strings.ToLower(strings.TrimSpace(in))
	if name == "" {
		return "user"
	}
	var b strings.Builder
	lastDash := false
	for _, r := range name {
		isAZ := r >= 'a' && r <= 'z'
		is09 := r >= '0' && r <= '9'
		if isAZ || is09 {
			b.WriteRune(r)
			lastDash = false
			continue
		}
		if !lastDash {
			b.WriteByte('-')
			lastDash = true
		}
	}
	out := strings.Trim(b.String(), "-")
	if out == "" {
		return "user"
	}
	return out
}

func (h *FileManagerFileHandler) getBaseByIDAnyType(ctx context.Context, id []byte) (*base.Base, string, error) {
	for _, categoryType := range []string{base.CategoryKeyHEMS, base.CategoryKeyRegular} {
		row, err := h.base.GetBaseByID(ctx, id, categoryType)
		if err != nil {
			return nil, "", err
		}
		if row != nil {
			return row, categoryType, nil
		}
	}
	return nil, "", nil
}

func ensureFileManagerFolder(ctx context.Context, svc filemanager.Service, parentID []byte, folderName string, actorID []byte) ([]byte, error) {
	normalizedName := strings.ToLower(strings.TrimSpace(folderName))
	if normalizedName == "" {
		return nil, filemanager.ErrInvalidName
	}

	row, err := svc.GetFolderByParentAndName(ctx, parentID, normalizedName)
	if err != nil {
		return nil, err
	}
	if row != nil && len(row.ID) == 16 {
		return row.ID, nil
	}

	created, err := svc.CreateFolderNode(ctx, filemanager.CreateFolderParams{
		Name:     folderName,
		ParentID: parentID,
		ActorID:  actorID,
	})
	if err == nil && created != nil && len(created.ID) == 16 {
		return created.ID, nil
	}
	if !errors.Is(err, filemanager.ErrDuplicateName) {
		return nil, err
	}

	row, err = svc.GetFolderByParentAndName(ctx, parentID, normalizedName)
	if err != nil {
		return nil, err
	}
	if row == nil || len(row.ID) != 16 {
		return nil, filemanager.ErrFolderNotFound
	}
	return row.ID, nil
}

func (h *FileManagerFileHandler) ensureFolderByParentAndName(ctx context.Context, parentID []byte, folderName string, actorID []byte) ([]byte, error) {
	return ensureFileManagerFolder(ctx, h.svc, parentID, folderName, actorID)
}

func ensureMissionFileFolderPath(ctx context.Context, svc filemanager.Service, missionID []byte, actorID []byte) ([]byte, error) {
	rootFolder, err := svc.GetOrCreateSystemRootFolder(ctx)
	if err != nil {
		return nil, err
	}
	if rootFolder == nil || len(rootFolder.ID) != 16 {
		return nil, filemanager.ErrFolderNotFound
	}
	missionsFolderID, err := ensureFileManagerFolder(ctx, svc, rootFolder.ID, missionFileFolderName, actorID)
	if err != nil {
		return nil, err
	}
	return ensureFileManagerFolder(ctx, svc, missionsFolderID, idString(missionID), actorID)
}

// GetFile godoc
// @Summary Get file detail
// @ID FileManagerGetFile
// @Description Return file metadata by UUID.
// @Tags File Manager
// @Produce json
// @Param uuid path string true "File UUID (UUIDv7)"
// @Success 200 {object} responsedto.FileManagerFileDetailResponse
// @Failure 404 {object} dto.ErrorResponse
// @Failure 422 {object} dto.ErrorResponse
// @Router /api/v1/file-manager/files/get/{uuid} [get]
func (h *FileManagerFileHandler) GetFile(c *fiber.Ctx) error {
	id, err := h.parseUUIDParam(c, "uuid", "/path/uuid")
	if err != nil {
		return err
	}

	row, err := h.svc.GetFileByID(c.UserContext(), id)
	if err != nil {
		return writeFileManagerError(c, fiber.StatusBadRequest, "Get failed", err.Error(), nil)
	}
	if row == nil {
		return writeFileManagerError(c, fiber.StatusNotFound, "Not found", "file not found", nil)
	}

	isRootFile := false
	root, rootErr := h.svc.GetOrCreateSystemRootFolder(c.Context())
	if rootErr != nil {
		return writeFileManagerActionError(c, "Get failed", rootErr)
	}
	if root != nil && len(root.ID) == len(row.FolderID) && bytes.Equal(root.ID, row.FolderID) {
		isRootFile = true
	}

	return response.Write(c, fiber.StatusOK, fileManagerFileDetailResource(c.UserContext(), h.storage, row, isRootFile))
}

func (h *FileManagerFileHandler) ListFiles(c *fiber.Ctx) error {
	folderUUID := strings.TrimSpace(c.Query("folder_uuid"))
	var folderID []byte
	if folderUUID != "" {
		parsed, err := uuidv7.ParseString(folderUUID)
		if err != nil {
			return writeFileManagerError(c, fiber.StatusUnprocessableEntity, "Validation error", "folder_uuid is invalid UUID", &jsonapi.ErrorSource{Pointer: "/query/folder_uuid"})
		}
		folderID = parsed
	} else {
		root, err := h.svc.GetOrCreateSystemRootFolder(c.UserContext())
		if err != nil {
			return writeFileManagerActionError(c, "List failed", err)
		}
		folderID = root.ID
	}

	pageNumber, pageSize, offset := parseFileManagerFilePagination(c)
	_ = offset
	sort := c.Query("sort")

	rows, total, err := h.svc.ListFilesByFolder(c.UserContext(), folderID, sort, 0, 0)
	if err != nil {
		return writeFileManagerError(c, fiber.StatusBadRequest, "List failed", err.Error(), nil)
	}

	data := make([]responsedto.FileManagerFileListResource, 0, len(rows))
	for i := range rows {
		data = append(data, fileManagerFileListResource(c.UserContext(), h.storage, &rows[i]))
	}

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

func (h *FileManagerFileHandler) ListTrashFiles(c *fiber.Ctx) error {
	var folderID []byte
	folderUUID := strings.TrimSpace(c.Query("folder_uuid"))
	if folderUUID != "" {
		id, err := uuidv7.ParseString(folderUUID)
		if err != nil {
			return writeFileManagerError(c, fiber.StatusUnprocessableEntity, "Validation error", "folder_uuid is invalid UUID", &jsonapi.ErrorSource{Pointer: "/query/folder_uuid"})
		}
		folderID = id
	}

	pageNumber, pageSize, offset := parseFileManagerFilePagination(c)
	_ = offset
	sort := c.Query("sort")

	rows, total, err := h.svc.ListTrashFilesByFolder(c.UserContext(), folderID, sort, 0, 0)
	if err != nil {
		return writeFileManagerError(c, fiber.StatusBadRequest, "List failed", err.Error(), nil)
	}

	data := make([]responsedto.FileManagerFileListResource, 0, len(rows))
	for i := range rows {
		data = append(data, fileManagerFileListResource(c.UserContext(), h.storage, &rows[i]))
	}

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

// UpdateFile godoc
// @Summary Update file entity (rename and/or move)
// @ID FileManagerUpdateFileEntity
// @Description Update file metadata by UUID (rename and/or move folder).
// @Tags File Manager
// @Accept json
// @Produce json
// @Param uuid path string true "File UUID (UUIDv7)"
// @Param request body requestdto.FileManagerFileUpdateRequest true "JSON:API file manager file update request"
// @Success 200 {object} responsedto.FileManagerFileResponse
// @Failure 422 {object} dto.ErrorResponse
// @Failure 404 {object} dto.ErrorResponse
// @Router /api/v1/file-manager/files/update/{uuid} [patch]
func (h *FileManagerFileHandler) UpdateFile(c *fiber.Ctx) error {
	uuidStr := c.Params("uuid")
	id, err := uuidv7.ParseString(uuidStr)
	if err != nil {
		return writeFileManagerError(c, fiber.StatusUnprocessableEntity, "Validation error", "uuid is invalid UUID", &jsonapi.ErrorSource{Pointer: "/path/uuid"})
	}

	var req requestdto.FileManagerFileUpdateRequest
	if err := c.BodyParser(&req); err != nil {
		return writeFileManagerError(c, fiber.StatusBadRequest, "Invalid JSON", err.Error(), nil)
	}
	if errs := h.validate.ValidateStruct(req); errs != nil {
		return response.WriteErrors(c, fiber.StatusUnprocessableEntity, errs)
	}
	if strings.TrimSpace(req.Data.ID) == "" || req.Data.ID != uuidStr {
		return writeFileManagerError(c, fiber.StatusUnprocessableEntity, "Validation error", "id does not match path uuid", &jsonapi.ErrorSource{Pointer: "/data/id"})
	}
	attrs := req.Data.Attributes
	if attrs.Name == nil && attrs.FolderID == nil {
		return writeFileManagerError(c, fiber.StatusUnprocessableEntity, "Validation error", "at least one attribute must be provided", &jsonapi.ErrorSource{Pointer: "/data/attributes"})
	}

	var folderID []byte
	folderProvided := attrs.FolderID != nil
	if folderProvided && strings.TrimSpace(*attrs.FolderID) != "" {
		parsed, err := uuidv7.ParseString(strings.TrimSpace(*attrs.FolderID))
		if err != nil {
			return writeFileManagerError(c, fiber.StatusUnprocessableEntity, "Validation error", "folder_id is invalid UUID", &jsonapi.ErrorSource{Pointer: "/data/attributes/folder_id"})
		}
		folderID = parsed
	}

	updated, err := h.svc.MoveFileNode(c.UserContext(), id, filemanager.MoveFileParams{
		Name:           attrs.Name,
		FolderID:       folderID,
		FolderProvided: folderProvided,
		ActorID:        actorUserID(c),
	})
	if err != nil {
		return writeFileManagerActionError(c, "Update failed", err)
	}
	return response.Write(c, fiber.StatusOK, fileManagerFileResource(updated))
}

func (h *FileManagerFileHandler) DeleteFile(c *fiber.Ctx) error {
	id, err := h.parseUUIDParam(c, "uuid", "/path/uuid")
	if err != nil {
		return err
	}
	if err := h.svc.DeleteFile(c.UserContext(), id, actorUserID(c), nil); err != nil {
		return writeFileManagerActionError(c, "Delete failed", err)
	}

	return response.Write(c, fiber.StatusOK, fileManagerActionResultResource("file_manager_delete_result", id, "trashed", ""))
}

// RestoreFile restores a trashed file by ID to its original location.
func (h *FileManagerFileHandler) RestoreFile(c *fiber.Ctx) error {
	id, err := h.parseUUIDParam(c, "uuid", "/path/uuid")
	if err != nil {
		return err
	}

	if err := h.svc.RestoreFile(c.Context(), id, nil, false, actorUserID(c)); err != nil {
		return writeFileManagerActionError(c, "Restore failed", err)
	}

	row, err := h.svc.GetFileByID(c.Context(), id)
	if err != nil {
		return writeFileManagerActionError(c, "Restore failed", err)
	}
	if row == nil {
		return writeFileManagerActionError(c, "Restore failed", filemanager.ErrFileNotFound)
	}

	root, rootErr := h.svc.GetOrCreateSystemRootFolder(c.Context())
	if rootErr != nil {
		return writeFileManagerActionError(c, "Restore failed", rootErr)
	}
	locationFolderID := ""
	if root == nil || !bytes.Equal(row.FolderID, root.ID) {
		locationFolderID = idString(row.FolderID)
	}

	return response.Write(c, fiber.StatusOK, fileManagerActionResultResource("file_manager_restore_result", id, filemanager.FileStatusReady, locationFolderID))
}

func fileManagerFileResource(row *filemanager.File) responsedto.FileManagerFileResource {
	return responsedto.FileManagerFileResource{
		Type: "file_manager_file",
		ID:   idString(row.ID),
		Attributes: shareddto.FileManagerFileAttributes{
			FileUUID:            idString(row.ID),
			FolderID:            idString(row.FolderID),
			Name:                row.Name,
			NameNormalized:      row.NameNormalized,
			Extension:           row.Extension,
			SizeBytes:           row.SizeBytes,
			MimeType:            row.MimeType,
			IsTemplate:          row.IsTemplate,
			Status:              row.Status,
			FailureReason:       firstNonEmpty(strings.TrimSpace(row.FailureReason), strings.TrimSpace(row.UploadError)),
			UploadError:         firstNonEmpty(strings.TrimSpace(row.UploadError), strings.TrimSpace(row.FailureReason)),
			ProcessingStartedAt: timeStringOrEmpty(row.ProcessingStartedAt),
			ProcessedAt:         timeStringOrEmpty(row.ProcessedAt),
			ValidatedAt:         timeStringOrEmpty(row.ValidatedAt),
			FailedAt:            timeStringOrEmpty(row.FailedAt),
			CreatedAt:           row.CreatedAt.UTC().Format(time.RFC3339),
			CreatedBy:           uuidStringOrEmpty(row.CreatedBy),
			UpdatedAt:           row.UpdatedAt.UTC().Format(time.RFC3339),
			UpdatedBy:           uuidStringOrEmpty(row.UpdatedBy),
			DeletedAt:           timeStringOrEmpty(row.DeletedAt),
			DeletedBy:           uuidStringOrEmpty(row.DeletedBy),
			TrashedAt:           timeStringOrEmpty(row.TrashedAt),
			PurgeAt:             timeStringOrEmpty(row.PurgeAt),
		},
	}
}

func fileManagerFileDetailResource(ctx context.Context, storage filemanager.ObjectStorage, row *filemanager.File, isRootFile bool) responsedto.FileManagerFileDetailResource {
	urls := resolveFileManagerDownloadURLs(ctx, storage, row)
	detail := responsedto.FileManagerFileDetailResource{
		Type: "file_manager_file",
		ID:   idString(row.ID),
		Attributes: shareddto.FileManagerFileDetailAttributes{
			Name:           row.Name,
			NameNormalized: row.NameNormalized,
			Extension:      row.Extension,
			SizeBytes:      row.SizeBytes,
			MimeType:       row.MimeType,
			IsTemplate:     row.IsTemplate,
			DownloadURL:    urls.OriginalURL,
			PreviewURL:     urls.PreviewURL,
			Status:         row.Status,
			Audit: shareddto.FileManagerFileAuditAttributes{
				CreatedAt: row.CreatedAt.UTC().Format(time.RFC3339),
				CreatedBy: uuidStringOrEmpty(row.CreatedBy),
				UpdatedAt: row.UpdatedAt.UTC().Format(time.RFC3339),
				UpdatedBy: uuidStringOrEmpty(row.UpdatedBy),
			},
		},
	}

	if !isRootFile {
		folderID := strings.TrimSpace(idString(row.FolderID))
		if folderID != "" {
			detail.Attributes.Location = &shareddto.FileManagerFileLocationAttributes{
				FolderID: folderID,
			}
		}
	}

	processing := shareddto.FileManagerFileProcessingAttributes{
		ProcessingStartedAt: timeStringOrEmpty(row.ProcessingStartedAt),
		ProcessedAt:         timeStringOrEmpty(row.ProcessedAt),
		ValidatedAt:         timeStringOrEmpty(row.ValidatedAt),
		FailedAt:            timeStringOrEmpty(row.FailedAt),
		FailureReason:       firstNonEmpty(strings.TrimSpace(row.FailureReason), strings.TrimSpace(row.UploadError)),
	}
	if processing.ProcessingStartedAt != "" ||
		processing.ProcessedAt != "" ||
		processing.ValidatedAt != "" ||
		processing.FailedAt != "" ||
		processing.FailureReason != "" {
		detail.Attributes.Processing = &processing
	}

	return detail
}

func fileManagerFileListResource(ctx context.Context, storage filemanager.ObjectStorage, row *filemanager.File) responsedto.FileManagerFileListResource {
	return responsedto.FileManagerFileListResource{
		Type: "file_manager_file",
		ID:   idString(row.ID),
		Attributes: shareddto.FileManagerFileListAttributes{
			Name:           row.Name,
			NameNormalized: row.NameNormalized,
			Extension:      row.Extension,
			SizeBytes:      row.SizeBytes,
			MimeType:       row.MimeType,
			IsTemplate:     row.IsTemplate,
			DownloadURL:    resolveFileManagerDownloadURL(ctx, storage, row),
			Status:         row.Status,
			CreatedAt:      row.CreatedAt.UTC().Format(time.RFC3339),
			CreatedBy:      uuidStringOrEmpty(row.CreatedBy),
			UpdatedAt:      row.UpdatedAt.UTC().Format(time.RFC3339),
			UpdatedBy:      uuidStringOrEmpty(row.UpdatedBy),
		},
	}
}

func fileManagerFileUploadIntentResource(grant *filemanager.FileUploadIntentGrant) responsedto.FileManagerFileUploadIntentResource {
	if grant == nil {
		return responsedto.FileManagerFileUploadIntentResource{Type: "file_manager_file_upload_intent"}
	}

	expiresAt := grant.ExpiresAt.UTC()
	expiresAtRaw := ""
	expiresInSeconds := int64(0)
	if !expiresAt.IsZero() {
		expiresAtRaw = expiresAt.Format(time.RFC3339)
		expiresInSeconds = int64(time.Until(expiresAt).Seconds())
		if expiresInSeconds < 0 {
			expiresInSeconds = 0
		}
	}

	attributes := shareddto.FileManagerFileUploadIntentAttributes{
		UploadIntentID:  idString(grant.UploadIntentID),
		UploadURL:       strings.TrimSpace(grant.UploadURL),
		Method:          strings.ToUpper(strings.TrimSpace(grant.Method)),
		RequiredHeaders: sanitizeUploadRequiredHeaders(grant.Headers),
		Object: shareddto.FileUploadObject{
			Bucket: strings.TrimSpace(grant.Bucket),
			Key:    strings.TrimSpace(grant.ObjectKey),
		},
		File: shareddto.FileUploadFile{
			Name:      strings.TrimSpace(grant.Name),
			MimeType:  strings.TrimSpace(grant.MimeType),
			SizeBytes: grant.SizeBytes,
		},
		ExpiresAt:        expiresAtRaw,
		ExpiresInSeconds: expiresInSeconds,
	}

	return responsedto.FileManagerFileUploadIntentResource{
		Type:       "file_manager_file_upload_intent",
		ID:         idString(grant.UploadIntentID),
		Attributes: attributes,
	}
}

func sanitizeUploadRequiredHeaders(headers map[string]string) map[string]string {
	if len(headers) == 0 {
		return nil
	}
	out := make(map[string]string, len(headers))
	for key, value := range headers {
		k := strings.TrimSpace(strings.ToLower(key))
		if k == "" || k == "host" || k == "content-length" || k == "x-amz-meta-file_name" {
			continue
		}
		v := strings.TrimSpace(value)
		if v == "" {
			continue
		}
		out[k] = v
	}
	if len(out) == 0 {
		return nil
	}
	return out
}

func writeFileManagerBulkResponse(c *fiber.Ctx, items []responsedto.FileManagerFileBulkItem) error {
	normalizeFileManagerBulkItems(items)

	total := len(items)
	success := 0
	for i := range items {
		if items[i].Success {
			success++
		}
	}
	failed := total - success

	meta := map[string]any{
		"total":           total,
		"success":         success,
		"failed":          failed,
		"partial_success": success > 0 && failed > 0,
	}

	statusCode := fiber.StatusOK
	if failed == 0 {
		statusCode = fiber.StatusCreated
	} else if success > 0 {
		statusCode = fiber.StatusMultiStatus
	} else if success == 0 && total == 1 {
		if itemStatus := items[0].Error.Status; itemStatus >= 400 {
			statusCode = itemStatus
		}
	}

	return response.WriteWithMeta(c, statusCode, items, meta)
}

func writeFileManagerUploadIntentBulkResponse(c *fiber.Ctx, items []responsedto.FileManagerFileUploadIntentBulkItem) error {
	normalizeFileManagerUploadIntentBulkItems(items)

	total := len(items)
	success := 0
	for i := range items {
		if items[i].Success {
			success++
		}
	}
	failed := total - success

	meta := map[string]any{
		"total":           total,
		"success":         success,
		"failed":          failed,
		"partial_success": success > 0 && failed > 0,
	}

	statusCode := fiber.StatusOK
	if failed == 0 {
		statusCode = fiber.StatusCreated
	} else if success > 0 {
		statusCode = fiber.StatusMultiStatus
	} else if success == 0 && total == 1 {
		if itemStatus := items[0].Error.Status; itemStatus >= 400 {
			statusCode = itemStatus
		}
	}

	return response.WriteWithMeta(c, statusCode, items, meta)
}

func writeFileManagerCancelUploadBulkResponse(c *fiber.Ctx, items []responsedto.FileManagerFileCancelUploadBulkItem) error {
	normalizeFileManagerCancelUploadBulkItems(items)

	total := len(items)
	success := 0
	for i := range items {
		if items[i].Success {
			success++
		}
	}
	failed := total - success

	meta := map[string]any{
		"total":           total,
		"success":         success,
		"failed":          failed,
		"partial_success": success > 0 && failed > 0,
	}

	return response.WriteWithMeta(c, fiber.StatusOK, items, meta)
}

func normalizeFileManagerBulkItems(items []responsedto.FileManagerFileBulkItem) {
	for i := range items {
		if items[i].Success {
			items[i].Error = nil
			if items[i].Resource == nil {
				items[i].Success = false
				items[i].Message = ""
				items[i].Error = &responsedto.FileManagerFileBulkError{
					Status:    fiber.StatusInternalServerError,
					Code:      apperrorsx.CodeInternalServerError,
					ErrorCode: apperrorsx.ErrInternal.ErrorCode,
					Title:     "Internal error",
					Detail:    "missing success resource",
				}
			}
			continue
		}

		items[i].Resource = nil
		items[i].Message = ""
		if items[i].Error == nil {
			items[i].Error = &responsedto.FileManagerFileBulkError{
				Status:    fiber.StatusInternalServerError,
				Code:      apperrorsx.CodeInternalServerError,
				ErrorCode: apperrorsx.ErrInternal.ErrorCode,
				Title:     "Internal error",
				Detail:    "missing item error",
			}
		}
	}
}

func normalizeFileManagerUploadIntentBulkItems(items []responsedto.FileManagerFileUploadIntentBulkItem) {
	for i := range items {
		if items[i].Success {
			items[i].Error = nil
			if items[i].Resource == nil {
				items[i].Success = false
				items[i].Error = &responsedto.FileManagerFileBulkError{
					Status:    fiber.StatusInternalServerError,
					Code:      apperrorsx.CodeInternalServerError,
					ErrorCode: apperrorsx.ErrInternal.ErrorCode,
					Title:     "Internal error",
					Detail:    "missing success resource",
				}
			}
			continue
		}

		items[i].Resource = nil
		if items[i].Error == nil {
			items[i].Error = &responsedto.FileManagerFileBulkError{
				Status:    fiber.StatusInternalServerError,
				Code:      apperrorsx.CodeInternalServerError,
				ErrorCode: apperrorsx.ErrInternal.ErrorCode,
				Title:     "Internal error",
				Detail:    "missing item error",
			}
		}
	}
}

func normalizeFileManagerCancelUploadBulkItems(items []responsedto.FileManagerFileCancelUploadBulkItem) {
	for i := range items {
		if items[i].Success {
			items[i].Error = nil
			if items[i].Resource == nil {
				items[i].Success = false
				items[i].Error = &responsedto.FileManagerFileBulkError{
					Status:    fiber.StatusInternalServerError,
					Code:      apperrorsx.CodeInternalServerError,
					ErrorCode: apperrorsx.ErrInternal.ErrorCode,
					Title:     "Internal error",
					Detail:    "missing success resource",
				}
			}
			continue
		}

		items[i].Resource = nil
		if items[i].Error == nil {
			items[i].Error = &responsedto.FileManagerFileBulkError{
				Status:    fiber.StatusInternalServerError,
				Code:      apperrorsx.CodeInternalServerError,
				ErrorCode: apperrorsx.ErrInternal.ErrorCode,
				Title:     "Internal error",
				Detail:    "missing item error",
			}
		}
	}
}

func fileManagerBulkSuccessItem(index int, message string, row *filemanager.File) responsedto.FileManagerFileBulkItem {
	resource := fileManagerFileResource(row)
	return responsedto.FileManagerFileBulkItem{
		Index:    index,
		Success:  true,
		Message:  message,
		Resource: &resource,
	}
}

func fileManagerBulkSystemErrorItem(index int, status int, title, detail string) responsedto.FileManagerFileBulkItem {
	def := inferFileManagerDef(status, title, detail)
	return responsedto.FileManagerFileBulkItem{
		Index:    index,
		Success:  false,
		Resource: nil,
		Error: &responsedto.FileManagerFileBulkError{
			Status:    status,
			Code:      def.Code,
			ErrorCode: def.ErrorCode,
			Title:     def.Title,
			Detail:    def.Message,
		},
	}
}

func missionFileBulkErrorItem(index int, def apperrorsx.Def, detail string) responsedto.FileManagerFileBulkItem {
	msg := strings.TrimSpace(detail)
	if msg == "" {
		msg = def.Message
	}
	return responsedto.FileManagerFileBulkItem{
		Index:    index,
		Success:  false,
		Resource: nil,
		Error: &responsedto.FileManagerFileBulkError{
			Status:    def.Status,
			Code:      def.Code,
			ErrorCode: def.ErrorCode,
			Title:     def.Title,
			Detail:    msg,
		},
	}
}

func fileManagerBulkActionErrorItem(index int, action string, err error) responsedto.FileManagerFileBulkItem {
	def := mapFileManagerActionDef(action, err)
	statusCode := def.Status
	item := fileManagerBulkSystemErrorItem(index, statusCode, def.Title, def.Message)
	if item.Error != nil {
		item.Error.Code = def.Code
		item.Error.ErrorCode = def.ErrorCode
	}
	return item
}

func fileManagerUploadIntentBulkSystemErrorItem(index int, status int, title, detail string) responsedto.FileManagerFileUploadIntentBulkItem {
	def := inferFileManagerDef(status, title, detail)
	return responsedto.FileManagerFileUploadIntentBulkItem{
		Index:    index,
		Success:  false,
		Resource: nil,
		Error: &responsedto.FileManagerFileBulkError{
			Status:    status,
			Code:      def.Code,
			ErrorCode: def.ErrorCode,
			Title:     def.Title,
			Detail:    def.Message,
		},
	}
}

func fileManagerUploadIntentBulkActionErrorItem(index int, action string, err error) responsedto.FileManagerFileUploadIntentBulkItem {
	def := mapFileManagerActionDef(action, err)
	statusCode := def.Status
	title := def.Title
	detail := def.Message
	item := fileManagerUploadIntentBulkSystemErrorItem(index, statusCode, title, detail)
	if item.Error != nil {
		item.Error.Code = def.Code
		item.Error.ErrorCode = def.ErrorCode
	}
	return item
}

func fileManagerCancelUploadBulkSuccessItem(index int, uploadIntentID []byte) responsedto.FileManagerFileCancelUploadBulkItem {
	resource := responsedto.FileManagerFileCancelUploadResource{
		Type: "file_manager_file_cancel_upload_result",
		ID:   idString(uploadIntentID),
	}
	resource.Attributes.Status = "canceled"

	return responsedto.FileManagerFileCancelUploadBulkItem{
		Index:    index,
		Success:  true,
		Resource: &resource,
	}
}

func fileManagerCancelUploadBulkSystemErrorItem(index int, status int, title, detail string) responsedto.FileManagerFileCancelUploadBulkItem {
	def := inferFileManagerDef(status, title, detail)
	return responsedto.FileManagerFileCancelUploadBulkItem{
		Index:    index,
		Success:  false,
		Resource: nil,
		Error: &responsedto.FileManagerFileBulkError{
			Status:    status,
			Code:      def.Code,
			ErrorCode: def.ErrorCode,
			Title:     def.Title,
			Detail:    def.Message,
		},
	}
}

func fileManagerCancelUploadBulkActionErrorItem(index int, action string, err error) responsedto.FileManagerFileCancelUploadBulkItem {
	def := mapFileManagerActionDef(action, err)
	statusCode := def.Status
	title := def.Title
	detail := def.Message
	item := fileManagerCancelUploadBulkSystemErrorItem(index, statusCode, title, detail)
	if item.Error != nil {
		item.Error.Code = def.Code
		item.Error.ErrorCode = def.ErrorCode
	}
	return item
}

func (h *FileManagerFileHandler) parseUUIDParam(c *fiber.Ctx, paramName, sourcePointer string) ([]byte, error) {
	uuidStr := c.Params(paramName)
	id, err := uuidv7.ParseString(uuidStr)
	if err != nil {
		return nil, writeFileManagerError(c, fiber.StatusUnprocessableEntity, "Validation error", fmt.Sprintf("%s is invalid UUID", paramName), &jsonapi.ErrorSource{Pointer: sourcePointer})
	}
	return id, nil
}

func parseFileManagerFilePagination(c *fiber.Ctx) (pageNumber int, pageSize int, offset int) {
	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
	return
}

func firstNonEmpty(values ...string) string {
	for i := range values {
		if strings.TrimSpace(values[i]) != "" {
			return values[i]
		}
	}
	return ""
}
