package fs

import (
	"context"
	"encoding/gob"
	"errors"
	"fmt"
	"io"
	"time"

	"github.com/cloudreve/Cloudreve/v4/ent"
	"github.com/cloudreve/Cloudreve/v4/inventory"
	"github.com/cloudreve/Cloudreve/v4/inventory/types"
	"github.com/cloudreve/Cloudreve/v4/pkg/boolset"
	"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/lock"
	"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
	"github.com/cloudreve/Cloudreve/v4/pkg/queue"
	"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
	"github.com/gofrs/uuid"
)

type FsCapability int

const (
	FsCapabilityList = FsCapability(iota)
)

var (
	ErrDirectLinkInvalid    = serializer.NewError(serializer.CodeNotFound, "Direct link invalid", nil)
	ErrUnknownPolicyType    = serializer.NewError(serializer.CodeInternalSetting, "Unknown policy type", nil)
	ErrPathNotExist         = serializer.NewError(serializer.CodeParentNotExist, "Path not exist", nil)
	ErrFileDeleted          = serializer.NewError(serializer.CodeFileDeleted, "File deleted", nil)
	ErrEntityNotExist       = serializer.NewError(serializer.CodeEntityNotExist, "Entity not exist", nil)
	ErrFileExisted          = serializer.NewError(serializer.CodeObjectExist, "Object existed", nil)
	ErrNotSupportedAction   = serializer.NewError(serializer.CodeNoPermissionErr, "Not supported action", nil)
	ErrLockConflict         = serializer.NewError(serializer.CodeLockConflict, "Lock conflict", nil)
	ErrLockExpired          = serializer.NewError(serializer.CodeLockConflict, "Lock expired", nil)
	ErrModified             = serializer.NewError(serializer.CodeConflict, "Object conflict", nil)
	ErrIllegalObjectName    = serializer.NewError(serializer.CodeIllegalObjectName, "Invalid object name", nil)
	ErrFileSizeTooBig       = serializer.NewError(serializer.CodeFileTooLarge, "File is too large", nil)
	ErrInsufficientCapacity = serializer.NewError(serializer.CodeInsufficientCapacity, "Insufficient capacity", nil)
	ErrStaleVersion         = serializer.NewError(serializer.CodeStaleVersion, "File is updated during your edit", nil)
	ErrOwnerOnly            = serializer.NewError(serializer.CodeOwnerOnly, "Only owner or administrator can perform this action", nil)
	ErrArchiveSrcSizeTooBig = ErrFileSizeTooBig.WithError(fmt.Errorf("total size of to-be compressed file exceed group limit (%w)", queue.CriticalErr))
)

