package handlers

import (
	"bytes"
	"context"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"io"
	"log/slog"
	"net/http"
	"path"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/gofiber/fiber/v2"

	filemanager "wucher/internal/domain/file_manager"
	takeoverdomain "wucher/internal/domain/takeover"
	"wucher/internal/service"
	"wucher/internal/shared/pkg/apperrorsx"
	"wucher/internal/shared/pkg/userctx"
	"wucher/internal/shared/pkg/uuidv7"
)

const wopiSaveTimestampSkew = 30 * time.Second

func (h *TakeoverHandler) CheckWOPIFileInfo(c *fiber.Ctx) error {
	row, fileRow, claims, err := h.resolveWOPIFileByToken(c)
	if err != nil {
		return h.writeWOPIByTokenError(c, "CheckWOPIFileInfo", err)
	}
	meta, err := h.wopiHeadObject(c, fileRow)
	if err != nil {
		return h.writeWOPIByTokenError(c, "CheckWOPIFileInfo", err)
	}
	userID := tokenUserID(claims)
	ownerID := uuidStringOrEmpty(fileRow.CreatedBy)
	if row != nil {
		ownerID = firstNonEmpty(uuidStringOrEmpty(row.CreatedBy), ownerID)
	}
	info := takeoverWOPIFileInfo{
		BaseFileName:               strings.TrimSpace(fileRow.Name),
		Version:                    wopiFileVersion(fileRow),
		OwnerId:                    ownerID,
		UserId:                     uuidStringOrEmpty(userID),
		UserFriendlyName:           strings.TrimSpace(userctx.GetDisplayName(c.UserContext(), userID)),
		Size:                       firstPositiveInt64(meta.SizeBytes, fileRow.SizeBytes),
		ReadOnly:                   !claimsCanWrite(claims),
		UserCanWrite:               claimsCanWrite(claims),
		SupportsUpdate:             true,
		SupportsLocks:              true,
		SupportsGetLock:            true,
		SupportsRename:             false,
		SupportsExtendedLockLength: false,
		LastModifiedTime:           lastModifiedRFC3339(meta.LastModified, fileRow.UpdatedAt),
	}
	return c.Status(fiber.StatusOK).JSON(info)
}

