package handlers

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"sort"
	"strings"
	"time"

	"github.com/gofiber/fiber/v2"

	filemanager "wucher/internal/domain/file_manager"
	"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 FileManagerFolderHandler struct {
	svc      filemanager.Service
	validate *validators.Validator
	storage  filemanager.ObjectStorage
}

var (
	_                          = dto.ErrorResponse{}
	_                          = dto.GenericDeleteResponse{}
	errFileManagerNodeNotFound = errors.New("node not found")
)

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

// CreateFolder godoc
// @Summary Create folder node
// @ID FileManagerCreateFolder
// @Description Create one folder metadata node under root or under provided parent_id.
// @Tags File Manager
// @Accept json
// @Produce json
// @Param request body requestdto.FileManagerFolderCreateRequest true "JSON:API file manager folder create request"
// @Success 201 {object} responsedto.FileManagerFolderResponse
// @Failure 422 {object} dto.ErrorResponse
// @Failure 400 {object} dto.ErrorResponse
// @Router /api/v1/file-manager/folders/create [post]
func (h *FileManagerFolderHandler) CreateFolder(c *fiber.Ctx) error {
	var req requestdto.FileManagerFolderCreateRequest
	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)
	}

	var parentID []byte
	if req.Data.Attributes.ParentID != nil && strings.TrimSpace(*req.Data.Attributes.ParentID) != "" {
		pid, err := uuidv7.ParseString(strings.TrimSpace(*req.Data.Attributes.ParentID))
		if err != nil {
			return writeFileManagerError(c, fiber.StatusUnprocessableEntity, "Validation error", "parent_id is invalid UUID", &jsonapi.ErrorSource{Pointer: "/data/attributes/parent_id"})
		}
		parentID = pid
	}

	created, err := h.svc.CreateFolderNode(c.UserContext(), filemanager.CreateFolderParams{
		Name:     req.Data.Attributes.Name,
		ParentID: parentID,
		ActorID:  actorUserID(c),
	})
	if err != nil {
		return writeFileManagerActionError(c, "Create failed", err)
	}
	return response.Write(c, fiber.StatusCreated, fileManagerFolderResource(created))
}

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

	row, err := h.svc.GetFolderByID(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", "folder not found", nil)
	}

	return response.Write(c, fiber.StatusOK, fileManagerFolderDetailResource(row))
}

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

	sort := c.Query("sort")

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

	data := make([]responsedto.FileManagerFolderResource, 0, len(rows))
	for i := range rows {
		data = append(data, fileManagerFolderResource(&rows[i]))
	}

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

// ListAll godoc
// @Summary List merged explorer nodes
// @ID FileManagerListMergedExplorerNodes
// @Description Return merged direct children (folders + files) for optional parent_uuid (non-recursive).
// @Description If parent_uuid is omitted, response returns root level nodes.
// @Description If search is provided, performs a global name search across all folders and files (parent_uuid is ignored).
// @Tags File Manager
// @Produce json
// @Param parent_uuid query string false "Parent folder UUID"
// @Param search query string false "Search keyword (global name search across all folders and files)"
// @Success 200 {object} responsedto.FileManagerNodeListResponse
// @Failure 422 {object} dto.ErrorResponse
// @Failure 400 {object} dto.ErrorResponse
// @Router /api/v1/file-manager/get-all [get]
func (h *FileManagerFolderHandler) ListAll(c *fiber.Ctx) error {
	search := strings.TrimSpace(c.Query("search"))
	if search != "" {
		folders, files, err := h.svc.SearchNodes(c.UserContext(), search)
		if err != nil {
			return writeFileManagerActionError(c, "Search failed", err)
		}

		data := make([]responsedto.FileManagerNodeResource, 0, len(folders)+len(files))
		for i := range folders {
			data = append(data, fileManagerNodeFromFolder(&folders[i], false))
		}
		for i := range files {
			downloadURL := resolveFileManagerDownloadURL(c.UserContext(), h.storage, &files[i])
			data = append(data, fileManagerNodeFromFile(&files[i], false, downloadURL))
		}

		meta := map[string]any{
			"is_root": false,
			"folders": len(folders),
			"files":   len(files),
			"total":   len(data),
			"search":  search,
		}
		return response.WriteWithMeta(c, fiber.StatusOK, data, meta)
	}

	var parentID []byte
	parentUUID := strings.TrimSpace(c.Query("parent_uuid"))
	if parentUUID != "" {
		id, err := uuidv7.ParseString(parentUUID)
		if err != nil {
			return writeFileManagerError(c, fiber.StatusUnprocessableEntity, "Validation error", "parent_uuid is invalid UUID", &jsonapi.ErrorSource{Pointer: "/query/parent_uuid"})
		}
		parentID = id
	}

	folders, files, err := h.svc.ListNodesByParent(c.UserContext(), parentID)
	if err != nil {
		return writeFileManagerActionError(c, "List failed", err)
	}

	isRoot := len(parentID) == 0
	data := make([]responsedto.FileManagerNodeResource, 0, len(folders)+len(files))
	for i := range folders {
		data = append(data, fileManagerNodeFromFolder(&folders[i], isRoot))
	}
	for i := range files {
		downloadURL := resolveFileManagerDownloadURL(c.UserContext(), h.storage, &files[i])
		data = append(data, fileManagerNodeFromFile(&files[i], isRoot, downloadURL))
	}

	var parentUUIDValue *string
	if !isRoot {
		parentUUID := idString(parentID)
		if strings.TrimSpace(parentUUID) != "" {
			parentUUIDValue = &parentUUID
		}
	}

	meta := map[string]any{
		"is_root": isRoot,
		"folders": len(folders),
		"files":   len(files),
		"total":   len(data),
	}
	if !isRoot && parentUUIDValue != nil {
		meta["parent_uuid"] = *parentUUIDValue
		breadcrumb, err := h.buildExplorerBreadcrumb(c.UserContext(), parentID)
		if err != nil {
			return writeFileManagerActionError(c, "List failed", err)
		}
		if len(breadcrumb) > 0 {
			meta["breadcrumb"] = breadcrumb
		}
	}
	return response.WriteWithMeta(c, fiber.StatusOK, data, meta)
}

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

	sort := c.Query("sort")

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

	data := make([]responsedto.FileManagerFolderResource, 0, len(rows))
	for i := range rows {
		data = append(data, fileManagerFolderResource(&rows[i]))
	}

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