type (
	FileSystem interface {
		LockSystem
		UploadManager
		FileManager
		// Recycle recycles a DBFS and its generated resources.
		Recycle()
		// Capacity returns the storage capacity of the filesystem.
		Capacity(ctx context.Context, u *ent.User) (*Capacity, error)
		// CheckCapability checks if the filesystem supports given capability.
		CheckCapability(ctx context.Context, uri *URI, opts ...Option) error
		// StaleEntities returns all stale entities of given IDs. If no ID is given, all
		// potential stale entities will be returned.
		StaleEntities(ctx context.Context, entities ...int) ([]Entity, error)
		// AllFilesInTrashBin returns all files in trash bin, despite owner.
		AllFilesInTrashBin(ctx context.Context, opts ...Option) (*ListFileResult, error)
		// Walk walks through all files under given path with given depth limit.
		Walk(ctx context.Context, path *URI, depth int, walk WalkFunc, opts ...Option) error
		// SharedAddressTranslation translates a path that potentially contain shared symbolic to a real address.
		SharedAddressTranslation(ctx context.Context, path *URI, opts ...Option) (File, *URI, error)
		// ExecuteNavigatorHooks executes hooks of given type on a file for navigator based custom hooks.
		ExecuteNavigatorHooks(ctx context.Context, hookType HookType, file File) error
	}

	FileManager interface {
		// Get returns a file by its path.
		Get(ctx context.Context, path *URI, opts ...Option) (File, error)
		// Create creates a file.
		Create(ctx context.Context, path *URI, fileType types.FileType, opts ...Option) (File, error)
		// List lists files under give path.
		List(ctx context.Context, path *URI, opts ...Option) (File, *ListFileResult, error)
		// Rename renames a file.
		Rename(ctx context.Context, path *URI, newName string) (File, error)
		// Move moves files to dst.
		MoveOrCopy(ctx context.Context, path []*URI, dst *URI, isCopy bool) error
		// Delete performs hard-delete for given paths, return newly generated stale entities in this delete operation.
		Delete(ctx context.Context, path []*URI, opts ...Option) ([]Entity, error)
		// GetEntitiesFromFileID returns all entities of a given file.
		GetEntity(ctx context.Context, entityID int) (Entity, error)
		// UpsertMetadata update or insert metadata of a file.
		PatchMetadata(ctx context.Context, path []*URI, metas ...MetadataPatch) error
		// SoftDelete moves given files to trash bin.
		SoftDelete(ctx context.Context, path ...*URI) error
		// Restore restores given files from trash bin to its original location.
		Restore(ctx context.Context, path ...*URI) error
		// VersionControl performs version control on given file.
		//  - `delete` is false: set version as current version;
		//  - `delete` is true: delete version.
		VersionControl(ctx context.Context, path *URI, versionId int, delete bool) error
		// GetFileFromDirectLink gets a file from a direct link.
		GetFileFromDirectLink(ctx context.Context, dl *ent.DirectLink) (File, error)
		// TraverseFile traverses a file to its root file, return the file with linked root.
		TraverseFile(ctx context.Context, fileID int) (File, error)
		// PatchProps patches the props of a file.
		PatchProps(ctx context.Context, uri *URI, props *types.FileProps, delete bool) error
	}

	UploadManager interface {
		// PrepareUpload prepares an upload session. It performs validation on upload request and returns a placeholder
		// file if needed.
		PrepareUpload(ctx context.Context, req *UploadRequest, opts ...Option) (*UploadSession, error)
		// CompleteUpload completes an upload session.
		CompleteUpload(ctx context.Context, session *UploadSession) (File, error)
		// CancelUploadSession cancels an upload session. Delete the placeholder file if no other entity is created.
		CancelUploadSession(ctx context.Context, path *URI, sessionID string, session *UploadSession) ([]Entity, error)
		// PreValidateUpload pre-validates an upload request.
		PreValidateUpload(ctx context.Context, dst *URI, files ...PreValidateFile) error
	}

	LockSystem interface {
		// ConfirmLock confirms if a lock token is valid on given URI.
		ConfirmLock(ctx context.Context, ancestor File, uri *URI, token ...string) (func(), LockSession, error)
		// Lock locks a file. If zeroDepth is true, only the file itself will be locked. Ancestor is closest ancestor
		// of the file that will be locked, if the given uri is an existing file, ancestor will be itself.
		// `token` is optional and can be used if the requester need to explicitly specify a token.
		Lock(ctx context.Context, d time.Duration, requester *ent.User, zeroDepth bool, application lock.Application,
			uri *URI, token string) (LockSession, error)
		// Unlock unlocks files by given tokens.
		Unlock(ctx context.Context, tokens ...string) error
		// Refresh refreshes a lock.
		Refresh(ctx context.Context, d time.Duration, token string) (lock.LockDetails, error)
	}

	StatelessUploadManager interface {
		// PrepareUpload prepares the upload on the node.
		PrepareUpload(ctx context.Context, args *StatelessPrepareUploadService) (*StatelessPrepareUploadResponse, error)
		// CompleteUpload completes the upload on the node.
		CompleteUpload(ctx context.Context, args *StatelessCompleteUploadService) error
		// OnUploadFailed handles the failed upload on the node.
		OnUploadFailed(ctx context.Context, args *StatelessOnUploadFailedService) error
		// CreateFile creates a file on the node.
		CreateFile(ctx context.Context, args *StatelessCreateFileService) error
	}

	WalkFunc func(file File, level int) error

	File interface {
		IsNil() bool
		ID() int
		Name() string
		DisplayName() string
		Ext() string
		Type() types.FileType
		Size() int64
		UpdatedAt() time.Time
		CreatedAt() time.Time
		Metadata() map[string]string
		// Uri returns the URI of the file.
		Uri(isRoot bool) *URI
		Owner() *ent.User
		OwnerID() int
		// RootUri return the URI of the user root file under owner's view.
		RootUri() *URI
		Entities() []Entity
		PrimaryEntity() Entity
		PrimaryEntityID() int
		Shared() bool
		IsSymbolic() bool
		PolicyID() (id int)
		ExtendedInfo() *FileExtendedInfo
		FolderSummary() *FolderSummary
		Capabilities() *boolset.BooleanSet
		IsRootFolder() bool
		View() *types.ExplorerView
	}

	Entities []Entity
	Entity   interface {
		ID() int
		Type() types.EntityType
		Size() int64
		UpdatedAt() time.Time
		CreatedAt() time.Time
		Source() string
		ReferenceCount() int
		PolicyID() int
		UploadSessionID() *uuid.UUID
		CreatedBy() *ent.User
		Model() *ent.Entity
		Props() *types.EntityProps
		Encrypted() bool
	}

	FileExtendedInfo struct {
		StoragePolicy         *ent.StoragePolicy
		StorageUsed           int64
		Shares                []*ent.Share
		EntityStoragePolicies map[int]*ent.StoragePolicy
		View                  *types.ExplorerView
		DirectLinks           []*ent.DirectLink
	}

	FolderSummary struct {
		Size         int64     `json:"size"`
		Files        int       `json:"files"`
		Folders      int       `json:"folders"`
		Completed    bool      `json:"completed"` // whether the size calculation is completed
		CalculatedAt time.Time `json:"calculated_at"`
	}

	MetadataPatch struct {
		Key              string `json:"key" binding:"required"`
		Value            string `json:"value"`
		Private          bool   `json:"private" binding:"ne=true"`
		Remove           bool   `json:"remove"`
		UpdateModifiedAt bool   `json:"-"`
	}

	// ListFileResult result of listing files.
	ListFileResult struct {
		Files                 []File
		Parent                File
		Pagination            *inventory.PaginationResults
		Props                 *NavigatorProps
		ContextHint           *uuid.UUID
		RecursionLimitReached bool
		MixedType             bool
		SingleFileView        bool
		StoragePolicy         *ent.StoragePolicy
		View                  *types.ExplorerView
	}

	// NavigatorProps is the properties of current filesystem.
	NavigatorProps struct {
		// Supported capabilities of the navigator.
		Capability *boolset.BooleanSet `json:"capability"`
		// MaxPageSize is the maximum page size of the navigator.
		MaxPageSize int `json:"max_page_size"`
		// OrderByOptions is the supported order by options of the navigator.
		OrderByOptions []string `json:"order_by_options"`
		// OrderDirectionOptions is the supported order direction options of the navigator.
		OrderDirectionOptions []string `json:"order_direction_options"`
	}

	// UploadCredential for uploading files in client side.
	UploadCredential struct {
		SessionID       string   `json:"session_id"`
		ChunkSize       int64    `json:"chunk_size"` // 分块大小，0 为部分快
		Expires         int64    `json:"expires"`    // 上传凭证过期时间， Unix 时间戳
		UploadURLs      []string `json:"upload_urls,omitempty"`
		Credential      string   `json:"credential,omitempty"`
		UploadID        string   `json:"uploadID,omitempty"`
		Callback        string   `json:"callback,omitempty"`
		Uri             string   `json:"uri,omitempty"` // 存储路径
		AccessKey       string   `json:"ak,omitempty"`
		KeyTime         string   `json:"keyTime,omitempty"` // COS用有效期
		CompleteURL     string   `json:"completeURL,omitempty"`
		StoragePolicy   *ent.StoragePolicy
		CallbackSecret  string                 `json:"callback_secret,omitempty"`
		MimeType        string                 `json:"mime_type,omitempty"`     // Expected mimetype
		UploadPolicy    string                 `json:"upload_policy,omitempty"` // Upyun upload policy
		EncryptMetadata *types.EncryptMetadata `json:"encrypt_metadata,omitempty"`
	}

	// UploadSession stores the information of an upload session, used in server side.
	UploadSession struct {
		UID             int // 发起者
		Policy          *ent.StoragePolicy
		FileID          int    // ID of the placeholder file
		EntityID        int    // ID of the new entity
		Callback        string // 回调 URL 地址
		CallbackSecret  string // Callback secret
		UploadID        string // Multi-part upload ID
		UploadURL       string
		Credential      string
		ChunkSize       int64
		SentinelTaskID  int
		NewFileCreated  bool // If new file is created for this session
		Importing       bool // If the upload is importing from another file
		EncryptMetadata *types.EncryptMetadata

		LockToken string // Token of the locked placeholder file
		Props     *UploadProps
	}

	// UploadProps properties of an upload session/request.
	UploadProps struct {
		Uri                    *URI
		Size                   int64
		UploadSessionID        string
		PreferredStoragePolicy int
		SavePath               string
		LastModified           *time.Time
		MimeType               string
		Metadata               map[string]string
		PreviousVersion        string
		// EntityType is the type of the entity to be created. If not set, a new file will be created
		// with a default version entity. This will be set in update request for existing files.
		EntityType          *types.EntityType
		ExpireAt            time.Time
		EncryptionSupported []types.Cipher
		ClientSideEncrypted bool // Whether the file stream is already encrypted by client side.
	}

	// FsOption options for underlying file system.
	FsOption struct {
		Page               int // Page number when listing files.
		PageSize           int // Size of pages when listing files.
		OrderBy            string
		OrderDirection     string
		UploadRequest      *UploadRequest
		UnlinkOnly         bool
		UploadSession      *UploadSession
		DownloadSpeed      int64
		IsDownload         bool
		Expire             *time.Time
		Entity             Entity
		IsThumb            bool
		EntityType         *types.EntityType
		EntityTypeNil      bool
		SkipSoftDelete     bool
		SysSkipSoftDelete  bool
		Metadata           map[string]string
		ArchiveCompression bool
		ProgressFunc
		MaxArchiveSize  int64
		DryRun          CreateArchiveDryRunFunc
		Policy          *ent.StoragePolicy
		Node            StatelessUploadManager
		StatelessUserID int
		NoCache         bool
	}

	// Option 发送请求的额外设置
	Option interface {
		Apply(any)
	}

	OptionFunc func(*FsOption)

	// Ctx keys used to detect user canceled operation.
	UserCancelCtx struct{}
	GinCtx        struct{}

	// Capacity describes the capacity of a filesystem.
	Capacity struct {
		Total int64 `json:"total"`
		Used  int64 `json:"used"`
	}

	FileCapacity int

	LockSession interface {
		LastToken() string
	}

	HookType int

	CreateArchiveDryRunFunc func(name string, e Entity)

	StatelessPrepareUploadService struct {
		UploadRequest *UploadRequest `json:"upload_request" binding:"required"`
		UserID        int            `json:"user_id"`
	}
	StatelessCompleteUploadService struct {
		UploadSession *UploadSession `json:"upload_session" binding:"required"`
		UserID        int            `json:"user_id"`
	}
	StatelessOnUploadFailedService struct {
		UploadSession *UploadSession `json:"upload_session" binding:"required"`
		UserID        int            `json:"user_id"`
	}
	StatelessCreateFileService struct {
		Path   string         `json:"path" binding:"required"`
		Type   types.FileType `json:"type" binding:"required"`
		UserID int            `json:"user_id"`
	}
	StatelessPrepareUploadResponse struct {
		Session *UploadSession
		Req     *UploadRequest
	}

	PrepareRelocateRes struct {
		Entities  map[int]*RelocateEntity `json:"entities,omitempty"`
		LockToken string                  `json:"lock_token,omitempty"`
		Policy    *ent.StoragePolicy      `json:"policy,omitempty"`
	}

	RelocateEntity struct {
		SrcEntity                *ent.Entity `json:"src_entity"`
		FileUri                  *URI        `json:"file_uri,omitempty"`
		NewSavePath              string      `json:"new_save_path"`
		ParentFiles              []int       `json:"parent_files"`
		PrimaryEntityParentFiles []int       `json:"primary_entity_parent_files"`
	}

	PreValidateFile struct {
		Name     string
		Size     int64
		OmitName bool // if true, file name will not be validated
	}

	PhysicalObject struct {
		Name         string    `json:"name"`
		Source       string    `json:"source"`
		RelativePath string    `json:"relative_path"`
		Size         int64     `json:"size"`
		IsDir        bool      `json:"is_dir"`
		LastModify   time.Time `json:"last_modify"`
	}
)