func (h *TakeoverHandler) GetWOPIContents(c *fiber.Ctx) error {
	_, fileRow, _, err := h.resolveWOPIFileByToken(c)
	if err != nil {
		return h.writeWOPIByTokenError(c, "GetWOPIContents", err)
	}
	if h.storage == nil {
		return h.writeWOPIByTokenError(c, "GetWOPIContents", filemanager.ErrStorageUnavailable)
	}

	rawStorage, ok := any(h.storage).(takeoverWOPIStorage)
	if !ok {
		return h.writeWOPIByTokenError(c, "GetWOPIContents", filemanager.ErrStorageUnavailable)
	}

	objectKey := strings.TrimSpace(fileRow.ObjectKey)
	if objectKey == "" {
		return h.writeWOPIByTokenError(c, "GetWOPIContents", filemanager.ErrFileNotFound)
	}

	body, meta, err := rawStorage.GetObject(c.UserContext(), objectKey)
	if err != nil {
		if errors.Is(err, filemanager.ErrUploadObjectNotFound) {
			return h.writeWOPIByTokenError(c, "GetWOPIContents", filemanager.ErrFileNotFound)
		}
		return h.writeWOPIByTokenError(c, "GetWOPIContents", err)
	}
	defer body.Close()

	// Read the object fully while the request context is still alive. fasthttp's
	// SetBodyStream writes the reader to the socket AFTER the handler returns, by
	// which point requestTimeoutMiddleware's defer cancel() has torn the
	// context-bound storage reader down — shipping a committed 200 + full
	// Content-Length with a truncated body that Collabora saves verbatim as a
	// corrupt document. Buffering here keeps any read/size failure a non-200 that
	// is written before headers are committed. WOPI office docs are small and
	// bounded, so buffering the GET is safe.
	payload, err := io.ReadAll(body)
	if err != nil {
		slog.ErrorContext(c.UserContext(), "wopi get contents read failed",
			slog.String("file_id", idString(fileRow.ID)),
			slog.String("object_key", objectKey),
			slog.String("error", err.Error()),
		)
		return h.writeWOPIByTokenError(c, "GetWOPIContents", err)
	}
	if meta.SizeBytes > 0 && int64(len(payload)) != meta.SizeBytes {
		slog.ErrorContext(c.UserContext(), "wopi get contents body size mismatch",
			slog.String("file_id", idString(fileRow.ID)),
			slog.String("object_key", objectKey),
			slog.Int64("expected_bytes", meta.SizeBytes),
			slog.Int64("actual_bytes", int64(len(payload))),
		)
		return h.writeWOPIByTokenError(c, "GetWOPIContents", errors.New("wopi get contents body truncated: storage returned fewer bytes than advertised size"))
	}

	contentType := strings.TrimSpace(meta.ContentType)
	if contentType == "" {
		contentType = strings.TrimSpace(fileRow.MimeType)
	}
	if contentType == "" {
		contentType = "application/octet-stream"
	}
	c.Set(fiber.HeaderContentType, contentType)
	if etag := strings.TrimSpace(meta.ETag); etag != "" {
		c.Set(fiber.HeaderETag, etag)
	}
	if modified := meta.LastModified; modified != nil && !modified.IsZero() {
		c.Set(fiber.HeaderLastModified, modified.UTC().Format(http.TimeFormat))
	}
	c.Set("X-WOPI-ItemVersion", wopiFileVersion(fileRow))

	slog.InfoContext(c.UserContext(), "wopi get contents",
		slog.String("file_id", idString(fileRow.ID)),
		slog.String("object_key", objectKey),
		slog.Int64("size_bytes", int64(len(payload))),
		slog.String("content_type", contentType),
	)
	// Content-Length is derived from the validated buffer.
	return c.Status(fiber.StatusOK).Send(payload)
}