// ListTrashItems godoc
// @Summary List merged trash items
// @ID FileManagerListMergedTrashItems
// @Description Return merged trashed nodes (folders + files) for trash explorer.
// @Tags File Manager
// @Produce json
// @Param page[number] query int false "Page number (default 1)"
// @Param page[size] query int false "Page size (default 20, max 100)"
// @Param sort query string false "Sort values: deleted_at,-deleted_at,name,-name,type,-type,size_bytes,-size_bytes"
// @Success 200 {object} responsedto.FileManagerTrashListResponse
// @Failure 400 {object} dto.ErrorResponse
// @Router /api/v1/file-manager/trash/get-all [get]
func (h *FileManagerFolderHandler) ListTrashItems(c *fiber.Ctx) error {
	folders, files, err := h.svc.ListTrashNodes(c.UserContext())
	if err != nil {
		return writeFileManagerActionError(c, "List failed", err)
	}

	items := make([]fileManagerTrashItemView, 0, len(folders)+len(files))
	folderCount := 0
	fileCount := 0

	for i := range folders {
		row := &folders[i]
		if row.DeletedAt == nil {
			continue
		}
		if len(row.ParentID) == 0 && strings.EqualFold(strings.TrimSpace(row.NameNormalized), filemanager.SystemRootFolderName) {
			continue
		}
		folderCount++
		items = append(items, fileManagerTrashItemView{
			NodeType:  "folder",
			UUID:      idString(row.ID),
			Name:      row.Name,
			PathCache: row.PathCache,
			DeletedAt: row.DeletedAt.UTC(),
			CreatedAt: row.CreatedAt.UTC(),
			UpdatedAt: row.UpdatedAt.UTC(),
		})
	}

	for i := range files {
		row := &files[i]
		if row.DeletedAt == nil {
			continue
		}
		fileCount++
		items = append(items, fileManagerTrashItemView{
			NodeType:    "file",
			UUID:        idString(row.ID),
			Name:        row.Name,
			Extension:   row.Extension,
			MimeType:    row.MimeType,
			DownloadURL: resolveFileManagerDownloadURL(c.UserContext(), h.storage, row),
			SizeBytes:   row.SizeBytes,
			DeletedAt:   row.DeletedAt.UTC(),
			CreatedAt:   row.CreatedAt.UTC(),
			UpdatedAt:   row.UpdatedAt.UTC(),
		})
	}

	sortFileManagerTrashItems(items, c.Query("sort"))

	total := len(items)
	data := make([]responsedto.FileManagerTrashNodeResource, 0, len(items))
	for i := range items {
		data = append(data, responsedto.FileManagerTrashNodeResource{
			Type: "file_manager_trash_node",
			ID:   items[i].UUID,
			Attributes: responsedto.FileManagerTrashNodeAttributes{
				NodeType:    items[i].NodeType,
				Name:        items[i].Name,
				Extension:   items[i].Extension,
				SizeBytes:   items[i].SizeBytes,
				MimeType:    items[i].MimeType,
				DownloadURL: items[i].DownloadURL,
				PathCache:   items[i].PathCache,
				TrashedAt:   items[i].DeletedAt.Format(time.RFC3339),
				CreatedAt:   items[i].CreatedAt.Format(time.RFC3339),
				UpdatedAt:   items[i].UpdatedAt.Format(time.RFC3339),
			},
		})
	}

	meta := map[string]any{
		"total":   total,
		"folders": folderCount,
		"files":   fileCount,
	}
	return response.WriteWithMeta(c, fiber.StatusOK, data, meta)
}