const (
	FileCapacityPreview FileCapacity = iota
	FileCapacityEnter
	FileCapacityDownload
	FileCapacityRename
	FileCapacityCopy
	FileCapacityMove
)

const (
	HookTypeBeforeDownload = HookType(iota)
)

func (p *UploadProps) Copy() *UploadProps {
	newProps := *p
	return &newProps
}

func (f OptionFunc) Apply(o any) {
	f(o.(*FsOption))
}

// ==================== FS Options ====================

// WithUploadSession sets upload session for manager.
func WithUploadSession(s *UploadSession) Option {
	return OptionFunc(func(o *FsOption) {
		o.UploadSession = s
	})
}

// WithPageSize limit items in a page for listing files.
func WithPageSize(s int) Option {
	return OptionFunc(func(o *FsOption) {
		o.PageSize = s
	})
}

// WithPage set page number for listing files.
func WithPage(p int) Option {
	return OptionFunc(func(o *FsOption) {
		o.Page = p
	})
}

// WithOrderBy set order by for listing files.
func WithOrderBy(p string) Option {
	return OptionFunc(func(o *FsOption) {
		o.OrderBy = p
	})
}

// WithOrderDirection set order direction for listing files.
func WithOrderDirection(p string) Option {
	return OptionFunc(func(o *FsOption) {
		o.OrderDirection = p
	})
}

