package explorer

import (
	"context"
	"encoding/gob"
	"fmt"
	"net/http"
	"time"

	"github.com/cloudreve/Cloudreve/v4/application/dependency"
	"github.com/cloudreve/Cloudreve/v4/inventory"
	"github.com/cloudreve/Cloudreve/v4/inventory/types"
	"github.com/cloudreve/Cloudreve/v4/pkg/auth"
	"github.com/cloudreve/Cloudreve/v4/pkg/cluster/routes"
	"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
	"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs/dbfs"
	"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager"
	"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager/entitysource"
	"github.com/cloudreve/Cloudreve/v4/pkg/hashid"
	"github.com/cloudreve/Cloudreve/v4/pkg/request"
	"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
	"github.com/cloudreve/Cloudreve/v4/pkg/setting"
	"github.com/cloudreve/Cloudreve/v4/pkg/util"
	"github.com/gin-gonic/gin"
	"github.com/gofrs/uuid"
	"github.com/samber/lo"
)

// SingleFileService 对单文件进行操作的五福，path为文件完整路径
type SingleFileService struct {
	Path string `uri:"path" json:"path" binding:"required,min=1,max=65535"`
}

// FileIDService 通过文件ID对文件进行操作的服务
type FileIDService struct {
}

// FileAnonymousGetService 匿名（外链）获取文件服务
type FileAnonymousGetService struct {
	ID   uint   `uri:"id" binding:"required,min=1"`
	Name string `uri:"name" binding:"required"`
}

func init() {
	gob.Register(ArchiveDownloadSession{})
}

// ArchiveService 文件流式打包下載服务
type (
	ArchiveService struct {
		ID string `uri:"sessionID" binding:"required"`
	}
	ArchiveParamCtx struct{}
)

// DownloadArchived 通过预签名 URL 打包下载
func (service *ArchiveService) DownloadArchived(c *gin.Context) error {
	dep := dependency.FromContext(c)
	archiveSessionRaw, found := dep.KV().Get(ArchiveDownloadSessionPrefix + service.ID)
	if !found {
		return serializer.NewError(serializer.CodeNotFound, "Archive session not exist", nil)
	}

	// Switch to user context
	archiveSession := archiveSessionRaw.(ArchiveDownloadSession)
	requester, err := dep.UserClient().GetLoginUserByID(c, archiveSession.RequesterID)
	if err != nil {
		return serializer.NewError(serializer.CodeNotFound, "Requester not found", err)
	}

	util.WithValue(c, inventory.UserCtx{}, requester)

	fm := manager.NewFileManager(dep, requester)
	defer fm.Recycle()

	// 开始打包
	c.Header("Content-Disposition", "attachment;")
	c.Header("Content-Type", "application/zip")

	if _, err := fm.CreateArchive(c, archiveSession.Uris, c.Writer); err != nil {
		return serializer.NewError(serializer.CodeIOFailed, "Failed to create archive", err)
	}

	return nil
}

type (
	GetDirectLinkParamCtx struct{}
	GetDirectLinkService  struct {
		Uris []string `json:"uris" binding:"required,min=1"`
	}
)

func (s *GetDirectLinkService) GetUris() []string {
	return s.Uris
}

// Sources 批量获取对象的外链
func (s *GetDirectLinkService) Get(c *gin.Context) ([]DirectLinkResponse, error) {
	dep := dependency.FromContext(c)
	u := inventory.UserFromContext(c)

	if u.Edges.Group.Settings.SourceBatchSize == 0 {
		return nil, serializer.NewError(serializer.CodeGroupNotAllowed, "", nil)
	}

	if len(s.Uris) > u.Edges.Group.Settings.SourceBatchSize {
		return nil, serializer.NewError(serializer.CodeBatchSourceSize, "", nil)
	}

	m := manager.NewFileManager(dep, u)
	defer m.Recycle()

	uris, err := fs.NewUriFromStrings(s.Uris...)
	if err != nil {
		return nil, serializer.NewError(serializer.CodeParamErr, "unknown uri", err)
	}

	res, err := m.GetDirectLink(c, uris...)
	return BuildDirectLinkResponse(res), err
}