func (h *TakeoverHandler) PutWOPIContents(c *fiber.Ctx) error {
	row, fileRow, claims, err := h.resolveWOPIFileByToken(c)
	if err != nil {
		return h.writeWOPIByTokenError(c, "PutWOPIContents", err)
	}
	if !claimsCanWrite(claims) {
		return h.writeWOPIByTokenError(c, "PutWOPIContents", apperrorsx.New(apperrorsx.ErrForbiddenAccess))
	}
	if h.fileManager == nil || h.storage == nil {
		return h.writeWOPIByTokenError(c, "PutWOPIContents", filemanager.ErrStorageUnavailable)
	}

	rawStorage, ok := any(h.storage).(takeoverWOPIStorage)
	if !ok {
		return h.writeWOPIByTokenError(c, "PutWOPIContents", filemanager.ErrStorageUnavailable)
	}

	objectKey := strings.TrimSpace(fileRow.ObjectKey)
	if objectKey == "" {
		return h.writeWOPIByTokenError(c, "PutWOPIContents", filemanager.ErrFileNotFound)
	}

	currentLock := strings.TrimSpace(fileRow.WOPILockID)
	incomingLock := strings.TrimSpace(c.Get("X-WOPI-Lock"))
	if currentLock != "" && incomingLock != "" && !strings.EqualFold(currentLock, incomingLock) {
		c.Set("X-WOPI-Lock", currentLock)
		return c.Status(fiber.StatusConflict).JSON(map[string]any{"error": "lock mismatch"})
	}

	timestampHeader := firstNonEmpty(strings.TrimSpace(c.Get("X-COOL-WOPI-Timestamp")), strings.TrimSpace(c.Get("X-LOOL-WOPI-Timestamp")))
	if timestampHeader != "" {
		if modified, ok := parseWOPITimestamp(timestampHeader); ok {
			meta, headErr := rawStorage.HeadObject(c.UserContext(), objectKey)
			if headErr != nil {
				if errors.Is(headErr, filemanager.ErrUploadObjectNotFound) {
					return h.writeWOPIByTokenError(c, "PutWOPIContents", filemanager.ErrFileNotFound)
				}
				return h.writeWOPIByTokenError(c, "PutWOPIContents", headErr)
			}
			stored := firstNonZeroTime(meta.LastModified, &fileRow.UpdatedAt)
			if stored.After(modified.Add(wopiSaveTimestampSkew)) {
				return c.Status(fiber.StatusConflict).JSON(map[string]any{"COOLStatusCode": 1010})
			}
		}
	}

	contentType := strings.TrimSpace(c.Get(fiber.HeaderContentType))
	if contentType == "" {
		contentType = strings.TrimSpace(fileRow.MimeType)
	}
	if contentType == "" {
		contentType = "application/octet-stream"
	}

	payload := append([]byte(nil), c.Body()...)
	ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(fileRow.Name), "."))
	if ext == "" {
		ext = strings.ToLower(strings.TrimPrefix(filepath.Ext(objectKey), "."))
	}
	if ext == "" {
		ext = strings.ToLower(strings.TrimPrefix(filepath.Ext(fileRow.ObjectKey), "."))
	}
	if ext == "xlsx" {
		beforeLen := len(payload)
		beforeSum := sha256.Sum256(payload)
		if sanitizedPayload, styleFixes, changed, err := sanitizeXLSXPayload(payload); err != nil {
			return h.writeWOPIByTokenError(c, "PutWOPIContents", apperrorsx.New(apperrorsx.ErrFileManagerWOPIValidationFailed).WithCause(err))
		} else if changed {
			afterSum := sha256.Sum256(sanitizedPayload)
			slog.InfoContext(c.UserContext(), "wopi put contents sanitized xlsx",
				slog.String("object_key", objectKey),
				slog.Int("before_bytes", beforeLen),
				slog.Int("after_bytes", len(sanitizedPayload)),
				slog.Int("style_fixes", styleFixes),
				slog.Bool("changed", true),
				slog.String("before_sha256", hex.EncodeToString(beforeSum[:])),
				slog.String("after_sha256", hex.EncodeToString(afterSum[:])),
			)
			payload = sanitizedPayload
		}
	}
	if err := validateWOPISaveContent(fileRow.Name, contentType, payload); err != nil {
		return h.writeWOPIByTokenError(c, "PutWOPIContents", apperrorsx.New(apperrorsx.ErrFileManagerWOPIValidationFailed).WithCause(err))
	}
	if normalized := canonicalOfficeContentType(fileRow.Name); normalized != "" {
		contentType = normalized
	}
	tempObjectKey := wopiTemporaryObjectKey(objectKey)
	tempMeta, putErr := rawStorage.PutObject(c.UserContext(), filemanager.PutObjectInput{
		ObjectKey:   tempObjectKey,
		ContentType: contentType,
		SizeBytes:   int64(len(payload)),
		Body:        bytes.NewReader(payload),
	})
	if putErr != nil {
		return h.writeWOPIByTokenError(c, "PutWOPIContents", putErr)
	}
	if err := validateWOPISavedObject(rawStorage, c.UserContext(), fileRow, tempObjectKey, tempMeta); err != nil {
		_ = rawStorage.DeleteObject(c.UserContext(), tempObjectKey)
		return h.writeWOPIByTokenError(c, "PutWOPIContents", apperrorsx.New(apperrorsx.ErrFileManagerWOPIValidationFailed).WithCause(err))
	}
	if _, err := rawStorage.CopyObject(c.UserContext(), tempObjectKey, objectKey); err != nil {
		_ = rawStorage.DeleteObject(c.UserContext(), tempObjectKey)
		return h.writeWOPIByTokenError(c, "PutWOPIContents", err)
	}
	meta, err := rawStorage.HeadObject(c.UserContext(), objectKey)
	if err != nil {
		_ = rawStorage.DeleteObject(c.UserContext(), tempObjectKey)
		return h.writeWOPIByTokenError(c, "PutWOPIContents", err)
	}
	_ = rawStorage.DeleteObject(c.UserContext(), tempObjectKey)

	now := time.Now().UTC()
	fileRow.SizeBytes = int64(len(payload))
	fileRow.MimeType = contentType
	fileRow.ObjectVersion = nextWOPIVersion(fileRow.ObjectVersion)
	fileRow.ETag = strings.TrimSpace(meta.ETag)
	if strings.TrimSpace(meta.Bucket) != "" {
		fileRow.Bucket = meta.Bucket
	}
	if strings.TrimSpace(meta.ObjectKey) != "" {
		fileRow.ObjectKey = meta.ObjectKey
	}
	if isTrueish(c.Get("X-WOPI-IsModifiedByUser"), c.Get("X-COOL-WOPI-IsModifiedByUser"), c.Get("X-LOOL-WOPI-IsModifiedByUser")) {
		fileRow.LifecycleStatus = filemanager.FileLifecycleStatusActive
	}
	if isTrueish(c.Get("X-WOPI-IsExitSave"), c.Get("X-COOL-WOPI-IsExitSave"), c.Get("X-LOOL-WOPI-IsExitSave")) {
		fileRow.LifecycleStatus = filemanager.FileLifecycleStatusFinal
		fileRow.NameSlot = "live"
	}
	if currentLock != "" {
		lockExpires := now.Add(30 * time.Minute)
		fileRow.WOPILockID = currentLock
		fileRow.WOPILockedAt = &now
		fileRow.WOPILockExpiresAt = &lockExpires
	}
	fileRow.UpdatedAt = now
	fileRow.UpdatedBy = tokenUserID(claims)
	if err := h.fileManager.UpdateFile(c.UserContext(), fileRow); err != nil {
		return h.writeWOPIByTokenError(c, "PutWOPIContents", err)
	}

	c.Set("X-WOPI-ItemVersion", wopiFileVersion(fileRow))
	_ = row
	return c.Status(fiber.StatusOK).JSON(map[string]string{
		"LastModifiedTime": now.UTC().Format(time.RFC3339Nano),
	})
}

