package handlers

import (
	"bytes"
	"context"
	"errors"
	"io"
	"path/filepath"
	"strconv"
	"strings"
	"time"

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

	filemanager "wucher/internal/domain/file_manager"
	"wucher/internal/domain/helicopter"
	helicopterfile "wucher/internal/domain/helicopter_file"
	takeoverdomain "wucher/internal/domain/takeover"
	helicopterfilerealtime "wucher/internal/realtime/helicopterfile"
	"wucher/internal/shared/pkg/apperrorsx"
	"wucher/internal/shared/pkg/uuidv7"
	requestdto "wucher/internal/transport/http/dto/request"
	responsedto "wucher/internal/transport/http/dto/response"
	"wucher/internal/transport/http/jsonapi"
	"wucher/internal/transport/http/response"
	"wucher/internal/transport/http/validators"
)

const (
	helicopterFileBulkMaxItems = 20
)

var (
	_ responsedto.HelicopterFileResponse
	_ responsedto.HelicopterFileListResponse
)

type HelicopterFileHandler struct {
	svc                  helicopterfile.Service
	helicopter           helicopter.Service
	takeover             helicopterFileTakeoverLookup
	fileManager          filemanager.Service
	helicopterFileEvents helicopterFileEventPublisher
	storage              fileManagerDownloadURLStorage
	validate             *validators.Validator
}

type helicopterFileTakeoverLookup interface {
	GetByID(ctx context.Context, id []byte) (*takeoverdomain.TakeoverAc, error)
}

func NewHelicopterFileHandler(svc helicopterfile.Service) *HelicopterFileHandler {
	return &HelicopterFileHandler{
		svc:      svc,
		validate: validators.New(),
	}
}

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

func (h *HelicopterFileHandler) WithHelicopterService(helicopterSvc helicopter.Service) *HelicopterFileHandler {
	h.helicopter = helicopterSvc
	return h
}

func (h *HelicopterFileHandler) WithTakeoverService(takeoverSvc helicopterFileTakeoverLookup) *HelicopterFileHandler {
	h.takeover = takeoverSvc
	return h
}

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

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

// UploadHelicopterFile godoc
// @Summary Request upload intents + presigned PUT URLs (helicopter file)
// @Description Control-plane endpoint for helicopter file upload intent + presigned URL flow.
// @Description Public flow: POST /api/v1/helicopter-files/upload -> PUT to presigned URL -> POST /api/v1/helicopter-files/create.
// @Description Canonical type is `helicopter_file_upload`.
// @Description `required_headers` must be sent exactly as returned when doing PUT to `upload_url`.
// @Description Example `required_headers`: `{"content-type":"application/pdf","x-amz-acl":"private"}`
// @Description Supports single and bulk payload.
// @Description Single payload example: `{"data":{"type":"helicopter_file_upload","attributes":{"name":"A.pdf","content_type":"application/pdf","size_bytes":12345}}}`
// @Description Bulk payload example: `{"data":[{"type":"helicopter_file_upload","attributes":{"name":"A.pdf","content_type":"application/pdf","size_bytes":12345}}]}`
// @Description Note: OpenAPI 2.0 cannot express object-or-array union in one schema; request schema below represents bulk envelope.
// @Tags Helicopter - Master File
// @Accept json
// @Produce json
// @Param request body requestdto.HelicopterFileUploadBulkRequest true "Helicopter file upload intent request (single object or bulk array)"
// @Success 201 {object} responsedto.HelicopterFileUploadIntentResponse
// @Success 200 {object} responsedto.HelicopterFileUploadIntentBulkResponse
// @Success 207 {object} responsedto.HelicopterFileUploadIntentBulkResponse
// @Failure 400 {object} jsonapi.ErrorDocument
// @Failure 422 {object} jsonapi.ErrorDocument
// @Router /api/v1/helicopter-files/upload [post]
func (h *HelicopterFileHandler) Upload(c *fiber.Ctx) error {
	if h.fileManager == nil {
		writeHelicopterFileBadRequestError(c, "Upload failed", "file manager service is not configured")
		return nil
	}
	isBulk, err := isJSONAPIDataArrayPayload(c.Body())
	if err != nil {
		return writeHelicopterFileErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "Invalid JSON",
			Detail: safeInternalDetail(),
		}})
	}
	if isBulk {
		return h.uploadBulk(c)
	}

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

	typ := strings.TrimSpace(req.Data.Type)
	if typ != "helicopter_file_upload" {
		return writeHelicopterFileErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
			Status: "422",
			Title:  "Validation error",
			Detail: "type must be helicopter_file_upload",
			Source: &jsonapi.ErrorSource{Pointer: "/data/type"},
		}})
	}

	name := strings.TrimSpace(req.Data.Attributes.Name)
	contentType := strings.TrimSpace(req.Data.Attributes.ContentType)
	sizeBytes := req.Data.Attributes.SizeBytes

	grant, grantErr := h.fileManager.RequestFileUploadIntent(c.Context(), filemanager.RequestFileUploadIntentParams{
		Name:        name,
		ContentType: contentType,
		SizeBytes:   sizeBytes,
		ActorID:     actorUserID(c),
	})
	if grantErr != nil {
		return writeFileManagerActionError(c, "Upload intent failed", grantErr)
	}

	baseResource := fileManagerFileUploadIntentResource(grant)
	resource := responsedto.HelicopterFileUploadIntentResource{
		Type:       "helicopter_file_upload_intent",
		ID:         baseResource.ID,
		Attributes: baseResource.Attributes,
	}
	return response.Write(c, fiber.StatusCreated, resource)
}

func (h *HelicopterFileHandler) uploadBulk(c *fiber.Ctx) error {
	var req requestdto.HelicopterFileUploadBulkRequest
	if err := c.BodyParser(&req); err != nil {
		return writeHelicopterFileErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "Invalid JSON",
			Detail: safeInternalDetail(),
		}})
	}
	if len(req.Data) == 0 {
		return writeHelicopterFileErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
			Status: "422",
			Title:  "Validation error",
			Detail: "data must contain at least one item",
			Source: &jsonapi.ErrorSource{Pointer: "/data"},
		}})
	}
	if len(req.Data) > helicopterFileBulkMaxItems {
		return writeHelicopterFileErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
			Status: "422",
			Title:  "Validation error",
			Detail: "too many items in one request",
			Source: &jsonapi.ErrorSource{Pointer: "/data"},
		}})
	}

	inputs := make([]helicopterfile.UploadIntentBulkInput, len(req.Data))
	for i := range req.Data {
		item := req.Data[i]
		inputs[i] = helicopterfile.UploadIntentBulkInput{
			Type:        strings.TrimSpace(item.Type),
			Name:        strings.TrimSpace(item.Attributes.Name),
			ContentType: strings.TrimSpace(item.Attributes.ContentType),
			SizeBytes:   item.Attributes.SizeBytes,
		}
	}

	results := h.svc.RequestUploadIntentsBulk(c.Context(), inputs, actorUserID(c))
	items := make([]responsedto.HelicopterFileUploadIntentBulkItem, len(results))
	for i := range results {
		result := results[i]
		if result.Success && result.Grant != nil {
			baseResource := fileManagerFileUploadIntentResource(result.Grant)
			resource := responsedto.HelicopterFileUploadIntentResource{
				Type:       "helicopter_file_upload_intent",
				ID:         baseResource.ID,
				Attributes: baseResource.Attributes,
			}
			items[i] = responsedto.HelicopterFileUploadIntentBulkItem{
				Index:    result.Index,
				Success:  true,
				Resource: &resource,
			}
			continue
		}

		errStatus := fiber.StatusInternalServerError
		errTitle := "Internal error"
		errDetail := "missing item error"
		if result.Error != nil {
			errStatus = result.Error.Status
			errTitle = result.Error.Title
			errDetail = result.Error.Detail
		}
		items[i] = helicopterFileUploadBulkErrorItem(result.Index, errStatus, errTitle, errDetail)
	}

	return writeHelicopterFileUploadIntentBulkResponse(c, items)
}