// WithUploadRequest set upload request for uploading files.
func WithUploadRequest(p *UploadRequest) Option {
	return OptionFunc(func(o *FsOption) {
		o.UploadRequest = p
	})
}

// WithProgressFunc set progress function for manager.
func WithProgressFunc(p ProgressFunc) Option {
	return OptionFunc(func(o *FsOption) {
		o.ProgressFunc = p
	})
}

// WithUnlinkOnly set unlink only for unlinking files.
func WithUnlinkOnly(p bool) Option {
	return OptionFunc(func(o *FsOption) {
		o.UnlinkOnly = p
	})
}

// WithDownloadSpeed sets download speed limit for manager.
func WithDownloadSpeed(speed int64) Option {
	return OptionFunc(func(o *FsOption) {
		o.DownloadSpeed = speed
	})
}

func WithIsDownload(b bool) Option {
	return OptionFunc(func(o *FsOption) {
		o.IsDownload = b
	})
}

// WithSysSkipSoftDelete sets whether to skip soft delete without checking
// file ownership.
func WithSysSkipSoftDelete(b bool) Option {
	return OptionFunc(func(o *FsOption) {
		o.SysSkipSoftDelete = b
	})
}

// WithNoCache sets whether to disable cache for entity's URL.
func WithNoCache(b bool) Option {
	return OptionFunc(func(o *FsOption) {
		o.NoCache = b
	})
}