// UpdateFolder godoc
// @Summary Update folder node
// @ID FileManagerUpdateFolder
// @Description Update folder metadata by UUID (rename and/or move parent).
// @Tags File Manager
// @Accept json
// @Produce json
// @Param uuid path string true "Folder UUID (UUIDv7)"
// @Param request body requestdto.FileManagerFolderUpdateRequest true "JSON:API file manager folder update request"
// @Success 200 {object} responsedto.FileManagerFolderResponse
// @Failure 422 {object} dto.ErrorResponse
// @Failure 404 {object} dto.ErrorResponse
// @Router /api/v1/file-manager/folders/update/{uuid} [patch]
func (h *FileManagerFolderHandler) UpdateFolder(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.FileManagerFolderUpdateRequest
	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.ParentID == nil {
		return writeFileManagerError(c, fiber.StatusUnprocessableEntity, "Validation error", "at least one attribute must be provided", &jsonapi.ErrorSource{Pointer: "/data/attributes"})
	}
	var parentID []byte
	parentProvided := attrs.ParentID != nil
	if parentProvided && strings.TrimSpace(*attrs.ParentID) != "" {
		parsed, err := uuidv7.ParseString(strings.TrimSpace(*attrs.ParentID))
		if err != nil {
			return writeFileManagerError(c, fiber.StatusUnprocessableEntity, "Validation error", "parent_id is invalid UUID", &jsonapi.ErrorSource{Pointer: "/data/attributes/parent_id"})
		}
		parentID = parsed
	}

	updated, err := h.svc.MoveFolderNode(c.UserContext(), id, filemanager.MoveFolderParams{
		Name:           attrs.Name,
		ParentID:       parentID,
		ParentProvided: parentProvided,
		ActorID:        actorUserID(c),
	})
	if err != nil {
		return writeFileManagerActionError(c, "Update failed", err)
	}
	return response.Write(c, fiber.StatusOK, fileManagerFolderResource(updated))
}

// MoveNodes godoc
// @Summary Move nodes (file/folder) in bulk
// @ID FileManagerMoveNodesBulk
// @Description Unified move endpoint. Each item UUID can be either folder UUID or file UUID.
// @Description destination_folder_uuid null/empty means move to root.
// @Description Bulk payload accepts one or many items.
// @Tags File Manager
// @Accept json
// @Produce json
// @Param request body requestdto.FileManagerNodeMoveBulkRequest true "Bulk move request"
// @Success 200 {object} responsedto.FileManagerMoveNodeBulkResponse
// @Failure 422 {object} dto.ErrorResponse
// @Failure 400 {object} dto.ErrorResponse
// @Router /api/v1/file-manager/move [patch]
func (h *FileManagerFolderHandler) MoveNodes(c *fiber.Ctx) error {
	var req requestdto.FileManagerNodeMoveBulkRequest
	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.FileManagerMoveNodeBulkItem, len(req.Data))
	for i := range req.Data {
		item := req.Data[i]
		nodeUUID := strings.TrimSpace(item.UUID)
		if nodeUUID == "" {
			items[i] = fileManagerMoveBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "uuid is required")
			continue
		}

		nodeID, err := uuidv7.ParseString(nodeUUID)
		if err != nil {
			items[i] = fileManagerMoveBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "uuid is invalid UUID")
			continue
		}

		var destinationID []byte
		destinationUUID := ""
		if item.DestinationFolderUUID != nil && strings.TrimSpace(*item.DestinationFolderUUID) != "" {
			destinationUUID = strings.TrimSpace(*item.DestinationFolderUUID)
			parsed, err := uuidv7.ParseString(destinationUUID)
			if err != nil {
				items[i] = fileManagerMoveBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "destination_folder_uuid is invalid UUID")
				continue
			}
			destinationID = parsed
		}

		nodeType, moveErr := h.moveNodeAutoDetect(c.UserContext(), nodeID, destinationID, actorID)
		if moveErr != nil {
			items[i] = fileManagerMoveBulkActionErrorItem(i, moveErr)
			continue
		}
		items[i] = fileManagerMoveBulkSuccessItem(i, nodeID, nodeType, destinationUUID)
	}

	return writeFileManagerMoveBulkResponse(c, items)
}