// CancelUploadHelicopterFile godoc
// @Summary Cancel helicopter file upload intent (bulk)
// @Description Cancel pending upload intents from helicopter file upload flow.
// @Description Bulk payload accepts one or many items.
// @Tags Helicopter - Master File
// @Accept json
// @Produce json
// @Param request body requestdto.HelicopterFileCancelUploadBulkRequest true "Bulk helicopter file cancel upload intent request"
// @Success 200 {object} responsedto.FileManagerFileCancelUploadBulkResponse
// @Failure 400 {object} jsonapi.ErrorDocument
// @Failure 422 {object} jsonapi.ErrorDocument
// @Router /api/v1/helicopter-files/cancel-upload [post]
func (h *HelicopterFileHandler) CancelUpload(c *fiber.Ctx) error {
	if h.fileManager == nil {
		writeHelicopterFileBadRequestError(c, "Cancel upload failed", "file manager service is not configured")
		return nil
	}

	isBulk, err := isJSONAPIDataArrayPayload(c.Body())
	if err != nil {
		return writeHelicopterFileErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "Invalid JSON",
			Detail: safeInternalDetail(),
		}})
	}
	if !isBulk {
		return writeHelicopterFileErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
			Status: "422",
			Title:  "Validation error",
			Detail: "data must be an array",
			Source: &jsonapi.ErrorSource{Pointer: "/data"},
		}})
	}

	var req requestdto.HelicopterFileCancelUploadBulkRequest
	if err := c.BodyParser(&req); err != nil {
		return writeHelicopterFileErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "Invalid JSON",
			Detail: safeInternalDetail(),
		}})
	}
	if len(req.Data) == 0 {
		return writeHelicopterFileErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
			Status: "422",
			Title:  "Validation error",
			Detail: "data must contain at least one item",
			Source: &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) != "helicopter_file_cancel_upload" {
			items[i] = fileManagerCancelUploadBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "type must be helicopter_file_cancel_upload")
			continue
		}

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

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

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

	return writeFileManagerCancelUploadBulkResponse(c, items)
}

// CreateHelicopterFile godoc
// @Summary Create helicopter file
// @Description Create helicopter file template record from upload intent.
// @Description Section allowed values: `bfi` (Before First Flight Inspection), `fpwb` (Flight Preparation and WB), `afi` (After Last Flight Inspection).
// @Description Required attributes: `section`, `helicopter_id`, `upload_intent_uuid`, `file_name`.
// @Description Optional attributes: `position`, `is_required`, `folder_uuid` (if omitted, file is created in root folder).
// @Description When `position` is omitted (or <= 0), backend auto-assigns next position per `helicopter_id + section`.
// @Description If file name already exists in target folder, existing file is reused and attached.
// @Description Supports single and bulk payload.
// @Description Single payload example: `{"data":{"type":"helicopter_file_create","attributes":{"section":"bfi","position":1,"is_required":true,"helicopter_id":"019d6771-5fb5-7337-837f-bc5ed85181a1","upload_intent_uuid":"019d6d0d-2de2-7db1-80a2-2862d43af620","file_name":"A.pdf"}}}`
// @Description Bulk payload example: `{"data":[{"type":"helicopter_file_create","attributes":{"section":"bfi","position":1,"is_required":true,"helicopter_id":"019d6771-5fb5-7337-837f-bc5ed85181a1","upload_intent_uuid":"019d6d0d-2de2-7db1-80a2-2862d43af620","file_name":"A.pdf"}}]}`
// @Description Note: OpenAPI 2.0 cannot express object-or-array union in one schema; request schema below represents bulk envelope.
// @Tags Helicopter - Master File
// @Accept json
// @Produce json
// @Param request body requestdto.HelicopterFileCreateBulkRequest true "JSON:API helicopter file create request (single object or bulk array)"
// @Success 201 {object} responsedto.HelicopterFileResponse
// @Success 200 {object} responsedto.HelicopterFileCreateBulkResponse
// @Success 207 {object} responsedto.HelicopterFileCreateBulkResponse
// @Failure 400 {object} jsonapi.ErrorDocument
// @Failure 422 {object} jsonapi.ErrorDocument
// @Router /api/v1/helicopter-files/create [post]
func (h *HelicopterFileHandler) Create(c *fiber.Ctx) error {
	isBulk, err := isJSONAPIDataArrayPayload(c.Body())
	if err != nil {
		return writeHelicopterFileErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "Invalid JSON",
			Detail: safeInternalDetail(),
		}})
	}
	if isBulk {
		return h.createBulk(c)
	}

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

	section := strings.TrimSpace(req.Data.Attributes.Section)
	if !isValidHelicopterFileSection(section) {
		return writeHelicopterFileErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
			Status: "422",
			Title:  "Validation error",
			Detail: "section is invalid",
			Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/section"},
		}})
	}

	attrs := req.Data.Attributes
	helicopterRaw := strings.TrimSpace(attrs.HelicopterID)
	if helicopterRaw == "" {
		return writeHelicopterFileErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
			Status: "422",
			Title:  "Validation error",
			Detail: "helicopter_id is required",
			Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/helicopter_id"},
		}})
	}
	helicopterID, err := uuidv7.ParseString(helicopterRaw)
	if err != nil {
		return writeHelicopterFileErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
			Status: "422",
			Title:  "Validation error",
			Detail: "helicopter_id is invalid UUID",
			Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/helicopter_id"},
		}})
	}
	exists, err := h.svc.HelicopterExists(c.UserContext(), helicopterID)
	if err != nil {
		return writeHelicopterFileErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "Create failed",
			Detail: safeInternalDetail(),
		}})
	}
	if !exists {
		return writeHelicopterFileErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
			Status: "422",
			Title:  "Validation error",
			Detail: "helicopter_id not found",
			Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/helicopter_id"},
		}})
	}

	uploadIntentUUID := strings.TrimSpace(attrs.UploadIntentUUID)
	fileName := strings.TrimSpace(attrs.FileName)
	uploadIntentID, parseErr := uuidv7.ParseString(uploadIntentUUID)
	if parseErr != nil {
		writeHelicopterFileValidationError(c, "upload_intent_uuid is invalid UUID", "/data/attributes/upload_intent_uuid")
		return nil
	}
	if fileName == "" {
		writeHelicopterFileValidationError(c, "file_name is required", "/data/attributes/file_name")
		return nil
	}
	if h.fileManager == nil {
		writeHelicopterFileBadRequestError(c, "Create failed", "file manager service is not configured")
		return nil
	}

	var folderID []byte
	folderProvided := false
	if attrs.FolderUUID != nil {
		folderProvided = true
		folderUUID := strings.TrimSpace(*attrs.FolderUUID)
		if folderUUID != "" {
			parsedFolderID, folderErr := uuidv7.ParseString(folderUUID)
			if folderErr != nil {
				writeHelicopterFileValidationError(c, "folder_uuid is invalid UUID", "/data/attributes/folder_uuid")
				return nil
			}
			folderID = parsedFolderID
		}
	} else {
		root, rootErr := h.fileManager.GetOrCreateSystemRootFolder(c.UserContext())
		if rootErr == nil && root != nil && len(root.ID) == 16 {
			folderID = root.ID
			folderProvided = true
		}
	}
	reusedExistingFile := false
	createdFile, createErr := h.fileManager.CreateFileNodeFromUploadIntent(c.UserContext(), filemanager.CreateFileFromUploadIntentParams{
		UploadIntentID: uploadIntentID,
		FolderID:       folderID,
		FolderProvided: folderProvided,
		Name:           fileName,
		ActorID:        actorUserID(c),
		IsTemplate:     true,
	})
	if createErr != nil {
		if errors.Is(createErr, filemanager.ErrDuplicateName) {
			reusedExistingFile = true
			targetFolderID := folderID
			if !folderProvided || len(targetFolderID) == 0 {
				root, rootErr := h.fileManager.GetOrCreateSystemRootFolder(c.UserContext())
				if rootErr != nil || root == nil {
					detail := "unable to resolve target folder"
					if rootErr != nil {
						detail = rootErr.Error()
					}
					writeHelicopterFileBadRequestError(c, "Create failed", detail)
					return nil
				}
				targetFolderID = root.ID
			}

			nameNormalized := strings.ToLower(filepath.Base(strings.TrimSpace(fileName)))
			if nameNormalized == "" {
				writeHelicopterFileValidationError(c, "file_name is invalid", "/data/attributes/file_name")
				return nil
			}
			existingFile, existingErr := h.fileManager.GetFileByFolderAndName(c.UserContext(), targetFolderID, nameNormalized)
			if existingErr != nil || existingFile == nil {
				detail := "duplicate file exists but cannot be resolved"
				if existingErr != nil {
					detail = existingErr.Error()
				}
				writeHelicopterFileBadRequestError(c, "Create failed", detail)
				return nil
			}
			createdFile = existingFile
		} else {
			writeHelicopterFileBadRequestError(c, "Create failed", createErr.Error())
			return nil
		}
	}
	if createdFile == nil {
		writeHelicopterFileBadRequestError(c, "Create failed", "unable to create file from upload intent")
		return nil
	}

	if err := h.validateStoredHelicopterFileContent(c.UserContext(), createdFile); err != nil {
		// Freshly uploaded corrupt files are removed; a reused existing template is left
		// intact (it may be referenced elsewhere) and only reported.
		if !reusedExistingFile {
			_ = h.fileManager.DeleteFile(c.UserContext(), createdFile.ID, actorUserID(c), nil)
		}
		return writeHelicopterFileErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
			Status: "422",
			Title:  "Validation error",
			Detail: "uploaded file content is corrupt or unsupported",
			Source: &jsonapi.ErrorSource{Pointer: "/data/attributes/upload_intent_uuid"},
		}})
	}

	row := &helicopterfile.HelicopterFile{
		Section:      section,
		Position:     intOrDefault(attrs.Position, 0),
		IsMandatory:  boolOrDefault(attrs.IsMandatory, false),
		HelicopterID: helicopterID,
		SourceFileID: append([]byte(nil), createdFile.ID...),
		CreatedBy:    actorUserID(c),
		UpdatedBy:    actorUserID(c),
	}
	if err := h.svc.Create(c.UserContext(), row); err != nil {
		return writeHelicopterFileErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "Create failed",
			Detail: safeInternalDetail(),
		}})
	}
	h.publishHelicopterFileCreated(actorUserID(c), row, createdFile)

	createdRow, getErr := h.svc.GetByID(c.UserContext(), row.ID)
	if getErr == nil && createdRow != nil {
		return response.Write(c, fiber.StatusCreated, helicopterFileResource(c.UserContext(), h.storage, createdRow))
	}

	return response.Write(c, fiber.StatusCreated, helicopterFileResource(c.UserContext(), h.storage, row))
}