func wopiTemporaryObjectKey(objectKey string) string {
	objectKey = strings.TrimSpace(objectKey)
	suffix := idString(uuidv7.MustBytes())
	if objectKey == "" {
		return "wopi/tmp/.wopi-tmp-" + suffix
	}
	if idx := strings.LastIndex(objectKey, "/"); idx >= 0 {
		return objectKey[:idx+1] + ".wopi-tmp-" + suffix + path.Ext(objectKey)
	}
	return ".wopi-tmp-" + suffix + "-" + objectKey
}

func validateWOPISavedObject(storage takeoverWOPIStorage, ctx context.Context, fileRow *filemanager.File, objectKey string, meta filemanager.ObjectMetadata) error {
	if storage == nil || fileRow == nil {
		return nil
	}
	if ctx == nil {
		ctx = context.Background()
	}
	tempRow := &filemanager.File{
		Name:      fileRow.Name,
		ObjectKey: objectKey,
		MimeType:  firstNonEmpty(meta.ContentType, fileRow.MimeType),
	}
	return validateStoredWOPIFile(ctx, storage, tempRow)
}

func (h *TakeoverHandler) PostWOPIFile(c *fiber.Ctx) error {
	override := strings.ToUpper(strings.TrimSpace(c.Get("X-WOPI-Override")))
	switch override {
	case "LOCK":
		return h.wopiLock(c)
	case "UNLOCK":
		return h.wopiUnlock(c)
	case "REFRESH_LOCK":
		return h.wopiRefreshLock(c)
	case "GET_LOCK":
		return h.wopiGetLock(c)
	default:
		return h.writeWOPIByTokenError(c, "PostWOPIFile", apperrorsx.New(apperrorsx.ErrInvalidRequest))
	}
}