// DeleteNodesBulk godoc
// @Summary Delete nodes (file/folder) in bulk
// @ID FileManagerDeleteNodesBulk
// @Description Soft-delete multiple nodes by UUID. UUID can be folder or file.
// @Description Bulk payload accepts one or many items.
// @Tags File Manager
// @Accept json
// @Produce json
// @Param request body requestdto.FileManagerNodePurgeBulkRequest true "Bulk delete request"
// @Success 200 {object} responsedto.FileManagerMoveNodeBulkResponse
// @Failure 422 {object} dto.ErrorResponse
// @Failure 400 {object} dto.ErrorResponse
// @Router /api/v1/file-manager/delete [delete]
func (h *FileManagerFolderHandler) DeleteNodesBulk(c *fiber.Ctx) error {
	var req requestdto.FileManagerNodePurgeBulkRequest
	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.FileManagerMoveNodeBulkItem, len(req.Data))
	for i := range req.Data {
		nodeUUID := strings.TrimSpace(req.Data[i].UUID)
		if nodeUUID == "" {
			items[i] = fileManagerMoveBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "uuid is required")
			continue
		}

		nodeID, err := uuidv7.ParseString(nodeUUID)
		if err != nil {
			items[i] = fileManagerMoveBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "uuid is invalid UUID")
			continue
		}

		nodeType, err := h.deleteNodeAutoDetect(c.UserContext(), nodeID, actorID)
		if err != nil {
			if errors.Is(err, errFileManagerNodeNotFound) {
				items[i] = fileManagerMoveBulkSystemErrorItem(i, fiber.StatusNotFound, "Not found", apperrorsx.ErrDataNotFound.Message)
				continue
			}
			def := mapFileManagerActionDef("Delete failed", err)
			items[i] = fileManagerMoveBulkSystemErrorItem(i, def.Status, def.Title, def.Message)
			continue
		}

		resource := responsedto.FileManagerMoveNodeResultResource{
			Type: "file_manager_delete_result",
			ID:   idString(nodeID),
			Attributes: responsedto.FileManagerMoveNodeResultAttributes{
				NodeType: nodeType,
				Status:   "trashed",
			},
		}
		items[i] = responsedto.FileManagerMoveNodeBulkItem{
			Index:    i,
			Success:  true,
			Resource: &resource,
		}
	}

	return writeFileManagerMoveBulkResponse(c, items)
}

// RestoreNodesBulk godoc
// @Summary Restore nodes (file/folder) in bulk
// @ID FileManagerRestoreNodesBulk
// @Description Restore multiple trashed nodes by UUID to original location.
// @Description Bulk payload accepts one or many items.
// @Tags File Manager
// @Accept json
// @Produce json
// @Param request body requestdto.FileManagerNodePurgeBulkRequest true "Bulk restore request"
// @Success 200 {object} responsedto.FileManagerMoveNodeBulkResponse
// @Failure 422 {object} dto.ErrorResponse
// @Failure 400 {object} dto.ErrorResponse
// @Router /api/v1/file-manager/restore [patch]
func (h *FileManagerFolderHandler) RestoreNodesBulk(c *fiber.Ctx) error {
	var req requestdto.FileManagerNodePurgeBulkRequest
	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.FileManagerMoveNodeBulkItem, len(req.Data))
	for i := range req.Data {
		nodeUUID := strings.TrimSpace(req.Data[i].UUID)
		if nodeUUID == "" {
			items[i] = fileManagerMoveBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "uuid is required")
			continue
		}

		nodeID, err := uuidv7.ParseString(nodeUUID)
		if err != nil {
			items[i] = fileManagerMoveBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "uuid is invalid UUID")
			continue
		}

		nodeType, locationFolderID, err := h.restoreNodeAutoDetect(c.UserContext(), nodeID, actorID)
		if err != nil {
			if errors.Is(err, errFileManagerNodeNotFound) {
				items[i] = fileManagerMoveBulkSystemErrorItem(i, fiber.StatusNotFound, "Not found", apperrorsx.ErrDataNotFound.Message)
				continue
			}
			def := mapFileManagerActionDef("Restore failed", err)
			items[i] = fileManagerMoveBulkSystemErrorItem(i, def.Status, def.Title, def.Message)
			continue
		}

		resource := responsedto.FileManagerMoveNodeResultResource{
			Type: "file_manager_restore_result",
			ID:   idString(nodeID),
			Attributes: responsedto.FileManagerMoveNodeResultAttributes{
				NodeType: nodeType,
				Status:   "ready",
			},
		}
		if strings.TrimSpace(locationFolderID) != "" {
			resource.Attributes.Location = &responsedto.FileManagerActionLocation{
				FolderID: locationFolderID,
			}
		}
		items[i] = responsedto.FileManagerMoveNodeBulkItem{
			Index:    i,
			Success:  true,
			Resource: &resource,
		}
	}

	return writeFileManagerMoveBulkResponse(c, items)
}