// WithUrlExpire sets expire time for entity's URL.
func WithUrlExpire(t *time.Time) Option {
	return OptionFunc(func(o *FsOption) {
		o.Expire = t
	})
}

// WithEntity sets entity for manager.
func WithEntity(e Entity) Option {
	return OptionFunc(func(o *FsOption) {
		o.Entity = e
	})
}

// WithPolicy sets storage policy overwrite for manager.
func WithPolicy(p *ent.StoragePolicy) Option {
	return OptionFunc(func(o *FsOption) {
		o.Policy = p
	})
}

// WithUseThumb sets whether entity's URL is used for thumbnail.
func WithUseThumb(b bool) Option {
	return OptionFunc(func(o *FsOption) {
		o.IsThumb = b
	})
}

// WithEntityType sets entity type for manager.
func WithEntityType(t types.EntityType) Option {
	return OptionFunc(func(o *FsOption) {
		o.EntityType = &t
	})
}

// WithNoEntityType sets entity type to nil for manager.
func WithNoEntityType() Option {
	return OptionFunc(func(o *FsOption) {
		o.EntityTypeNil = true
	})
}

// WithSkipSoftDelete sets whether to skip soft delete.
func WithSkipSoftDelete(b bool) Option {
	return OptionFunc(func(o *FsOption) {
		o.SkipSoftDelete = b
	})
}