func (h *HelicopterFileHandler) createBulk(c *fiber.Ctx) error {
	var req requestdto.HelicopterFileCreateBulkRequest
	if err := c.BodyParser(&req); err != nil {
		return writeHelicopterFileErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "Invalid JSON",
			Detail: safeInternalDetail(),
		}})
	}
	if len(req.Data) == 0 {
		return writeHelicopterFileErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
			Status: "422",
			Title:  "Validation error",
			Detail: "data must contain at least one item",
			Source: &jsonapi.ErrorSource{Pointer: "/data"},
		}})
	}
	if len(req.Data) > helicopterFileBulkMaxItems {
		return writeHelicopterFileErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
			Status: "422",
			Title:  "Validation error",
			Detail: "too many items in one request",
			Source: &jsonapi.ErrorSource{Pointer: "/data"},
		}})
	}

	inputs := make([]helicopterfile.CreateBulkInput, len(req.Data))
	for i := range req.Data {
		item := req.Data[i]
		inputs[i] = helicopterfile.CreateBulkInput{
			Type:             strings.TrimSpace(item.Type),
			Section:          strings.TrimSpace(item.Attributes.Section),
			Position:         item.Attributes.Position,
			IsMandatory:      item.Attributes.IsMandatory,
			HelicopterID:     strings.TrimSpace(item.Attributes.HelicopterID),
			UploadIntentUUID: strings.TrimSpace(item.Attributes.UploadIntentUUID),
			FileName:         strings.TrimSpace(item.Attributes.FileName),
			FolderUUID:       item.Attributes.FolderUUID,
		}
	}

	if h.fileManager != nil {
		rootCache := make(map[string][]byte)
		for i := range inputs {
			if inputs[i].FolderUUID != nil {
				continue
			}
			rootKey := "root"
			rootID, ok := rootCache[rootKey]
			if !ok {
				root, rootErr := h.fileManager.GetOrCreateSystemRootFolder(c.UserContext())
				if rootErr != nil || root == nil || len(root.ID) != 16 {
					continue
				}
				rootID = append([]byte(nil), root.ID...)
				rootCache[rootKey] = rootID
			}
			folderStr, strErr := uuidv7.BytesToString(rootID)
			if strErr != nil {
				continue
			}
			inputs[i].FolderUUID = &folderStr
		}
	}

	results := h.svc.CreateFromUploadIntentsBulk(c.UserContext(), inputs, actorUserID(c))
	items := make([]responsedto.HelicopterFileCreateBulkItem, len(results))
	for i := range results {
		result := results[i]
		if result.Success && result.Row != nil {
			if err := h.validateStoredHelicopterFileContent(c.UserContext(), result.Row.SourceFile); err != nil {
				// Reject corrupt uploads: drop the helicopter file row we just created and
				// report the item as failed. The now-unreferenced file is reclaimed by GC.
				_ = h.svc.Delete(c.UserContext(), result.Row.ID)
				items[i] = helicopterFileCreateBulkErrorItem(
					result.Index,
					fiber.StatusUnprocessableEntity,
					"Validation error",
					"uploaded file content is corrupt or unsupported",
				)
				continue
			}
			h.publishHelicopterFileCreated(actorUserID(c), result.Row, result.Row.SourceFile)
			res := helicopterFileResource(c.UserContext(), h.storage, result.Row)
			items[i] = responsedto.HelicopterFileCreateBulkItem{
				Index:    result.Index,
				Success:  true,
				Resource: &res,
			}
			continue
		}

		errStatus := fiber.StatusInternalServerError
		errTitle := "Internal error"
		errDetail := "missing item error"
		if result.Error != nil {
			errStatus = result.Error.Status
			errTitle = result.Error.Title
			errDetail = result.Error.Detail
		}
		items[i] = helicopterFileCreateBulkErrorItem(result.Index, errStatus, errTitle, errDetail)
	}

	return writeHelicopterFileCreateBulkResponse(c, items)
}

// UpdateHelicopterFile godoc
// @Summary Update helicopter file
// @Description Update a helicopter file template record.
// @Description Section allowed values: `bfi` (Before First Flight Inspection), `fpwb` (Flight Preparation and WB), `afi` (After Last Flight Inspection).
// @Description Optional attributes: `section`, `position`, `is_required`, `helicopter_id`, `file_name`.
// @Tags Helicopter - Master File
// @Accept json
// @Produce json
// @Param request body requestdto.HelicopterFileUpdateBulkRequest true "JSON:API helicopter file update request (single object or bulk array)"
// @Success 200 {object} responsedto.HelicopterFileResponse
// @Success 207 {object} responsedto.HelicopterFileUpdateBulkResponse
// @Failure 400 {object} jsonapi.ErrorDocument
// @Failure 404 {object} jsonapi.ErrorDocument
// @Failure 422 {object} jsonapi.ErrorDocument
// @Router /api/v1/helicopter-files/update [patch]
func (h *HelicopterFileHandler) Update(c *fiber.Ctx) error {
	isBulk, err := isJSONAPIDataArrayPayload(c.Body())
	if err != nil {
		return writeHelicopterFileErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "Invalid JSON",
			Detail: safeInternalDetail(),
		}})
	}
	if isBulk {
		return h.updateBulk(c)
	}

	var req requestdto.HelicopterFileUpdateRequest
	if err := c.BodyParser(&req); err != nil {
		return writeHelicopterFileErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "Invalid JSON",
			Detail: safeInternalDetail(),
		}})
	}
	if errs := h.validate.ValidateStruct(req); errs != nil {
		return writeHelicopterFileErrors(c, fiber.StatusUnprocessableEntity, errs)
	}
	resource, errItem := h.updateOne(c, req.Data)
	if errItem != nil {
		return writeHelicopterFileErrors(c, errItem.Status, []jsonapi.ErrorObject{{
			Status: strconv.Itoa(errItem.Status),
			Title:  errItem.Title,
			Detail: errItem.Detail,
		}})
	}
	return response.Write(c, fiber.StatusOK, *resource)
}