func (h *TakeoverHandler) wopiLock(c *fiber.Ctx) error {
	_, fileRow, claims, err := h.resolveWOPIFileByToken(c)
	if err != nil {
		return h.writeWOPIByTokenError(c, "LOCK", err)
	}
	if !claimsCanWrite(claims) {
		return h.writeWOPIByTokenError(c, "LOCK", apperrorsx.New(apperrorsx.ErrForbiddenAccess))
	}
	lockID := strings.TrimSpace(c.Get("X-WOPI-Lock"))
	if lockID == "" {
		return c.Status(fiber.StatusBadRequest).JSON(map[string]any{"error": "missing lock"})
	}
	if current := strings.TrimSpace(fileRow.WOPILockID); current != "" && !strings.EqualFold(current, lockID) {
		c.Set("X-WOPI-Lock", current)
		return c.Status(fiber.StatusConflict).JSON(map[string]any{"error": "lock mismatch"})
	}
	now := time.Now().UTC()
	exp := now.Add(30 * time.Minute)
	fileRow.WOPILockID = lockID
	fileRow.WOPILockJTI = claims.ID
	fileRow.WOPILockedAt = &now
	fileRow.WOPILockExpiresAt = &exp
	fileRow.UpdatedAt = now
	fileRow.UpdatedBy = tokenUserID(claims)
	if err := h.fileManager.UpdateFile(c.UserContext(), fileRow); err != nil {
		return h.writeWOPIByTokenError(c, "LOCK", err)
	}
	return c.SendStatus(fiber.StatusOK)
}

func (h *TakeoverHandler) wopiUnlock(c *fiber.Ctx) error {
	_, fileRow, claims, err := h.resolveWOPIFileByToken(c)
	if err != nil {
		return h.writeWOPIByTokenError(c, "UNLOCK", err)
	}
	if !claimsCanWrite(claims) {
		return h.writeWOPIByTokenError(c, "UNLOCK", apperrorsx.New(apperrorsx.ErrForbiddenAccess))
	}
	lockID := strings.TrimSpace(c.Get("X-WOPI-Lock"))
	if current := strings.TrimSpace(fileRow.WOPILockID); current != "" && !strings.EqualFold(current, lockID) {
		c.Set("X-WOPI-Lock", current)
		return c.Status(fiber.StatusConflict).JSON(map[string]any{"error": "lock mismatch"})
	}
	fileRow.WOPILockID = ""
	fileRow.WOPILockJTI = ""
	fileRow.WOPILockedAt = nil
	fileRow.WOPILockExpiresAt = nil
	fileRow.UpdatedAt = time.Now().UTC()
	fileRow.UpdatedBy = tokenUserID(claims)
	if err := h.fileManager.UpdateFile(c.UserContext(), fileRow); err != nil {
		return h.writeWOPIByTokenError(c, "UNLOCK", err)
	}
	return c.SendStatus(fiber.StatusOK)
}

func (h *TakeoverHandler) wopiRefreshLock(c *fiber.Ctx) error {
	_, fileRow, claims, err := h.resolveWOPIFileByToken(c)
	if err != nil {
		return h.writeWOPIByTokenError(c, "REFRESH_LOCK", err)
	}
	lockID := strings.TrimSpace(c.Get("X-WOPI-Lock"))
	if current := strings.TrimSpace(fileRow.WOPILockID); current != "" && !strings.EqualFold(current, lockID) {
		c.Set("X-WOPI-Lock", current)
		return c.Status(fiber.StatusConflict).JSON(map[string]any{"error": "lock mismatch"})
	}
	now := time.Now().UTC()
	exp := now.Add(30 * time.Minute)
	fileRow.WOPILockID = lockID
	fileRow.WOPILockJTI = claims.ID
	fileRow.WOPILockedAt = &now
	fileRow.WOPILockExpiresAt = &exp
	fileRow.UpdatedAt = now
	fileRow.UpdatedBy = tokenUserID(claims)
	if err := h.fileManager.UpdateFile(c.UserContext(), fileRow); err != nil {
		return h.writeWOPIByTokenError(c, "REFRESH_LOCK", err)
	}
	return c.SendStatus(fiber.StatusOK)
}