func (h *FileManagerFolderHandler) moveNodeAutoDetect(ctx context.Context, nodeID, destinationID, actorID []byte) (string, error) {
	if _, err := h.svc.MoveFolderNode(ctx, nodeID, filemanager.MoveFolderParams{
		ParentID:       destinationID,
		ParentProvided: true,
		ActorID:        actorID,
	}); err == nil {
		return "folder", nil
	} else if !errors.Is(err, filemanager.ErrFolderNotFound) {
		return "", err
	}

	targetFolderID := destinationID
	if len(targetFolderID) == 0 {
		root, err := h.svc.GetOrCreateSystemRootFolder(ctx)
		if err != nil {
			return "", err
		}
		if root == nil || len(root.ID) == 0 {
			return "", errFileManagerNodeNotFound
		}
		targetFolderID = root.ID
	}

	if _, err := h.svc.MoveFileNode(ctx, nodeID, filemanager.MoveFileParams{
		FolderID:       targetFolderID,
		FolderProvided: true,
		ActorID:        actorID,
	}); err != nil {
		if errors.Is(err, filemanager.ErrFileNotFound) {
			return "", errFileManagerNodeNotFound
		}
		return "", err
	}

	return "file", nil
}

func (h *FileManagerFolderHandler) deleteNodeAutoDetect(ctx context.Context, nodeID, actorID []byte) (string, error) {
	if err := h.svc.DeleteFolder(ctx, nodeID, actorID, nil); err == nil {
		return "folder", nil
	} else if !errors.Is(err, filemanager.ErrFolderNotFound) {
		return "", err
	}

	if err := h.svc.DeleteFile(ctx, nodeID, actorID, nil); err != nil {
		if errors.Is(err, filemanager.ErrFileNotFound) {
			return "", errFileManagerNodeNotFound
		}
		return "", err
	}
	return "file", nil
}

func (h *FileManagerFolderHandler) restoreNodeAutoDetect(ctx context.Context, nodeID, actorID []byte) (string, string, error) {
	if err := h.svc.RestoreFolder(ctx, nodeID, nil, false, actorID); err == nil {
		row, getErr := h.svc.GetFolderByID(ctx, nodeID)
		if getErr != nil {
			return "", "", getErr
		}
		if row == nil {
			return "", "", errFileManagerNodeNotFound
		}
		locationFolderID := idString(row.ParentID)
		if strings.TrimSpace(locationFolderID) != "" {
			root, rootErr := h.svc.GetOrCreateSystemRootFolder(ctx)
			if rootErr != nil {
				return "", "", rootErr
			}
			if root != nil && bytes.Equal(row.ParentID, root.ID) {
				locationFolderID = ""
			}
		}
		return "folder", locationFolderID, nil
	} else if !errors.Is(err, filemanager.ErrFolderNotFound) {
		return "", "", err
	}

	if err := h.svc.RestoreFile(ctx, nodeID, nil, false, actorID); err != nil {
		if errors.Is(err, filemanager.ErrFileNotFound) {
			return "", "", errFileManagerNodeNotFound
		}
		return "", "", err
	}
	row, getErr := h.svc.GetFileByID(ctx, nodeID)
	if getErr != nil {
		return "", "", getErr
	}
	if row == nil {
		return "", "", errFileManagerNodeNotFound
	}
	root, rootErr := h.svc.GetOrCreateSystemRootFolder(ctx)
	if rootErr != nil {
		return "", "", rootErr
	}
	locationFolderID := ""
	if root == nil || !bytes.Equal(row.FolderID, root.ID) {
		locationFolderID = idString(row.FolderID)
	}
	return "file", locationFolderID, nil
}

func writeFileManagerMoveBulkResponse(c *fiber.Ctx, items []responsedto.FileManagerMoveNodeBulkItem) error {
	success := 0
	for i := range items {
		if items[i].Success {
			success++
		}
	}

	resp := responsedto.FileManagerMoveNodeBulkResponse{
		Data: items,
	}
	resp.Meta.Total = len(items)
	resp.Meta.Success = success
	resp.Meta.Failed = len(items) - success
	resp.Meta.PartialSuccess = resp.Meta.Success > 0 && resp.Meta.Failed > 0

	return c.Status(fiber.StatusOK).JSON(resp)
}