func (h *HelicopterFileHandler) updateBulk(c *fiber.Ctx) error {
	var req requestdto.HelicopterFileUpdateBulkRequest
	if err := c.BodyParser(&req); err != nil {
		return writeHelicopterFileErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "Invalid JSON",
			Detail: safeInternalDetail(),
		}})
	}
	if len(req.Data) == 0 {
		return writeHelicopterFileErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
			Status: "422",
			Title:  "Validation error",
			Detail: "data must contain at least one item",
			Source: &jsonapi.ErrorSource{Pointer: "/data"},
		}})
	}
	if len(req.Data) > helicopterFileBulkMaxItems {
		return writeHelicopterFileErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
			Status: "422",
			Title:  "Validation error",
			Detail: "too many items in one request",
			Source: &jsonapi.ErrorSource{Pointer: "/data"},
		}})
	}

	items := make([]responsedto.HelicopterFileUpdateBulkItem, len(req.Data))
	for i := range req.Data {
		itemReq := requestdto.HelicopterFileUpdateRequest{Data: req.Data[i]}
		if errs := h.validate.ValidateStruct(itemReq); errs != nil {
			detail := "invalid request item"
			if len(errs) > 0 && strings.TrimSpace(errs[0].Detail) != "" {
				detail = errs[0].Detail
			}
			items[i] = helicopterFileUpdateBulkErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", detail)
			continue
		}

		resource, errItem := h.updateOne(c, req.Data[i])
		if errItem != nil {
			items[i] = helicopterFileUpdateBulkErrorItem(i, errItem.Status, errItem.Title, errItem.Detail)
			continue
		}

		items[i] = responsedto.HelicopterFileUpdateBulkItem{
			Index:    i,
			Success:  true,
			Resource: resource,
		}
	}

	return writeHelicopterFileUpdateBulkResponse(c, items)
}

func (h *HelicopterFileHandler) updateOne(c *fiber.Ctx, data requestdto.HelicopterFileUpdateData) (*jsonapi.Resource, *responsedto.HelicopterFileBulkError) {
	id, err := uuidv7.ParseString(strings.TrimSpace(data.ID))
	if err != nil {
		return nil, helicopterFileBulkErrorFromDef(fiber.StatusUnprocessableEntity, "Validation error", "id is invalid UUID")
	}
	existing, err := h.svc.GetByID(c.UserContext(), id)
	if err != nil {
		return nil, helicopterFileBulkErrorFromError("Get failed", err)
	}
	if existing == nil {
		return nil, helicopterFileBulkErrorFromDef(fiber.StatusNotFound, "Not found", "helicopter file not found")
	}
	prevAttachmentID := append([]byte(nil), existing.FileAttachmentID...)

	if data.Attributes.Section != nil {
		section := strings.TrimSpace(*data.Attributes.Section)
		if !isValidHelicopterFileSection(section) {
			return nil, helicopterFileBulkErrorFromDef(fiber.StatusUnprocessableEntity, "Validation error", "section is invalid")
		}
		existing.Section = section
	}
	if data.Attributes.Position != nil {
		existing.Position = *data.Attributes.Position
	}
	if data.Attributes.IsMandatory != nil {
		existing.IsMandatory = *data.Attributes.IsMandatory
	}

	helicopterIDRaw := strings.TrimSpace(optionalStringValue(data.Attributes.HelicopterID))
	if helicopterIDRaw != "" {
		helicopterID, parseErr := uuidv7.ParseString(helicopterIDRaw)
		if parseErr != nil {
			return nil, helicopterFileBulkErrorFromDef(fiber.StatusUnprocessableEntity, "Validation error", "helicopter_id is invalid UUID")
		}
		exists, existsErr := h.svc.HelicopterExists(c.UserContext(), helicopterID)
		if existsErr != nil {
			return nil, helicopterFileBulkErrorFromError("Update failed", existsErr)
		}
		if !exists {
			return nil, helicopterFileBulkErrorFromDef(fiber.StatusUnprocessableEntity, "Validation error", "helicopter_id not found")
		}
		existing.HelicopterID = helicopterID
	}

	if data.Attributes.FileName != nil {
		fileName := strings.TrimSpace(*data.Attributes.FileName)
		if fileName == "" {
			return nil, helicopterFileBulkErrorFromDef(fiber.StatusUnprocessableEntity, "Validation error", "file_name is required")
		}
		if h.fileManager == nil {
			return nil, helicopterFileBulkErrorFromDef(fiber.StatusBadRequest, "Update failed", "file manager service is not configured")
		}
		fileID := append([]byte(nil), existing.SourceFileID...)
		if len(fileID) != 16 {
			attachment, attachmentErr := h.fileManager.GetAttachmentByID(c.UserContext(), existing.FileAttachmentID)
			if attachmentErr != nil {
				return nil, helicopterFileBulkErrorFromError("Update failed", attachmentErr)
			}
			if attachment == nil || len(attachment.FileID) == 0 {
				return nil, helicopterFileBulkErrorFromDef(fiber.StatusNotFound, "Not found", "file attachment not found")
			}
			fileID = attachment.FileID
		}
		if _, moveErr := h.fileManager.MoveFileNode(c.UserContext(), fileID, filemanager.MoveFileParams{
			Name:    &fileName,
			ActorID: actorUserID(c),
		}); moveErr != nil {
			return nil, helicopterFileBulkErrorFromError("Update failed", moveErr)
		}
	}

	existing.UpdatedBy = actorUserID(c)
	if err := h.svc.Update(c.UserContext(), existing); err != nil {
		return nil, helicopterFileBulkErrorFromError("Update failed", err)
	}
	if err := syncAttachmentLifecycleSwap(c.UserContext(), h.fileManager, prevAttachmentID, existing.FileAttachmentID, actorUserID(c)); err != nil {
		return nil, helicopterFileBulkErrorFromError("Update failed", err)
	}

	updatedRow, getErr := h.svc.GetByID(c.UserContext(), existing.ID)
	if getErr == nil && updatedRow != nil {
		res := helicopterFileResource(c.UserContext(), h.storage, updatedRow)
		return &res, nil
	}

	res := helicopterFileResource(c.UserContext(), h.storage, existing)
	return &res, nil
}

func helicopterFileBulkErrorFromDef(status int, title, detail string) *responsedto.HelicopterFileBulkError {
	def := inferHelicopterFileDef(status, title, detail)
	return &responsedto.HelicopterFileBulkError{
		Status:    status,
		Code:      def.Code,
		ErrorCode: def.ErrorCode,
		Title:     def.Title,
		Detail:    def.Message,
	}
}

func helicopterFileBulkErrorFromError(title string, err error) *responsedto.HelicopterFileBulkError {
	def := inferHelicopterFileDef(fiber.StatusBadRequest, title, "")
	if errors.Is(err, filemanager.ErrFileNotFound) {
		def = inferHelicopterFileDef(fiber.StatusNotFound, title, "not found")
	}
	if errors.Is(err, filemanager.ErrDuplicateName) {
		def = inferHelicopterFileDef(fiber.StatusUnprocessableEntity, "Validation error", "duplicate name")
	}
	return &responsedto.HelicopterFileBulkError{
		Status:    def.Status,
		Code:      def.Code,
		ErrorCode: def.ErrorCode,
		Title:     def.Title,
		Detail:    def.Message,
	}
}