func DeleteDirectLink(c *gin.Context) error {
	dep := dependency.FromContext(c)
	user := inventory.UserFromContext(c)
	m := manager.NewFileManager(dep, user)
	defer m.Recycle()

	linkId := hashid.FromContext(c)
	linkClient := dep.DirectLinkClient()
	ctx := context.WithValue(c, inventory.LoadDirectLinkFile{}, true)
	link, err := linkClient.GetByID(ctx, linkId)
	if err != nil || link.Edges.File == nil {
		return serializer.NewError(serializer.CodeNotFound, "Direct link not found", err)
	}

	if link.Edges.File.OwnerID != user.ID {
		return serializer.NewError(serializer.CodeNotFound, "Direct link not found", err)
	}

	if err := linkClient.Delete(c, link.ID); err != nil {
		return serializer.NewError(serializer.CodeDBError, "Failed to delete direct link", err)
	}

	return nil
}

type (
	// ListFileParameterCtx define key fore ListFileService
	ListFileParameterCtx struct{}

	// ListFileService stores parameters for list file by URI
	ListFileService struct {
		Uri            string `uri:"uri" form:"uri" json:"uri" binding:"required"`
		Page           int    `uri:"page" form:"page" json:"page" binding:"min=0"`
		PageSize       int    `uri:"page_size" form:"page_size" json:"page_size"`
		OrderBy        string `uri:"order_by" form:"order_by" json:"order_by"`
		OrderDirection string `uri:"order_direction" form:"order_direction" json:"order_direction"`
		NextPageToken  string `uri:"next_page_token" form:"next_page_token" json:"next_page_token"`
	}
)

// List all files for given path
func (service *ListFileService) List(c *gin.Context) (*ListResponse, error) {
	dep := dependency.FromContext(c)
	user := inventory.UserFromContext(c)
	m := manager.NewFileManager(dep, user)
	defer m.Recycle()

	uri, err := fs.NewUriFromString(service.Uri)
	if err != nil {
		return nil, serializer.NewError(serializer.CodeParamErr, "unknown uri", err)
	}

	pageSize := service.PageSize
	streamed := false
	hasher := dep.HashIDEncoder()
	parent, res, err := m.List(c, uri, &manager.ListArgs{
		Page:           service.Page,
		PageSize:       pageSize,
		Order:          service.OrderBy,
		OrderDirection: service.OrderDirection,
		PageToken:      service.NextPageToken,
		StreamResponseCallback: func(parent fs.File, files []fs.File) {
			if !streamed {
				WriteEventSourceHeader(c)
				streamed = true
			}

			WriteEventSource(c, "file", lo.Map(files, func(file fs.File, index int) *FileResponse {
				return BuildFileResponse(c, user, file, hasher, nil)
			}))
		},
	})
	if err != nil {
		return nil, err
	}

	listResponse := BuildListResponse(c, user, parent, res, hasher)
	if streamed {
		WriteEventSource(c, "list", listResponse)
		return nil, ErrSSETakeOver
	}

	return listResponse, nil
}

type (
	CreateFileParameterCtx struct{}
	CreateFileService      struct {
		Uri           string            `json:"uri" binding:"required"`
		Type          string            `json:"type" binding:"required,eq=file|eq=folder"`
		Metadata      map[string]string `json:"metadata"`
		ErrOnConflict bool              `json:"err_on_conflict"`
	}
)

func (service *CreateFileService) Create(c *gin.Context) (*FileResponse, error) {
	dep := dependency.FromContext(c)
	user := inventory.UserFromContext(c)
	m := manager.NewFileManager(dep, user)
	defer m.Recycle()

	uri, err := fs.NewUriFromString(service.Uri)
	if err != nil {
		return nil, serializer.NewError(serializer.CodeParamErr, "unknown uri", err)
	}

	fileType := types.FileTypeFromString(service.Type)
	opts := []fs.Option{
		fs.WithMetadata(service.Metadata),
	}
	if service.ErrOnConflict {
		opts = append(opts, dbfs.WithErrorOnConflict())
	}
	file, err := m.Create(c, uri, fileType, opts...)
	if err != nil {
		return nil, err
	}

	return BuildFileResponse(c, user, file, dep.HashIDEncoder(), nil), nil
}