func fileManagerMoveBulkSuccessItem(index int, nodeID []byte, nodeType, destinationUUID string) responsedto.FileManagerMoveNodeBulkItem {
	resource := responsedto.FileManagerMoveNodeResultResource{
		Type: "file_manager_move_result",
		ID:   idString(nodeID),
		Attributes: responsedto.FileManagerMoveNodeResultAttributes{
			NodeType: nodeType,
			Status:   "moved",
		},
	}
	if strings.TrimSpace(destinationUUID) != "" {
		resource.Attributes.Location = &responsedto.FileManagerActionLocation{
			FolderID: destinationUUID,
		}
	}

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

func fileManagerMoveBulkSystemErrorItem(index int, status int, title, detail string) responsedto.FileManagerMoveNodeBulkItem {
	def := inferFileManagerDef(status, title, detail)
	return responsedto.FileManagerMoveNodeBulkItem{
		Index:   index,
		Success: false,
		Error: &responsedto.FileManagerActionBulkError{
			Status:    status,
			Code:      def.Code,
			ErrorCode: def.ErrorCode,
			Title:     def.Title,
			Detail:    def.Message,
		},
	}
}

func fileManagerMoveBulkActionErrorItem(index int, err error) responsedto.FileManagerMoveNodeBulkItem {
	if errors.Is(err, errFileManagerNodeNotFound) {
		return fileManagerMoveBulkSystemErrorItem(index, fiber.StatusNotFound, "Not found", apperrorsx.ErrDataNotFound.Message)
	}

	def := mapFileManagerActionDef("Move failed", err)
	return fileManagerMoveBulkSystemErrorItem(index, def.Status, def.Title, def.Message)
}

func (h *FileManagerFolderHandler) buildExplorerBreadcrumb(ctx context.Context, parentID []byte) ([]responsedto.FileManagerBreadcrumbItem, error) {
	if len(parentID) == 0 {
		return nil, nil
	}

	breadcrumb := make([]responsedto.FileManagerBreadcrumbItem, 0, 8)
	currentID := append([]byte(nil), parentID...)
	for len(currentID) != 0 {
		row, err := h.svc.GetFolderByID(ctx, currentID)
		if err != nil {
			return nil, err
		}
		if row == nil {
			break
		}

		item := responsedto.FileManagerBreadcrumbItem{
			ID:   idString(row.ID),
			Name: row.Name,
		}
		breadcrumb = append([]responsedto.FileManagerBreadcrumbItem{item}, breadcrumb...)
		currentID = append([]byte(nil), row.ParentID...)
	}

	return breadcrumb, nil
}

func (h *FileManagerFolderHandler) DeleteFolder(c *fiber.Ctx) error {
	id, err := h.parseUUIDParam(c, "uuid", "/path/uuid")
	if err != nil {
		return err
	}

	if err := h.svc.DeleteFolder(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", ""))
}

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

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

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

	locationFolderID := idString(row.ParentID)
	if strings.TrimSpace(locationFolderID) != "" {
		root, rootErr := h.svc.GetOrCreateSystemRootFolder(c.Context())
		if rootErr != nil {
			return writeFileManagerActionError(c, "Restore failed", rootErr)
		}
		if root != nil && bytes.Equal(row.ParentID, root.ID) {
			locationFolderID = ""
		}
	}
	return response.Write(c, fiber.StatusOK, fileManagerActionResultResource("file_manager_restore_result", id, "ready", locationFolderID))
}

// PurgeNodesBulk godoc
// @Summary Purge nodes (file/folder) from trash in bulk
// @ID FileManagerPurgeNodesBulkFromTrash
// @Description Permanently delete multiple trashed nodes by UUID. UUID can be folder or file.
// @Description Bulk payload accepts one or many items.
// @Tags File Manager
// @Accept json
// @Produce json
// @Param request body requestdto.FileManagerNodePurgeBulkRequest true "Bulk purge request"
// @Success 200 {object} responsedto.FileManagerPurgeNodeBulkResponse
// @Failure 422 {object} dto.ErrorResponse
// @Failure 400 {object} dto.ErrorResponse
// @Router /api/v1/file-manager/purge-delete [delete]
func (h *FileManagerFolderHandler) PurgeNodesBulk(c *fiber.Ctx) error {
	var req requestdto.FileManagerNodePurgeBulkRequest
	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.FileManagerPurgeNodeBulkItem, len(req.Data))
	for i := range req.Data {
		item := req.Data[i]
		nodeUUID := strings.TrimSpace(item.UUID)
		if nodeUUID == "" {
			items[i] = fileManagerPurgeBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "uuid is required")
			continue
		}

		nodeID, err := uuidv7.ParseString(nodeUUID)
		if err != nil {
			items[i] = fileManagerPurgeBulkSystemErrorItem(i, fiber.StatusUnprocessableEntity, "Validation error", "uuid is invalid UUID")
			continue
		}

		nodeType, err := h.purgeNodeAutoDetect(c.UserContext(), nodeID)
		if err != nil {
			def := mapFileManagerActionDef("Purge failed", err)
			items[i] = fileManagerPurgeBulkSystemErrorItem(i, def.Status, def.Title, def.Message)
			continue
		}
		items[i] = fileManagerPurgeBulkSuccessItem(i, nodeID, nodeType)
	}

	return writeFileManagerPurgeBulkResponse(c, items)
}