// GetHelicopterFile godoc
// @Summary Get helicopter files by helicopter ID
// @Description Return helicopter file template collection for selected helicopter
// @Tags Helicopter - Master File
// @Produce json
// @Param helicopter_id path string true "Helicopter ID"
// @Success 200 {object} responsedto.HelicopterFileCollectionResponse
// @Failure 400 {object} jsonapi.ErrorDocument
// @Failure 404 {object} jsonapi.ErrorDocument
// @Failure 422 {object} jsonapi.ErrorDocument
// @Router /api/v1/helicopter-files/get/{helicopter_id} [get]
func (h *HelicopterFileHandler) Get(c *fiber.Ctx) error {
	helicopterID := strings.TrimSpace(c.Params("helicopter_id"))
	id, err := uuidv7.ParseString(helicopterID)
	if err != nil {
		return writeHelicopterFileErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
			Status: "422",
			Title:  "Validation error",
			Detail: "helicopter_id is invalid UUID",
			Source: &jsonapi.ErrorSource{Pointer: "/path/helicopter_id"},
		}})
	}

	exists, err := h.svc.HelicopterExists(c.UserContext(), id)
	if err != nil {
		return writeHelicopterFileErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "Get failed",
			Detail: safeInternalDetail(),
		}})
	}
	if !exists {
		return writeHelicopterFileErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{
			Status: "404",
			Title:  "Not found",
			Detail: "helicopter not found",
		}})
	}

	rows, err := h.svc.ListByHelicopter(c.UserContext(), id)
	if err != nil {
		return writeHelicopterFileErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "Get failed",
			Detail: safeInternalDetail(),
		}})
	}
	rows = h.filterVisibleHelicopterFileRows(c.UserContext(), actorUserID(c), rows)

	groupedFiles := responsedto.HelicopterFileCollectionSections{
		Before:  make([]responsedto.HelicopterFileCollectionItem, 0),
		Prepare: make([]responsedto.HelicopterFileCollectionItem, 0),
		After:   make([]responsedto.HelicopterFileCollectionItem, 0),
	}
	for i := range rows {
		item := helicopterFileCollectionItem(c.UserContext(), h.storage, &rows[i])
		switch strings.TrimSpace(rows[i].Section) {
		case helicopterfile.SectionBeforeFirstFlightInspection:
			groupedFiles.Before = append(groupedFiles.Before, item)
		case helicopterfile.SectionFlightPreparationAndWB:
			groupedFiles.Prepare = append(groupedFiles.Prepare, item)
		case helicopterfile.SectionAfterLastFlightInspection:
			groupedFiles.After = append(groupedFiles.After, item)
		}
	}

	helicopterSummary := responsedto.HelicopterFileCollectionHelicopter{
		ID: idString(id),
	}
	if len(rows) > 0 && rows[0].Helicopter != nil {
		helicopterSummary.Name = strings.TrimSpace(rows[0].Helicopter.Designation)
	} else if h.helicopter != nil {
		if helicopterRow, heliErr := h.helicopter.GetByID(c.UserContext(), id); heliErr == nil && helicopterRow != nil {
			helicopterSummary.Name = strings.TrimSpace(helicopterRow.Designation)
		}
	}

	return response.Write(c, fiber.StatusOK, responsedto.HelicopterFileCollectionResponse{
		Type: "helicopter_file_collection",
		Attributes: responsedto.HelicopterFileCollectionAttributes{
			Helicopter: helicopterSummary,
			Files:      groupedFiles,
		},
	})
}

// ListHelicopterFiles godoc
// @Summary List helicopter files
// @Description List helicopter file template records grouped by helicopter.
// @Description Pagination is applied per helicopter group (not per file row), so one helicopter's files stay in the same page.
// @Tags Helicopter - Master File
// @Produce json
// @Param filter[search] query string false "Search text"
// @Param search query string false "Search text (fallback)"
// @Param sort query string false "Sort field"
// @Param page[number] query int false "Page number"
// @Param page[size] query int false "Page size"
// @Param page query int false "Page number (fallback)"
// @Param size query int false "Page size (fallback)"
// @Success 200 {object} responsedto.HelicopterFileCollectionListResponse
// @Failure 400 {object} jsonapi.ErrorDocument
// @Router /api/v1/helicopter-files/get-all [get]
func (h *HelicopterFileHandler) List(c *fiber.Ctx) error {
	filter := c.Query("filter[search]")
	if filter == "" {
		filter = c.Query("search")
	}

	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("size"), 20)
	}
	if pageSize > 100 {
		pageSize = 100
	}
	if pageSize < 1 {
		pageSize = 20
	}
	if pageNumber < 1 {
		pageNumber = 1
	}

	sort := c.Query("sort")

	allRows, _, err := h.svc.List(c.UserContext(), filter, sort, 0, 0)
	if err != nil {
		return writeHelicopterFileErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "List failed",
			Detail: safeInternalDetail(),
		}})
	}
	allRows = h.filterVisibleHelicopterFileRows(c.UserContext(), actorUserID(c), allRows)

	grouped := make([]responsedto.HelicopterFileCollectionResponse, 0)
	indexByHelicopterID := make(map[string]int)
	for i := range allRows {
		helicopterID := idString(allRows[i].HelicopterID)
		idx, ok := indexByHelicopterID[helicopterID]
		if !ok {
			helicopterName := ""
			if allRows[i].Helicopter != nil {
				helicopterName = strings.TrimSpace(allRows[i].Helicopter.Designation)
			}
			grouped = append(grouped, responsedto.HelicopterFileCollectionResponse{
				Type: "helicopter_file_collection",
				Attributes: responsedto.HelicopterFileCollectionAttributes{
					Helicopter: responsedto.HelicopterFileCollectionHelicopter{
						ID:   helicopterID,
						Name: helicopterName,
					},
					Files: responsedto.HelicopterFileCollectionSections{
						Before:  make([]responsedto.HelicopterFileCollectionItem, 0),
						Prepare: make([]responsedto.HelicopterFileCollectionItem, 0),
						After:   make([]responsedto.HelicopterFileCollectionItem, 0),
					},
				},
			})
			idx = len(grouped) - 1
			indexByHelicopterID[helicopterID] = idx
		}

		item := helicopterFileCollectionItem(c.UserContext(), h.storage, &allRows[i])
		switch strings.TrimSpace(allRows[i].Section) {
		case helicopterfile.SectionBeforeFirstFlightInspection:
			grouped[idx].Attributes.Files.Before = append(grouped[idx].Attributes.Files.Before, item)
		case helicopterfile.SectionFlightPreparationAndWB:
			grouped[idx].Attributes.Files.Prepare = append(grouped[idx].Attributes.Files.Prepare, item)
		case helicopterfile.SectionAfterLastFlightInspection:
			grouped[idx].Attributes.Files.After = append(grouped[idx].Attributes.Files.After, item)
		}
	}

	totalGroups := len(grouped)
	start := (pageNumber - 1) * pageSize
	if start > totalGroups {
		start = totalGroups
	}
	end := start + pageSize
	if end > totalGroups {
		end = totalGroups
	}
	paged := make([]responsedto.HelicopterFileCollectionResponse, 0)
	if start < end {
		paged = grouped[start:end]
	}

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