func (h *TakeoverHandler) wopiGetLock(c *fiber.Ctx) error {
	_, fileRow, _, err := h.resolveWOPIFileByToken(c)
	if err != nil {
		return h.writeWOPIByTokenError(c, "GET_LOCK", err)
	}
	if strings.TrimSpace(fileRow.WOPILockID) == "" {
		return c.SendStatus(fiber.StatusNotFound)
	}
	c.Set("X-WOPI-Lock", strings.TrimSpace(fileRow.WOPILockID))
	return c.SendStatus(fiber.StatusOK)
}

func (h *TakeoverHandler) resolveWOPIFileByToken(c *fiber.Ctx) (*takeoverdomain.TakeoverAc, *filemanager.File, *service.WOPIClaims, error) {
	claims := currentWOPIClaims(c)
	if claims == nil {
		return nil, nil, nil, apperrorsx.New(apperrorsx.ErrForbiddenAccess)
	}
	fileID, err := resolveWOPIFileIDFromRequest(c)
	if err != nil {
		return nil, nil, nil, filemanager.ErrFileNotFound
	}
	if !strings.EqualFold(claims.FileID, idString(fileID)) {
		return nil, nil, nil, apperrorsx.New(apperrorsx.ErrForbiddenAccess)
	}
	if strings.TrimSpace(claims.TakeoverID) == "" {
		if h.fileManager == nil {
			return nil, nil, nil, apperrorsx.New(apperrorsx.ErrInternal)
		}
		fileRow, err := h.fileManager.GetFileByIDAny(c.UserContext(), fileID)
		if err != nil {
			return nil, nil, nil, err
		}
		if fileRow == nil || !bytes.Equal(fileRow.CreatedBy, tokenUserID(claims)) {
			return nil, nil, nil, apperrorsx.New(apperrorsx.ErrForbiddenAccess)
		}
		return nil, fileRow, claims, nil
	}
	takeoverID, err := uuidv7.ParsePathLikeString(claims.TakeoverID)
	if err != nil {
		return nil, nil, nil, apperrorsx.New(apperrorsx.ErrForbiddenAccess)
	}
	if h.takeoverAc == nil {
		return nil, nil, nil, apperrorsx.New(apperrorsx.ErrInternal)
	}
	row, err := h.takeoverAc.GetByID(c.UserContext(), takeoverID)
	if err != nil {
		return nil, nil, nil, err
	}
	userID := tokenUserID(claims)
	if row == nil || (!takeoverHasUserAccess(row, userID) && !h.takeoverUserHasGlobalAccessForUserID(c.UserContext(), userID)) {
		return nil, nil, nil, apperrorsx.New(apperrorsx.ErrForbiddenAccess)
	}
	fileRow, err := takeoverFindFileByID(row, fileID)
	if err != nil {
		return nil, nil, nil, err
	}
	if fileRow == nil {
		return nil, nil, nil, filemanager.ErrFileNotFound
	}
	return row, fileRow, claims, nil
}

func resolveWOPIFileIDFromRequest(c *fiber.Ctx) ([]byte, error) {
	if c == nil {
		return nil, fiber.ErrBadRequest
	}
	candidates := []string{
		c.Params("fileId"),
		c.Path(),
		c.OriginalURL(),
		c.Query("WOPISrc"),
	}
	for _, candidate := range candidates {
		if fileID, err := uuidv7.ParsePathLikeString(candidate); err == nil {
			return fileID, nil
		}
	}
	return nil, fiber.ErrBadRequest
}

func (h *TakeoverHandler) wopiHeadObject(c *fiber.Ctx, fileRow *filemanager.File) (filemanager.ObjectMetadata, error) {
	if h.storage == nil {
		return filemanager.ObjectMetadata{}, filemanager.ErrStorageUnavailable
	}
	rawStorage, ok := any(h.storage).(takeoverWOPIStorage)
	if !ok {
		return filemanager.ObjectMetadata{}, filemanager.ErrStorageUnavailable
	}
	objectKey := strings.TrimSpace(fileRow.ObjectKey)
	if objectKey == "" {
		return filemanager.ObjectMetadata{}, filemanager.ErrFileNotFound
	}
	return rawStorage.HeadObject(c.UserContext(), objectKey)
}