func (h *FileManagerFolderHandler) purgeNodeAutoDetect(ctx context.Context, id []byte) (string, error) {
	if err := h.svc.PurgeFolderNode(ctx, id); err == nil {
		return "folder", nil
	} else if !errors.Is(err, filemanager.ErrFolderNotFound) {
		return "", err
	}

	if err := h.svc.PurgeFileNode(ctx, id); err != nil {
		if errors.Is(err, filemanager.ErrFileNotFound) {
			return "", filemanager.ErrFileNotFound
		}
		return "", err
	}
	return "file", nil
}

func writeFileManagerPurgeBulkResponse(c *fiber.Ctx, items []responsedto.FileManagerPurgeNodeBulkItem) error {
	success := 0
	for i := range items {
		if items[i].Success {
			success++
		}
	}

	resp := responsedto.FileManagerPurgeNodeBulkResponse{
		Data: items,
	}
	resp.Meta.Total = len(items)
	resp.Meta.Success = success
	resp.Meta.Failed = len(items) - success
	resp.Meta.PartialSuccess = resp.Meta.Success > 0 && resp.Meta.Failed > 0

	return c.Status(fiber.StatusOK).JSON(resp)
}

func fileManagerPurgeBulkSuccessItem(index int, nodeID []byte, nodeType string) responsedto.FileManagerPurgeNodeBulkItem {
	resource := responsedto.FileManagerPurgeNodeResultResource{
		Type: "file_manager_purge_result",
		ID:   idString(nodeID),
		Attributes: responsedto.FileManagerPurgeNodeResultAttributes{
			NodeType: nodeType,
			Status:   "purged",
		},
	}
	return responsedto.FileManagerPurgeNodeBulkItem{
		Index:    index,
		Success:  true,
		Resource: &resource,
	}
}

func fileManagerPurgeBulkSystemErrorItem(index int, status int, title, detail string) responsedto.FileManagerPurgeNodeBulkItem {
	def := inferFileManagerDef(status, title, detail)
	return responsedto.FileManagerPurgeNodeBulkItem{
		Index:   index,
		Success: false,
		Error: &responsedto.FileManagerActionBulkError{
			Status:    status,
			Code:      def.Code,
			ErrorCode: def.ErrorCode,
			Title:     def.Title,
			Detail:    def.Message,
		},
	}
}