type (
	RenameFileParameterCtx struct{}
	RenameFileService      struct {
		Uri     string `json:"uri" binding:"required"`
		NewName string `json:"new_name" binding:"required,min=1,max=255"`
	}
)

func (service *RenameFileService) Rename(c *gin.Context) (*FileResponse, error) {
	dep := dependency.FromContext(c)
	user := inventory.UserFromContext(c)
	m := manager.NewFileManager(dep, user)
	defer m.Recycle()

	uri, err := fs.NewUriFromString(service.Uri)
	if err != nil {
		return nil, serializer.NewError(serializer.CodeParamErr, "unknown uri", err)
	}

	file, err := m.Rename(c, uri, service.NewName)
	if err != nil {
		return nil, err
	}

	return BuildFileResponse(c, user, file, dep.HashIDEncoder(), nil), nil
}

type (
	MoveFileParameterCtx struct{}
	MoveFileService      struct {
		Uris []string `json:"uris" binding:"required,min=1"`
		Dst  string   `json:"dst" binding:"required"`
		Copy bool     `json:"copy"`
	}
)

func (s *MoveFileService) GetUris() []string {
	return s.Uris
}

func (s *MoveFileService) Move(c *gin.Context) error {
	dep := dependency.FromContext(c)
	user := inventory.UserFromContext(c)
	m := manager.NewFileManager(dep, user)
	defer m.Recycle()

	uris, err := fs.NewUriFromStrings(s.Uris...)
	if err != nil {
		return serializer.NewError(serializer.CodeParamErr, "unknown uri", err)
	}

	dst, err := fs.NewUriFromString(s.Dst)
	if err != nil {
		return serializer.NewError(serializer.CodeParamErr, "unknown destination uri", err)
	}

	return m.MoveOrCopy(c, uris, dst, s.Copy)
}

type (
	FileUpdateParameterCtx struct{}
	FileUpdateService      struct {
		Uri      string `form:"uri" binding:"required"`
		Previous string `form:"previous"`
	}
)

func (service *FileUpdateService) PutContent(c *gin.Context, ls fs.LockSession) (*FileResponse, error) {
	dep := dependency.FromContext(c)
	settings := dep.SettingProvider()
	// 取得文件大小
	rc, fileSize, err := request.SniffContentLength(c.Request)
	if err != nil {
		return nil, serializer.NewError(serializer.CodeParamErr, "invalid content length", err)
	}

	if fileSize > settings.MaxOnlineEditSize(c) {
		return nil, fs.ErrFileSizeTooBig
	}

	uri, err := fs.NewUriFromString(service.Uri)
	if err != nil {
		return nil, serializer.NewError(serializer.CodeParamErr, "unknown uri", err)
	}

	fileData := &fs.UploadRequest{
		Props: &fs.UploadProps{
			Uri:             uri,
			PreviousVersion: service.Previous,
			Size:            fileSize,
		},
		File: rc,
		Mode: fs.ModeOverwrite,
	}

	user := inventory.UserFromContext(c)
	m := manager.NewFileManager(dep, user)
	defer m.Recycle()

	// Update file
	var ctx context.Context = c
	if ls != nil {
		ctx = fs.LockSessionToContext(c, ls)
	}
	res, err := m.Update(ctx, fileData)
	if err != nil {
		return nil, fmt.Errorf("failed to update file: %w", err)
	}

	return BuildFileResponse(c, user, res, dep.HashIDEncoder(), nil), nil
}

type (
	FileURLParameterCtx struct{}
	FileURLService      struct {
		Uris              []string `json:"uris" binding:"required"`
		Download          bool     `json:"download"`
		Redirect          bool     `json:"redirect"` // Only works if Uris count is 1.
		Entity            string   `json:"entity"`   // Only works if Uris count is 1.
		UsePrimarySiteURL bool     `json:"use_primary_site_url"`
		SkipError         bool     `json:"skip_error"`
		Archive           bool     `json:"archive"`
		NoCache           bool     `json:"no_cache"`
	}
	FileURLResponse struct {
		Urls    []manager.EntityUrl `json:"urls"`
		Expires *time.Time          `json:"expires"`
	}
	ArchiveDownloadSession struct {
		Uris        []*fs.URI `json:"uris"`
		RequesterID int       `json:"requester_id"`
	}
)