// DeleteHelicopterFile godoc
// @Summary Delete helicopter file
// @Description Delete helicopter file template record
// @Tags Helicopter - Master File
// @Accept json
// @Produce json
// @Param request body requestdto.HelicopterFileDeleteRequest true "JSON:API helicopter file delete request"
// @Success 200 {object} responsedto.HelicopterFileDeleteResponse
// @Failure 400 {object} jsonapi.ErrorDocument
// @Failure 404 {object} jsonapi.ErrorDocument
// @Failure 422 {object} jsonapi.ErrorDocument
// @Router /api/v1/helicopter-files/delete [delete]
func (h *HelicopterFileHandler) Delete(c *fiber.Ctx) error {
	var req requestdto.HelicopterFileDeleteRequest
	if err := c.BodyParser(&req); err != nil {
		return writeHelicopterFileErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "Invalid JSON",
			Detail: safeInternalDetail(),
		}})
	}
	if errs := h.validate.ValidateStruct(req); errs != nil {
		return writeHelicopterFileErrors(c, fiber.StatusUnprocessableEntity, errs)
	}

	id, err := uuidv7.ParseString(strings.TrimSpace(req.Data.ID))
	if err != nil {
		return writeHelicopterFileErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
			Status: "422",
			Title:  "Validation error",
			Detail: "id is invalid UUID",
			Source: &jsonapi.ErrorSource{Pointer: "/data/id"},
		}})
	}
	existing, _ := h.svc.GetByID(c.UserContext(), id)
	var prevAttachmentID []byte
	var prevSourceFileID []byte
	if existing != nil {
		prevAttachmentID = append([]byte(nil), existing.FileAttachmentID...)
		prevSourceFileID = append([]byte(nil), existing.SourceFileID...)
	}
	if err := h.svc.Delete(c.UserContext(), id); err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return writeHelicopterFileErrors(c, fiber.StatusNotFound, []jsonapi.ErrorObject{{
				Status: "404",
				Title:  "Not found",
				Detail: "helicopter file not found",
			}})
		}
		return writeHelicopterFileErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
			Status: "400",
			Title:  "Delete failed",
			Detail: safeInternalDetail(),
		}})
	}
	if len(prevAttachmentID) == 16 {
		if err := syncAttachmentLifecycleClear(c.UserContext(), h.fileManager, prevAttachmentID, actorUserID(c)); err != nil {
			return writeHelicopterFileErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
				Status: "400",
				Title:  "Delete failed",
				Detail: safeInternalDetail(),
			}})
		}
	}
	if len(prevAttachmentID) != 16 && len(prevSourceFileID) == 16 {
		if err := syncFileLifecycleSwap(c.UserContext(), h.fileManager, prevSourceFileID, nil, actorUserID(c)); err != nil {
			return writeHelicopterFileErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
				Status: "400",
				Title:  "Delete failed",
				Detail: safeInternalDetail(),
			}})
		}
	}

	return response.Write(c, fiber.StatusOK, jsonapi.Resource{
		Type: "helicopter_file_delete",
		Attributes: map[string]any{
			"deleted": true,
		},
	})
}

func helicopterFileResource(
	ctx context.Context,
	storage fileManagerDownloadURLStorage,
	row *helicopterfile.HelicopterFile,
) jsonapi.Resource {
	return jsonapi.Resource{
		Type:       "helicopter_file",
		ID:         idString(row.ID),
		Attributes: helicopterFileItemAttributesMap(ctx, storage, row),
	}
}

func (h *HelicopterFileHandler) publishHelicopterFileCreated(actorID []byte, row *helicopterfile.HelicopterFile, sourceFile *filemanager.File) {
	if h == nil || h.helicopterFileEvents == nil || row == nil || len(actorID) != 16 || len(row.ID) != 16 || len(row.HelicopterID) != 16 {
		return
	}
	if sourceFile == nil {
		sourceFile = row.SourceFile
	}
	if sourceFile == nil {
		return
	}

	actorUUID, err := uuidv7.BytesToString(actorID)
	if err != nil || strings.TrimSpace(actorUUID) == "" {
		return
	}
	helicopterUUID, err := uuidv7.BytesToString(row.HelicopterID)
	if err != nil || strings.TrimSpace(helicopterUUID) == "" {
		return
	}
	updatedAt := row.UpdatedAt.UTC()
	if updatedAt.IsZero() {
		updatedAt = time.Now().UTC()
	}

	event := helicopterfilerealtime.Event{
		EventType:         helicopterfilerealtime.DefaultEventType,
		HelicopterID:      helicopterUUID,
		HelicopterFileID:  idString(row.ID),
		FileID:            idString(row.SourceFileID),
		Section:           strings.TrimSpace(row.Section),
		Name:              strings.TrimSpace(sourceFile.Name),
		MimeType:          strings.TrimSpace(sourceFile.MimeType),
		AttachmentPending: row != nil && len(row.FileAttachmentID) != 16 && len(row.SourceFileID) == 16,
		Position:          row.Position,
		IsMandatory:       row.IsMandatory,
		UpdatedAt:         updatedAt.Format(time.RFC3339),
	}
	h.helicopterFileEvents.PublishToUsers([]string{actorUUID}, event)
}

func helicopterFileItemAttributesMap(
	ctx context.Context,
	storage fileManagerDownloadURLStorage,
	row *helicopterfile.HelicopterFile,
) map[string]any {
	fileName := ""
	downloadURL := ""
	helicopterName := ""
	if row != nil && row.Helicopter != nil {
		helicopterName = strings.TrimSpace(row.Helicopter.Designation)
	}
	if row != nil && row.FileAttachment != nil && row.FileAttachment.File != nil {
		fileName = strings.TrimSpace(row.FileAttachment.File.Name)
		downloadURL = strings.TrimSpace(resolveFileManagerDownloadURL(ctx, storage, row.FileAttachment.File))
	}
	attachmentPending := row != nil && len(row.FileAttachmentID) != 16 && len(row.SourceFileID) == 16
	isTemplate := helicopterFileIsTemplate(row)
	attachmentID := idString(row.FileAttachmentID)
	if attachmentPending {
		attachmentID = idString(row.SourceFileID)
		if fileName == "" && row.SourceFile != nil {
			fileName = strings.TrimSpace(row.SourceFile.Name)
		}
	}

	return map[string]any{
		"section":      strings.TrimSpace(row.Section),
		"position":     row.Position,
		"is_mandatory": row.IsMandatory,
		"is_template":  isTemplate,
		"helicopter": map[string]any{
			"id":   idString(row.HelicopterID),
			"name": helicopterName,
		},
		"attachment_pending": attachmentPending,
		"attachment": map[string]any{
			"id":           attachmentID,
			"file_name":    fileName,
			"download_url": downloadURL,
		},
		"created_at": row.CreatedAt.UTC().Format(time.RFC3339),
		"created_by": uuidStringOrEmpty(row.CreatedBy),
		"updated_at": row.UpdatedAt.UTC().Format(time.RFC3339),
		"updated_by": uuidStringOrEmpty(row.UpdatedBy),
	}
}

func (h *HelicopterFileHandler) filterVisibleHelicopterFileRows(ctx context.Context, actorID []byte, rows []helicopterfile.HelicopterFile) []helicopterfile.HelicopterFile {
	if len(rows) == 0 {
		return rows
	}
	filtered := make([]helicopterfile.HelicopterFile, 0, len(rows))
	for i := range rows {
		// Documents created from a template (e.g. via /files/create-from-template) are
		// persisted as helicopter_files rows for takeover/WOPI use, but must not surface
		// in the helicopter files listing.
		if helicopterFileCreatedFromTemplate(&rows[i]) {
			continue
		}
		if h.canViewHelicopterFileRow(ctx, actorID, &rows[i]) {
			filtered = append(filtered, rows[i])
		}
	}
	return filtered
}

func (h *HelicopterFileHandler) canViewHelicopterFileRow(ctx context.Context, actorID []byte, row *helicopterfile.HelicopterFile) bool {
	if row == nil {
		return false
	}
	takeoverID := helicopterFileTakeoverID(row)
	if len(takeoverID) != 16 {
		return true
	}
	if h == nil || h.takeover == nil || len(actorID) != 16 {
		return false
	}
	takeoverRow, err := h.takeover.GetByID(ctx, takeoverID)
	if err != nil || takeoverRow == nil {
		return false
	}
	return takeoverHasUserAccess(takeoverRow, actorID)
}

func helicopterFileTakeoverID(row *helicopterfile.HelicopterFile) []byte {
	if row == nil {
		return nil
	}
	if row.SourceFile != nil && len(row.SourceFile.TakeoverID) == 16 {
		return row.SourceFile.TakeoverID
	}
	if row.FileAttachment != nil && row.FileAttachment.File != nil && len(row.FileAttachment.File.TakeoverID) == 16 {
		return row.FileAttachment.File.TakeoverID
	}
	return nil
}