func (h *FileManagerFolderHandler) 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 parseFileManagerFolderPagination(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 replacePathPrefix(pathCache string, oldPrefix string, newPrefix string) string {
	if oldPrefix == "" {
		return pathCache
	}
	if pathCache == oldPrefix {
		return newPrefix
	}
	prefixWithSlash := oldPrefix + "/"
	if strings.HasPrefix(pathCache, prefixWithSlash) {
		return newPrefix + strings.TrimPrefix(pathCache, oldPrefix)
	}
	return pathCache
}

type fileManagerTrashItemView struct {
	NodeType    string
	UUID        string
	Name        string
	Extension   string
	PathCache   string
	MimeType    string
	DownloadURL string
	SizeBytes   int64
	DeletedAt   time.Time
	CreatedAt   time.Time
	UpdatedAt   time.Time
}

func sortFileManagerTrashItems(items []fileManagerTrashItemView, sortParam string) {
	sortParam = strings.TrimSpace(sortParam)
	if sortParam == "" {
		sortParam = "-deleted_at"
	}
	desc := strings.HasPrefix(sortParam, "-")
	key := strings.TrimPrefix(sortParam, "-")

	sort.SliceStable(items, func(i, j int) bool {
		a := items[i]
		b := items[j]

		compare := 0
		switch key {
		case "name":
			compare = strings.Compare(strings.ToLower(a.Name), strings.ToLower(b.Name))
		case "type":
			compare = strings.Compare(strings.ToLower(a.NodeType), strings.ToLower(b.NodeType))
		case "size_bytes":
			switch {
			case a.SizeBytes < b.SizeBytes:
				compare = -1
			case a.SizeBytes > b.SizeBytes:
				compare = 1
			}
		case "deleted_at":
			fallthrough
		default:
			switch {
			case a.DeletedAt.Before(b.DeletedAt):
				compare = -1
			case a.DeletedAt.After(b.DeletedAt):
				compare = 1
			}
		}

		if compare == 0 {
			compare = strings.Compare(strings.ToLower(a.Name), strings.ToLower(b.Name))
		}

		if desc {
			return compare > 0
		}
		return compare < 0
	})
}

func parentPathFromCache(pathCache string) string {
	pathCache = strings.TrimSpace(pathCache)
	pathCache = strings.TrimRight(pathCache, "/")
	if pathCache == "" || pathCache == "/" {
		return ""
	}
	lastSlash := strings.LastIndex(pathCache, "/")
	if lastSlash <= 0 {
		return ""
	}
	return pathCache[:lastSlash]
}

func fileManagerFolderResource(row *filemanager.Folder) responsedto.FileManagerFolderResource {
	return responsedto.FileManagerFolderResource{
		Type: "file_manager_folder",
		ID:   idString(row.ID),
		Attributes: shareddto.FileManagerFolderAttributes{
			ParentID:       idString(row.ParentID),
			Name:           row.Name,
			NameNormalized: row.NameNormalized,
			Depth:          row.Depth,
			PathCache:      row.PathCache,
			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),
			PurgeAt:        timeStringOrEmpty(row.PurgeAt),
		},
	}
}

func fileManagerFolderDetailResource(row *filemanager.Folder) responsedto.FileManagerFolderDetailResource {
	resource := responsedto.FileManagerFolderDetailResource{
		Type: "file_manager_folder",
		ID:   idString(row.ID),
		Attributes: shareddto.FileManagerFolderDetailAttributes{
			Name:           row.Name,
			NameNormalized: row.NameNormalized,
			PathCache:      row.PathCache,
			Audit: shareddto.FileManagerFolderAuditAttributes{
				CreatedAt: row.CreatedAt.UTC().Format(time.RFC3339),
				CreatedBy: uuidStringOrEmpty(row.CreatedBy),
				UpdatedAt: row.UpdatedAt.UTC().Format(time.RFC3339),
				UpdatedBy: uuidStringOrEmpty(row.UpdatedBy),
			},
		},
	}

	if row.Depth > 0 {
		depth := row.Depth
		resource.Attributes.Depth = &depth
	}

	parentID := strings.TrimSpace(idString(row.ParentID))
	if parentID != "" {
		resource.Attributes.Location = &shareddto.FileManagerFolderLocationAttributes{
			ParentID: parentID,
		}
	}

	return resource
}

func fileManagerNodeFromFolder(row *filemanager.Folder, isRoot bool) responsedto.FileManagerNodeResource {
	return responsedto.FileManagerNodeResource{
		Type: "file_manager_node",
		ID:   idString(row.ID),
		Attributes: responsedto.FileManagerNodeAttributes{
			NodeType:  "folder",
			ParentID:  fileManagerNodeParentID(row.ParentID, isRoot),
			Name:      row.Name,
			PathCache: row.PathCache,
			Status:    "ready",
			CreatedAt: row.CreatedAt.UTC().Format(time.RFC3339),
			UpdatedAt: row.UpdatedAt.UTC().Format(time.RFC3339),
		},
	}
}

func fileManagerNodeFromFile(row *filemanager.File, isRoot bool, downloadURL string) responsedto.FileManagerNodeResource {
	return responsedto.FileManagerNodeResource{
		Type: "file_manager_node",
		ID:   idString(row.ID),
		Attributes: responsedto.FileManagerNodeAttributes{
			NodeType:    "file",
			ParentID:    fileManagerNodeParentID(row.FolderID, isRoot),
			Name:        row.Name,
			Extension:   row.Extension,
			SizeBytes:   row.SizeBytes,
			MimeType:    row.MimeType,
			DownloadURL: strings.TrimSpace(downloadURL),
			Status:      row.Status,
			CreatedAt:   row.CreatedAt.UTC().Format(time.RFC3339),
			UpdatedAt:   row.UpdatedAt.UTC().Format(time.RFC3339),
		},
	}
}

func fileManagerNodeParentID(parentID []byte, isRoot bool) *string {
	if isRoot {
		return nil
	}
	id := idString(parentID)
	if strings.TrimSpace(id) == "" {
		return nil
	}
	return &id
}