const (
	ArchiveDownloadSessionPrefix = "archive_"
)

func (s *FileURLService) GetUris() []string {
	return s.Uris
}

// GetArchiveDownloadSession generates temporary download session for archive download.
func (s *FileURLService) GetArchiveDownloadSession(c *gin.Context) (*FileURLResponse, error) {
	dep := dependency.FromContext(c)
	settings := dep.SettingProvider()
	user := inventory.UserFromContext(c)

	uris, err := fs.NewUriFromStrings(s.Uris...)
	if err != nil {
		return nil, serializer.NewError(serializer.CodeParamErr, "unknown uri", err)
	}

	if !user.Edges.Group.Permissions.Enabled(int(types.GroupPermissionArchiveDownload)) {
		return nil, serializer.NewError(serializer.CodeGroupNotAllowed, "", nil)
	}

	// Create archive download session
	archiveSession := &ArchiveDownloadSession{
		Uris:        uris,
		RequesterID: user.ID,
	}
	sessionId := uuid.Must(uuid.NewV4()).String()
	ttl := settings.ArchiveDownloadSessionTTL(c)
	expire := time.Now().Add(time.Duration(ttl) * time.Second)
	if err := dep.KV().Set(ArchiveDownloadSessionPrefix+sessionId, *archiveSession, ttl); err != nil {
		return nil, serializer.NewError(serializer.CodeInternalSetting, "failed to create archive download session", err)
	}

	base := settings.SiteURL(c)
	downloadUrl := routes.MasterArchiveDownloadUrl(base, sessionId)
	finalUrl, err := auth.SignURI(c, dep.GeneralAuth(), downloadUrl.String(), &expire)
	if err != nil {
		return nil, serializer.NewError(serializer.CodeInternalSetting, "failed to sign archive download url", err)
	}

	return &FileURLResponse{
		Urls:    []manager.EntityUrl{{Url: finalUrl.String()}},
		Expires: &expire,
	}, nil
}

func (s *FileURLService) Get(c *gin.Context) (*FileURLResponse, error) {
	if s.Archive {
		return s.GetArchiveDownloadSession(c)
	}

	dep := dependency.FromContext(c)
	settings := dep.SettingProvider()
	user := inventory.UserFromContext(c)
	m := manager.NewFileManager(dep, user)
	defer m.Recycle()

	uris, err := fs.NewUriFromStrings(s.Uris...)
	if err != nil {
		return nil, serializer.NewError(serializer.CodeParamErr, "unknown uri", err)
	}

	// Request entity URL
	expire := time.Now().Add(settings.EntityUrlValidDuration(c))
	urlReq := lo.Map(uris, func(uri *fs.URI, _ int) manager.GetEntityUrlArgs {
		return manager.GetEntityUrlArgs{
			URI:               uri,
			PreferredEntityID: s.Entity,
		}
	})

	var ctx context.Context = c
	if s.UsePrimarySiteURL {
		ctx = setting.UseFirstSiteUrl(ctx)
	}

	res, earliestExpire, err := m.GetEntityUrls(ctx, urlReq,
		fs.WithDownloadSpeed(int64(user.Edges.Group.SpeedLimit)),
		fs.WithIsDownload(s.Download),
		fs.WithNoCache(s.NoCache),
		fs.WithUrlExpire(&expire),
	)
	if err != nil && !s.SkipError {
		return nil, fmt.Errorf("failed to get entity url: %w", err)
	}

	//if !s.NoCache && earliestExpire != nil {
	//	// Set cache header
	//	cacheTTL := int(earliestExpire.Sub(time.Now()).Seconds() - float64(settings.EntityUrlCacheMargin(c)))
	//	if cacheTTL > 0 {
	//		c.Header("Cache-Control", fmt.Sprintf("private, max-age=%d", cacheTTL))
	//	}
	//}

	if s.Redirect && len(uris) == 1 {
		c.Redirect(http.StatusFound, res[0].Url)
		return nil, nil
	}

	return &FileURLResponse{
		Urls:    res,
		Expires: earliestExpire,
	}, nil
}