func helicopterFileCollectionItem(
	ctx context.Context,
	storage fileManagerDownloadURLStorage,
	row *helicopterfile.HelicopterFile,
) responsedto.HelicopterFileCollectionItem {
	fileName := ""
	downloadURL := ""
	if row != nil && row.FileAttachment != nil && row.FileAttachment.File != nil {
		fileName = strings.TrimSpace(row.FileAttachment.File.Name)
		downloadURL = strings.TrimSpace(resolveFileManagerDownloadURL(ctx, storage, row.FileAttachment.File))
	}
	attachmentPending := row != nil && len(row.FileAttachmentID) != 16 && len(row.SourceFileID) == 16
	isTemplate := helicopterFileIsTemplate(row)
	attachmentID := idString(row.FileAttachmentID)
	if attachmentPending {
		attachmentID = idString(row.SourceFileID)
		if fileName == "" && row.SourceFile != nil {
			fileName = strings.TrimSpace(row.SourceFile.Name)
		}
	}
	return responsedto.HelicopterFileCollectionItem{
		HelicopterFileID:  idString(row.ID),
		Section:           strings.TrimSpace(row.Section),
		Position:          row.Position,
		IsMandatory:       row.IsMandatory,
		IsTemplate:        isTemplate,
		AttachmentPending: attachmentPending,
		Attachment: responsedto.HelicopterFileCollectionAttachment{
			ID:          attachmentID,
			FileName:    fileName,
			DownloadURL: downloadURL,
		},
		CreatedAt: row.CreatedAt.UTC().Format(time.RFC3339),
		CreatedBy: uuidStringOrEmpty(row.CreatedBy),
		UpdatedAt: row.UpdatedAt.UTC().Format(time.RFC3339),
		UpdatedBy: uuidStringOrEmpty(row.UpdatedBy),
	}
}

func helicopterFileIsTemplate(row *helicopterfile.HelicopterFile) bool {
	if row == nil {
		return false
	}
	if row.SourceFile != nil {
		return row.SourceFile.IsTemplate
	}
	if row.FileAttachment != nil && row.FileAttachment.File != nil {
		return row.FileAttachment.File.IsTemplate
	}
	return false
}

// helicopterFileCreatedFromTemplate reports whether the row points to a document
// instantiated from a template via the WOPI create-from-template flow (identified by
// a non-empty template_uuid on the underlying file). These rows stay in the database
// for takeover/WOPI use but are excluded from the helicopter files listing, which only
// surfaces templates and regular files — not template-derived working copies.
// officeEditableExtensions are the file types Collabora edits and whose byte-level
// integrity must hold before they can be registered as templates.
var officeEditableExtensions = map[string]struct{}{
	"docx": {}, "xlsx": {}, "pptx": {}, "odt": {}, "ods": {}, "odp": {},
	"doc": {}, "xls": {}, "ppt": {}, "rtf": {},
}

// validateStoredHelicopterFileContent rejects office documents whose stored bytes are
// corrupt or incomplete, so they can never be registered as templates and later surface
// as corrupt Collabora sessions on create-from-template. Non-office files pass through.
func (h *HelicopterFileHandler) validateStoredHelicopterFileContent(ctx context.Context, file *filemanager.File) error {
	if file == nil {
		return nil
	}
	ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(strings.TrimSpace(file.Name)), "."))
	if ext == "" {
		ext = strings.ToLower(strings.TrimPrefix(filepath.Ext(strings.TrimSpace(file.ObjectKey)), "."))
	}
	if _, ok := officeEditableExtensions[ext]; !ok {
		return nil
	}
	storage, ok := any(h.storage).(templateCopyStorage)
	if !ok {
		return nil
	}

	body, meta, err := storage.GetObject(ctx, file.ObjectKey)
	if err != nil {
		return err
	}
	defer body.Close()

	payload, err := io.ReadAll(body)
	if err != nil {
		return err
	}

	declaredMime := firstNonEmpty(meta.ContentType, file.MimeType)
	if ext == "xlsx" {
		if sanitizeStorage, ok := any(h.storage).(xlsxSanitizerStorage); ok {
			sanitizedPayload, _, changed, err := sanitizeXLSXPayload(payload)
			if err != nil {
				return err
			}
			if changed {
				contentType := strings.TrimSpace(firstNonEmpty(declaredMime, canonicalOfficeContentType(file.Name)))
				if contentType == "" {
					contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
				}
				newMeta, err := sanitizeStorage.PutObject(ctx, filemanager.PutObjectInput{
					ObjectKey:   file.ObjectKey,
					ContentType: contentType,
					SizeBytes:   int64(len(sanitizedPayload)),
					Body:        bytes.NewReader(sanitizedPayload),
				})
				if err != nil {
					return err
				}

				file.SizeBytes = int64(len(sanitizedPayload))
				file.MimeType = contentType
				if strings.TrimSpace(newMeta.Bucket) != "" {
					file.Bucket = newMeta.Bucket
				}
				if strings.TrimSpace(newMeta.ObjectKey) != "" {
					file.ObjectKey = newMeta.ObjectKey
				}
				if strings.TrimSpace(newMeta.ETag) != "" {
					file.ETag = newMeta.ETag
				}
				if strings.TrimSpace(newMeta.VersionID) != "" {
					file.ObjectVersion = newMeta.VersionID
				}
				if updater := h.fileManager; updater != nil {
					if err := updater.UpdateFile(ctx, file); err != nil {
						return err
					}
				}
			}
			payload = sanitizedPayload
		}
	}

	if err := validateOfficeContentSample(ext, declaredMime, payload); err != nil {
		return err
	}
	switch ext {
	case "docx", "xlsx", "pptx", "odt", "ods", "odp":
		return validateOfficeZipStructure(ext, payload)
	default:
		return nil
	}
}

func helicopterFileCreatedFromTemplate(row *helicopterfile.HelicopterFile) bool {
	if row == nil {
		return false
	}
	if row.SourceFile != nil {
		return len(row.SourceFile.TemplateUUID) == 16
	}
	if row.FileAttachment != nil && row.FileAttachment.File != nil {
		return len(row.FileAttachment.File.TemplateUUID) == 16
	}
	return false
}

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

func inferHelicopterFileDef(status int, title, detail string) apperrorsx.Def {
	lowerTitle := strings.ToLower(strings.TrimSpace(title))
	lowerDetail := strings.ToLower(strings.TrimSpace(detail))
	switch status {
	case fiber.StatusNotFound:
		return apperrorsx.ErrHelicopterFileNotFound
	case fiber.StatusUnprocessableEntity:
		switch {
		case strings.Contains(lowerDetail, "invalid uuid"):
			return apperrorsx.ErrHelicopterFileInvalidUUID
		case strings.Contains(lowerDetail, "section is invalid"):
			return apperrorsx.ErrHelicopterFileSectionInvalid
		case strings.Contains(lowerDetail, "file_name is required"):
			return apperrorsx.ErrHelicopterFileFileNameRequired
		default:
			return apperrorsx.ErrHelicopterFileInvalidPayload
		}
	case fiber.StatusBadRequest:
		switch {
		case strings.Contains(lowerTitle, "invalid json"):
			return apperrorsx.ErrHelicopterFileInvalidJSON
		case strings.Contains(lowerTitle, "create failed"), strings.Contains(lowerTitle, "upload"), strings.Contains(lowerTitle, "cancel upload"):
			return apperrorsx.ErrHelicopterFileCreateFailed
		case strings.Contains(lowerTitle, "update failed"):
			return apperrorsx.ErrHelicopterFileUpdateFailed
		case strings.Contains(lowerTitle, "delete failed"):
			return apperrorsx.ErrHelicopterFileDeleteFailed
		case strings.Contains(lowerTitle, "get failed"):
			return apperrorsx.ErrHelicopterFileGetFailed
		case strings.Contains(lowerTitle, "list failed"):
			return apperrorsx.ErrHelicopterFileListFailed
		default:
			if strings.Contains(lowerDetail, "get failed") {
				return apperrorsx.ErrHelicopterFileGetFailed
			}
			return apperrorsx.ErrHelicopterFileInvalidPayload
		}
	default:
		return apperrorsx.ErrInternal
	}
}

func inferHelicopterFileCode(status int, title, detail string) (string, string) {
	def := inferHelicopterFileDef(status, title, detail)
	return def.Code, def.ErrorCode
}