func currentWOPIClaims(c *fiber.Ctx) *service.WOPIClaims {
	if c == nil {
		return nil
	}
	raw := c.Locals("wopi_claims")
	claims, _ := raw.(*service.WOPIClaims)
	return claims
}

func claimsCanWrite(claims *service.WOPIClaims) bool {
	if claims == nil {
		return false
	}
	return strings.EqualFold(strings.TrimSpace(claims.Permission), string(service.WOPIPermissionWrite))
}

func tokenUserID(claims *service.WOPIClaims) []byte {
	if claims == nil {
		return nil
	}
	userID, err := uuidv7.ParsePathLikeString(claims.UserID)
	if err != nil {
		return nil
	}
	return userID
}

func parseWOPITimestamp(raw string) (time.Time, bool) {
	v, err := strconv.ParseInt(strings.TrimSpace(raw), 10, 64)
	if err != nil {
		return time.Time{}, false
	}
	if v > 1e12 {
		return time.UnixMilli(v).UTC(), true
	}
	return time.Unix(v, 0).UTC(), true
}

func firstNonZeroTime(values ...*time.Time) time.Time {
	for i := range values {
		if values[i] != nil && !values[i].IsZero() {
			return values[i].UTC()
		}
	}
	return time.Time{}
}

func lastModifiedRFC3339(values ...any) string {
	for i := range values {
		switch v := values[i].(type) {
		case *time.Time:
			if v != nil && !v.IsZero() {
				return v.UTC().Format(time.RFC3339Nano)
			}
		case time.Time:
			if !v.IsZero() {
				return v.UTC().Format(time.RFC3339Nano)
			}
		}
	}
	return ""
}

func firstPositiveInt64(values ...int64) int64 {
	for i := range values {
		if values[i] > 0 {
			return values[i]
		}
	}
	return 0
}

func isTrueish(values ...string) bool {
	for i := range values {
		switch strings.ToLower(strings.TrimSpace(values[i])) {
		case "1", "true", "yes", "on":
			return true
		}
	}
	return false
}

func nextWOPIVersion(current string) string {
	current = strings.TrimSpace(current)
	if current == "" {
		return "1"
	}
	if n, err := strconv.ParseInt(current, 10, 64); err == nil {
		return strconv.FormatInt(n+1, 10)
	}
	return strconv.FormatInt(time.Now().UTC().UnixNano(), 10)
}

func (h *TakeoverHandler) writeWOPIByTokenError(c *fiber.Ctx, action string, err error) error {
	if err == nil {
		err = errors.New("wopi request failed")
	}
	logTakeoverError(c, action, err)
	def := apperrorsx.ErrInternal
	switch {
	case appErrorCodeMatches(err, apperrorsx.ErrForbiddenAccess.Code), appErrorCodeMatches(err, apperrorsx.ErrUserUnauthorized.Code):
		def = apperrorsx.ErrFileManagerWOPIPermissionDenied
	case errors.Is(err, filemanager.ErrFileNotFound), errors.Is(err, filemanager.ErrUploadObjectNotFound):
		def = apperrorsx.ErrFileManagerWOPIFileNotFound
	case errors.Is(err, filemanager.ErrStorageUnavailable):
		def = apperrorsx.ErrFileManagerWOPIServiceUnavailable
	case appErrorCodeMatches(err, apperrorsx.ErrFileManagerWOPIValidationFailed.Code):
		def = apperrorsx.ErrFileManagerWOPIValidationFailed
	}
	return apperrorsx.Write(c, apperrorsx.New(def).WithCause(err))
}

func appErrorCodeMatches(err error, code string) bool {
	if err == nil || strings.TrimSpace(code) == "" {
		return false
	}
	var appErr *apperrorsx.AppError
	if !errors.As(err, &appErr) || appErr == nil {
		return false
	}
	return strings.EqualFold(strings.TrimSpace(appErr.Code), strings.TrimSpace(code))
}