type (
	FileThumbParameterCtx struct{}
	FileThumbService      struct {
		Uri string `form:"uri" binding:"required"`
	}
	FileThumbResponse struct {
		Url     string     `json:"url"`
		Expires *time.Time `json:"expires"`
	}
)

// Get redirect to thumb file.
func (s *FileThumbService) Get(c *gin.Context) (*FileThumbResponse, error) {
	dep := dependency.FromContext(c)
	user := inventory.UserFromContext(c)
	m := manager.NewFileManager(dep, user)
	defer m.Recycle()

	uri, err := fs.NewUriFromString(s.Uri)
	if err != nil {
		return nil, serializer.NewError(serializer.CodeParamErr, "unknown uri", err)
	}

	// Get thumbnail
	thumb, err := m.Thumbnail(c, uri)
	if err != nil {
		return nil, fmt.Errorf("failed to get thumbnail: %w", err)
	}

	expire := time.Now().Add(dep.SettingProvider().EntityUrlValidDuration(c))
	thumbUrl, err := thumb.Url(c, entitysource.WithExpire(&expire))
	if err != nil {
		return nil, fmt.Errorf("failed to get thumbnail url: %w", err)
	}

	return &FileThumbResponse{
		Url:     thumbUrl.Url,
		Expires: thumbUrl.ExpireAt,
	}, nil
}

type (
	DeleteFileParameterCtx struct{}
	DeleteFileService      struct {
		Uris           []string `json:"uris" binding:"required,min=1"`
		UnlinkOnly     bool     `json:"unlink"`
		SkipSoftDelete bool     `json:"skip_soft_delete"`
	}
)

func (s *DeleteFileService) GetUris() []string {
	return s.Uris
}

func (s *DeleteFileService) Delete(c *gin.Context) error {
	dep := dependency.FromContext(c)
	user := inventory.UserFromContext(c)
	m := manager.NewFileManager(dep, user)
	defer m.Recycle()

	uris, err := fs.NewUriFromStrings(s.Uris...)
	if err != nil {
		return serializer.NewError(serializer.CodeParamErr, "unknown uri", err)
	}

	if s.UnlinkOnly && !user.Edges.Group.Permissions.Enabled(int(types.GroupPermissionAdvanceDelete)) {
		return serializer.NewError(serializer.CodeNoPermissionErr, "advance delete permission is required", nil)
	}

	// Delete file
	if err = m.Delete(c, uris, fs.WithUnlinkOnly(s.UnlinkOnly), fs.WithSkipSoftDelete(s.SkipSoftDelete)); err != nil {
		return fmt.Errorf("failed to delete file: %w", err)
	}

	return nil
}

func (s *DeleteFileService) Restore(c *gin.Context) error {
	dep := dependency.FromContext(c)
	user := inventory.UserFromContext(c)
	m := manager.NewFileManager(dep, user)
	defer m.Recycle()

	uris, err := fs.NewUriFromStrings(s.Uris...)
	if err != nil {
		return serializer.NewError(serializer.CodeParamErr, "unknown uri", err)
	}

	// Delete file
	if err = m.Restore(c, uris...); err != nil {
		return fmt.Errorf("failed to restore file: %w", err)

	}

	return nil
}

type (
	UnlockFileParameterCtx struct{}
	UnlockFileService      struct {
		Tokens []string `json:"tokens" binding:"required,max=16384"`
	}
)

func (s *UnlockFileService) Unlock(c *gin.Context) error {
	dep := dependency.FromContext(c)
	user := inventory.UserFromContext(c)
	m := manager.NewFileManager(dep, user)
	defer m.Recycle()

	// Unlock file
	if err := m.Unlock(c, s.Tokens...); err != nil {
		return serializer.NewError(serializer.CodeParamErr, "failed to unlock file", err)
	}

	return nil
}