func writeHelicopterFileBadRequestError(c *fiber.Ctx, title, detail string) {
	_ = writeHelicopterFileErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
		Status: "400",
		Title:  title,
		Detail: detail,
	}})
}

func writeHelicopterFileValidationError(c *fiber.Ctx, detail, pointer string) {
	_ = writeHelicopterFileErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{{
		Status: "422",
		Title:  "Validation error",
		Detail: detail,
		Source: &jsonapi.ErrorSource{
			Pointer: pointer,
		},
	}})
}

func isValidHelicopterFileSection(section string) bool {
	switch strings.TrimSpace(section) {
	case helicopterfile.SectionBeforeFirstFlightInspection,
		helicopterfile.SectionFlightPreparationAndWB,
		helicopterfile.SectionAfterLastFlightInspection:
		return true
	default:
		return false
	}
}

func optionalStringValue(v *string) string {
	if v == nil {
		return ""
	}
	return *v
}

func helicopterFileUploadBulkErrorItem(index int, status int, title, detail string) responsedto.HelicopterFileUploadIntentBulkItem {
	def := inferHelicopterFileDef(status, title, detail)
	return responsedto.HelicopterFileUploadIntentBulkItem{
		Index:   index,
		Success: false,
		Error: &responsedto.HelicopterFileBulkError{
			Status:    status,
			Code:      def.Code,
			ErrorCode: def.ErrorCode,
			Title:     def.Title,
			Detail:    def.Message,
		},
	}
}

func helicopterFileCreateBulkErrorItem(index int, status int, title, detail string) responsedto.HelicopterFileCreateBulkItem {
	def := inferHelicopterFileDef(status, title, detail)
	return responsedto.HelicopterFileCreateBulkItem{
		Index:   index,
		Success: false,
		Error: &responsedto.HelicopterFileBulkError{
			Status:    status,
			Code:      def.Code,
			ErrorCode: def.ErrorCode,
			Title:     def.Title,
			Detail:    def.Message,
		},
	}
}

func helicopterFileUpdateBulkErrorItem(index int, status int, title, detail string) responsedto.HelicopterFileUpdateBulkItem {
	def := inferHelicopterFileDef(status, title, detail)
	return responsedto.HelicopterFileUpdateBulkItem{
		Index:   index,
		Success: false,
		Error: &responsedto.HelicopterFileBulkError{
			Status:    status,
			Code:      def.Code,
			ErrorCode: def.ErrorCode,
			Title:     def.Title,
			Detail:    def.Message,
		},
	}
}

func writeHelicopterFileUploadIntentBulkResponse(c *fiber.Ctx, items []responsedto.HelicopterFileUploadIntentBulkItem) error {
	total := len(items)
	success := 0
	for i := range items {
		if items[i].Success {
			success++
			items[i].Error = nil
			continue
		}
		items[i].Resource = nil
		if items[i].Error == nil {
			def := inferHelicopterFileDef(fiber.StatusInternalServerError, "Internal error", "missing item error")
			items[i].Error = &responsedto.HelicopterFileBulkError{
				Status:    fiber.StatusInternalServerError,
				Code:      def.Code,
				ErrorCode: def.ErrorCode,
				Title:     def.Title,
				Detail:    def.Message,
			}
		}
	}
	failed := total - success

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

	var statusCode int
	if failed == 0 {
		statusCode = fiber.StatusCreated
	} else if success > 0 {
		statusCode = fiber.StatusMultiStatus
	} else {
		statusCode = fiber.StatusUnprocessableEntity
		for i := range items {
			if items[i].Error != nil && items[i].Error.Status >= 400 {
				statusCode = items[i].Error.Status
				break
			}
		}
	}

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

func writeHelicopterFileCreateBulkResponse(c *fiber.Ctx, items []responsedto.HelicopterFileCreateBulkItem) error {
	total := len(items)
	success := 0
	for i := range items {
		if items[i].Success {
			success++
			items[i].Error = nil
			continue
		}
		items[i].Resource = nil
		if items[i].Error == nil {
			def := inferHelicopterFileDef(fiber.StatusInternalServerError, "Internal error", "missing item error")
			items[i].Error = &responsedto.HelicopterFileBulkError{
				Status:    fiber.StatusInternalServerError,
				Code:      def.Code,
				ErrorCode: def.ErrorCode,
				Title:     def.Title,
				Detail:    def.Message,
			}
		}
	}
	failed := total - success

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

	var statusCode int
	if failed == 0 {
		statusCode = fiber.StatusCreated
	} else if success > 0 {
		statusCode = fiber.StatusMultiStatus
	} else {
		statusCode = fiber.StatusUnprocessableEntity
		for i := range items {
			if items[i].Error != nil && items[i].Error.Status >= 400 {
				statusCode = items[i].Error.Status
				break
			}
		}
	}

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

func helicopterSectionFolderName(section string) (string, bool) {
	switch strings.TrimSpace(section) {
	case helicopterfile.SectionBeforeFirstFlightInspection:
		return "Before First Flight Inspection", true
	case helicopterfile.SectionFlightPreparationAndWB:
		return "Flight Preparation and W&B", true
	case helicopterfile.SectionAfterLastFlightInspection:
		return "After Last Flight Inspection", true
	default:
		return "", false
	}
}

func (h *HelicopterFileHandler) ensureHelicopterSectionFolder(ctx context.Context, actorID, helicopterID []byte, section string) ([]byte, error) {
	folderName, ok := helicopterSectionFolderName(section)
	if !ok {
		return nil, nil
	}

	heli, err := h.helicopter.GetByID(ctx, helicopterID)
	if err != nil || heli == nil {
		return nil, err
	}

	designation := strings.TrimSpace(heli.Designation)
	if designation == "" {
		return nil, nil
	}

	root, err := h.fileManager.GetOrCreateSystemRootFolder(ctx)
	if err != nil || root == nil {
		return nil, err
	}

	aircraftFolderID, err := h.ensureHelicopterFolderByParentAndName(ctx, root.ID, "aircraft", actorID)
	if err != nil {
		return nil, err
	}

	designationFolderID, err := h.ensureHelicopterFolderByParentAndName(ctx, aircraftFolderID, designation, actorID)
	if err != nil {
		return nil, err
	}

	return h.ensureHelicopterFolderByParentAndName(ctx, designationFolderID, folderName, actorID)
}

func (h *HelicopterFileHandler) ensureHelicopterFolderByParentAndName(ctx context.Context, parentID []byte, name string, actorID []byte) ([]byte, error) {
	normalized := strings.ToLower(name)
	existing, err := h.fileManager.GetFolderByParentAndName(ctx, parentID, normalized)
	if err != nil {
		return nil, err
	}
	if existing != nil {
		return existing.ID, nil
	}
	created, err := h.fileManager.CreateFolderNode(ctx, filemanager.CreateFolderParams{
		Name:     name,
		ParentID: parentID,
		ActorID:  actorID,
	})
	if err != nil {
		return nil, err
	}
	return created.ID, nil
}

func writeHelicopterFileUpdateBulkResponse(c *fiber.Ctx, items []responsedto.HelicopterFileUpdateBulkItem) error {
	total := len(items)
	success := 0
	for i := range items {
		if items[i].Success {
			success++
			items[i].Error = nil
			continue
		}
		items[i].Resource = nil
		if items[i].Error == nil {
			def := inferHelicopterFileDef(fiber.StatusInternalServerError, "Internal error", "missing item error")
			items[i].Error = &responsedto.HelicopterFileBulkError{
				Status:    fiber.StatusInternalServerError,
				Code:      def.Code,
				ErrorCode: def.ErrorCode,
				Title:     def.Title,
				Detail:    def.Message,
			}
		}
	}
	failed := total - success

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

	var statusCode int
	if failed == 0 {
		statusCode = fiber.StatusOK
	} else if success > 0 {
		statusCode = fiber.StatusMultiStatus
	} else {
		statusCode = fiber.StatusUnprocessableEntity
		for i := range items {
			if items[i].Error != nil && items[i].Error.Status >= 400 {
				statusCode = items[i].Error.Status
				break
			}
		}
	}

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