// WithMetadata sets metadata for file creation.
func WithMetadata(m map[string]string) Option {
	return OptionFunc(func(o *FsOption) {
		o.Metadata = m
	})
}

// WithArchiveCompression sets whether to compress files in archive.
func WithArchiveCompression(b bool) Option {
	return OptionFunc(func(o *FsOption) {
		o.ArchiveCompression = b
	})
}

// WithMaxArchiveSize sets maximum size of to be archived file or to-be decompressed
// size, 0 for unlimited.
func WithMaxArchiveSize(s int64) Option {
	return OptionFunc(func(o *FsOption) {
		o.MaxArchiveSize = s
	})
}

// WithDryRun sets whether to perform dry run.
func WithDryRun(b CreateArchiveDryRunFunc) Option {
	return OptionFunc(func(o *FsOption) {
		o.DryRun = b
	})
}

// WithNode sets node for stateless upload manager.
func WithNode(n StatelessUploadManager) Option {
	return OptionFunc(func(o *FsOption) {
		o.Node = n
	})
}

// WithStatelessUserID sets stateless user ID for manager.
func WithStatelessUserID(id int) Option {
	return OptionFunc(func(o *FsOption) {
		o.StatelessUserID = id
	})
}

type WriteMode int

const (
	ModeNone      WriteMode = 0x00000
	ModeOverwrite WriteMode = 0x00001
	// Deprecated
	ModeNop WriteMode = 0x00004
)

type (
	ProgressFunc  func(current, diff int64, total int64)
	UploadRequest struct {
		Props *UploadProps

		Mode         WriteMode
		File         io.ReadCloser `json:"-"`
		Seeker       io.Seeker     `json:"-"`
		Offset       int64
		ProgressFunc `json:"-"`

		ImportFrom *PhysicalObject `json:"-"`
		read       int64
	}
)

func (file *UploadRequest) Read(p []byte) (n int, err error) {
	if file.File != nil {
		n, err = file.File.Read(p)
		file.read += int64(n)
		if file.ProgressFunc != nil {
			file.ProgressFunc(file.read, int64(n), file.Props.Size)
		}

		return
	}

	return 0, io.EOF
}

func (file *UploadRequest) Close() error {
	if file.File != nil {
		return file.File.Close()
	}

	return nil
}

func (file *UploadRequest) Seek(offset int64, whence int) (int64, error) {
	if file.Seekable() {
		previous := file.read
		o, err := file.Seeker.Seek(offset, whence)
		file.read = o
		if file.ProgressFunc != nil {
			file.ProgressFunc(o, file.read-previous, file.Props.Size)
		}
		return o, err
	}

	return 0, errors.New("no seeker")
}