type (
	GetFileInfoParameterCtx struct{}
	GetFileInfoService      struct {
		Uri           string `form:"uri"`
		ID            string `form:"id"`
		ExtendedInfo  bool   `form:"extended"`
		FolderSummary bool   `form:"folder_summary"`
	}
)

func (s *GetFileInfoService) Get(c *gin.Context) (*FileResponse, error) {
	dep := dependency.FromContext(c)
	user := inventory.UserFromContext(c)
	m := manager.NewFileManager(dep, user)
	defer m.Recycle()

	if s.ID != "" && s.Uri == "" {
		fileId, err := dep.HashIDEncoder().Decode(s.ID, hashid.FileID)
		if err != nil {
			return nil, serializer.NewError(serializer.CodeParamErr, "unknown file id", err)
		}

		file, err := m.TraverseFile(c, fileId)
		if err != nil {
			return nil, fmt.Errorf("failed to traverse file: %w", err)
		}

		s.Uri = file.Uri(true).String()
	}

	if s.Uri == "" {
		return nil, serializer.NewError(serializer.CodeParamErr, "uri is required", nil)
	}

	uri, err := fs.NewUriFromString(s.Uri)
	if err != nil {
		return nil, serializer.NewError(serializer.CodeParamErr, "unknown uri", err)
	}

	opts := []fs.Option{dbfs.WithFilePublicMetadata(), dbfs.WithNotRoot()}
	if s.ExtendedInfo {
		opts = append(opts, dbfs.WithExtendedInfo(), dbfs.WithEntityUser(), dbfs.WithFileShareIfOwned())
	}
	if s.FolderSummary {
		opts = append(opts, dbfs.WithLoadFolderSummary())
	}

	file, err := m.Get(c, uri, opts...)
	if err != nil {
		return nil, fmt.Errorf("failed to get file: %w", err)
	}

	if file == nil {
		return nil, serializer.NewError(serializer.CodeNotFound, "file not found", nil)
	}

	return BuildFileResponse(c, user, file, dep.HashIDEncoder(), nil), nil
}

func RedirectDirectLink(c *gin.Context, name string, download bool) error {
	dep := dependency.FromContext(c)
	settings := dep.SettingProvider()

	sourceLinkID := hashid.FromContext(c)
	ctx := context.WithValue(c, inventory.LoadDirectLinkFile{}, true)
	ctx = context.WithValue(ctx, inventory.LoadFileEntity{}, true)
	ctx = context.WithValue(ctx, inventory.LoadFileUser{}, true)
	ctx = context.WithValue(ctx, inventory.LoadUserGroup{}, true)
	dl, err := dep.DirectLinkClient().GetByNameID(ctx, sourceLinkID, name)
	if err != nil {
		return serializer.NewError(serializer.CodeNotFound, "direct link not found", err)
	}

	m := manager.NewFileManager(dep, dl.Edges.File.Edges.Owner)
	defer m.Recycle()

	// Request entity URL
	expire := time.Now().Add(settings.EntityUrlValidDuration(c))
	res, earliestExpire, err := m.GetUrlForRedirectedDirectLink(c, dl,
		fs.WithUrlExpire(&expire),
		fs.WithIsDownload(download),
	)
	if err != nil {
		return err
	}

	c.Redirect(http.StatusFound, res)
	c.Header("Cache-Control", fmt.Sprintf("public, max-age=%d", int(earliestExpire.Sub(time.Now()).Seconds())))
	return nil
}

type (
	PatchViewParameterCtx struct{}
	PatchViewService      struct {
		Uri  string              `json:"uri" binding:"required"`
		View *types.ExplorerView `json:"view"`
	}
)

func (s *PatchViewService) Patch(c *gin.Context) error {
	dep := dependency.FromContext(c)
	user := inventory.UserFromContext(c)
	m := manager.NewFileManager(dep, user)
	defer m.Recycle()

	uri, err := fs.NewUriFromString(s.Uri)
	if err != nil {
		return serializer.NewError(serializer.CodeParamErr, "unknown uri", err)
	}

	if err := m.PatchView(c, uri, s.View); err != nil {
		return err
	}

	return nil
}