func (file *UploadRequest) Seekable() bool {
	return file.Seeker != nil
}

func init() {
	gob.Register(UploadSession{})
	gob.Register(FolderSummary{})
}

type ApplicationType string

const (
	ApplicationCreate         ApplicationType = "create"
	ApplicationRename         ApplicationType = "rename"
	ApplicationSetPermission  ApplicationType = "setPermission"
	ApplicationMoveCopy       ApplicationType = "moveCopy"
	ApplicationUpload         ApplicationType = "upload"
	ApplicationUpdateMetadata ApplicationType = "updateMetadata"
	ApplicationDelete         ApplicationType = "delete"
	ApplicationSoftDelete     ApplicationType = "softDelete"
	ApplicationDAV            ApplicationType = "dav"
	ApplicationVersionControl ApplicationType = "versionControl"
	ApplicationViewer         ApplicationType = "viewer"
	ApplicationMount          ApplicationType = "mount"
	ApplicationRelocate       ApplicationType = "relocate"
)

func LockApp(a ApplicationType) lock.Application {
	return lock.Application{Type: string(a)}
}

type LockSessionCtxKey struct{}

// LockSessionToContext stores lock session to context.
func LockSessionToContext(ctx context.Context, session LockSession) context.Context {
	return context.WithValue(ctx, LockSessionCtxKey{}, session)
}

// FindDesiredEntity finds the desired entity from the file.
// entityType is optional, if it is not nil, it will only return the entity with the given type.
func FindDesiredEntity(file File, version string, hasher hashid.Encoder, entityType *types.EntityType) (bool, Entity) {
	if version == "" {
		return true, file.PrimaryEntity()
	}

	requestedVersion, err := hasher.Decode(version, hashid.EntityID)
	if err != nil {
		return false, nil
	}

	hasVersions := false
	for _, entity := range file.Entities() {
		if entity.Type() == types.EntityTypeVersion {
			hasVersions = true
		}

		if entity.ID() == requestedVersion && (entityType == nil || *entityType == entity.Type()) {
			return true, entity
		}
	}

	// Happy path for: File has no versions, requested version is empty entity
	if !hasVersions && requestedVersion == 0 {
		return true, file.PrimaryEntity()
	}

	return false, nil
}

type DbEntity struct {
	model *ent.Entity
}

func NewEntity(model *ent.Entity) Entity {
	return &DbEntity{model: model}
}

func (e *DbEntity) ID() int {
	return e.model.ID
}

func (e *DbEntity) Type() types.EntityType {
	return types.EntityType(e.model.Type)
}

func (e *DbEntity) Size() int64 {
	return e.model.Size
}

func (e *DbEntity) UpdatedAt() time.Time {
	return e.model.UpdatedAt
}

func (e *DbEntity) CreatedAt() time.Time {
	return e.model.CreatedAt
}

func (e *DbEntity) CreatedBy() *ent.User {
	return e.model.Edges.User
}

func (e *DbEntity) Source() string {
	return e.model.Source
}

func (e *DbEntity) ReferenceCount() int {
	return e.model.ReferenceCount
}

func (e *DbEntity) PolicyID() int {
	return e.model.StoragePolicyEntities
}

func (e *DbEntity) UploadSessionID() *uuid.UUID {
	return e.model.UploadSessionID
}

func (e *DbEntity) Model() *ent.Entity {
	return e.model
}

func (e *DbEntity) Props() *types.EntityProps {
	return e.model.Props
}

func (e *DbEntity) Encrypted() bool {
	return e.model.Props != nil && e.model.Props.EncryptMetadata != nil
}

func NewEmptyEntity(u *ent.User) Entity {
	return &DbEntity{
		model: &ent.Entity{
			UpdatedAt:      time.Now(),
			ReferenceCount: 1,
			CreatedAt:      time.Now(),
			Edges: ent.EntityEdges{
				User: u,
			},
		},
	}
}
