package callback

import (
	"context"
	"fmt"
	"strings"

	"github.com/cloudreve/Cloudreve/v3/pkg/filesystem"
	"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver/cos"
	"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver/local"
	"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver/onedrive"
	"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/driver/s3"
	"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
	"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
	"github.com/cloudreve/Cloudreve/v3/pkg/util"
	"github.com/gin-gonic/gin"
)

// CallbackProcessService 上传请求回调正文接口
type CallbackProcessService interface {
	GetBody(*serializer.UploadSession) serializer.UploadCallback
}

// RemoteUploadCallbackService 远程存储上传回调请求服务
type RemoteUploadCallbackService struct {
	Data serializer.UploadCallback `json:"data" binding:"required"`
}

// GetBody 返回回调正文
func (service RemoteUploadCallbackService) GetBody(session *serializer.UploadSession) serializer.UploadCallback {
	return service.Data
}

// UploadCallbackService OOS/七牛云存储上传回调请求服务
type UploadCallbackService struct {
	Name       string `json:"name"`
	SourceName string `json:"source_name"`
	PicInfo    string `json:"pic_info"`
	Size       uint64 `json:"size"`
}

// UpyunCallbackService 又拍云上传回调请求服务
type UpyunCallbackService struct {
	Code       int    `form:"code" binding:"required"`
	Message    string `form:"message" binding:"required"`
	SourceName string `form:"url" binding:"required"`
	Width      string `form:"image-width"`
	Height     string `form:"image-height"`
	Size       uint64 `form:"file_size"`
}

// OneDriveCallback OneDrive 客户端回调正文
type OneDriveCallback struct {
	ID   string `json:"id" binding:"required"`
	Meta *onedrive.FileInfo
}

// COSCallback COS 客户端回调正文
type COSCallback struct {
	Bucket string `form:"bucket"`
	Etag   string `form:"etag"`
}

// S3Callback S3 客户端回调正文
type S3Callback struct {
	Bucket string `form:"bucket"`
	Etag   string `form:"etag"`
	Key    string `form:"key"`
}

// GetBody 返回回调正文
func (service UpyunCallbackService) GetBody(session *serializer.UploadSession) serializer.UploadCallback {
	res := serializer.UploadCallback{
		Name:       session.Name,
		SourceName: service.SourceName,
		Size:       service.Size,
	}
	if service.Width != "" {
		res.PicInfo = service.Width + "," + service.Height
	}

	return res
}

// GetBody 返回回调正文
func (service UploadCallbackService) GetBody(session *serializer.UploadSession) serializer.UploadCallback {
	return serializer.UploadCallback{
		Name:       service.Name,
		SourceName: service.SourceName,
		PicInfo:    service.PicInfo,
		Size:       service.Size,
	}
}

// GetBody 返回回调正文
func (service OneDriveCallback) GetBody(session *serializer.UploadSession) serializer.UploadCallback {
	var picInfo = "0,0"
	if service.Meta.Image.Width != 0 {
		picInfo = fmt.Sprintf("%d,%d", service.Meta.Image.Width, service.Meta.Image.Height)
	}
	return serializer.UploadCallback{
		Name:       session.Name,
		SourceName: session.SavePath,
		PicInfo:    picInfo,
		Size:       session.Size,
	}
}

// GetBody 返回回调正文
func (service COSCallback) GetBody(session *serializer.UploadSession) serializer.UploadCallback {
	return serializer.UploadCallback{
		Name:       session.Name,
		SourceName: session.SavePath,
		PicInfo:    "",
		Size:       session.Size,
	}
}

// GetBody 返回回调正文
func (service S3Callback) GetBody(session *serializer.UploadSession) serializer.UploadCallback {
	return serializer.UploadCallback{
		Name:       session.Name,
		SourceName: session.SavePath,
		PicInfo:    "",
		Size:       session.Size,
	}
}

// ProcessCallback 处理上传结果回调
func ProcessCallback(service CallbackProcessService, c *gin.Context) serializer.Response {
	// 创建文件系统
	fs, err := filesystem.NewFileSystemFromCallback(c)
	if err != nil {
		return serializer.Err(serializer.CodePolicyNotAllowed, err.Error(), err)
	}
	defer fs.Recycle()

	// 获取回调会话
	callbackSessionRaw, _ := c.Get("callbackSession")
	callbackSession := callbackSessionRaw.(*serializer.UploadSession)
	callbackBody := service.GetBody(callbackSession)

	// 获取父目录
	exist, parentFolder := fs.IsPathExist(callbackSession.VirtualPath)
	if !exist {
		newFolder, err := fs.CreateDirectory(context.Background(), callbackSession.VirtualPath)
		if err != nil {
			return serializer.Err(serializer.CodeParamErr, "指定目录不存在", err)
		}
		parentFolder = newFolder
	}

	// 创建文件头
	fileHeader := local.FileStream{
		Size:        callbackBody.Size,
		VirtualPath: callbackSession.VirtualPath,
		Name:        callbackSession.Name,
	}

	// 生成上下文
	ctx := context.WithValue(context.Background(), fsctx.FileHeaderCtx, fileHeader)
	ctx = context.WithValue(ctx, fsctx.SavePathCtx, callbackBody.SourceName)

	// 添加钩子
	fs.Use("BeforeAddFile", filesystem.HookValidateFile)
	fs.Use("BeforeAddFile", filesystem.HookValidateCapacity)
	fs.Use("AfterValidateFailed", filesystem.HookGiveBackCapacity)
	fs.Use("AfterValidateFailed", filesystem.HookDeleteTempFile)
	fs.Use("BeforeAddFileFailed", filesystem.HookDeleteTempFile)

	// 向数据库中添加文件
	file, err := fs.AddFile(ctx, parentFolder)
	if err != nil {
		return serializer.Err(serializer.CodeUploadFailed, err.Error(), err)
	}

	// 如果是图片，则更新图片信息
	if callbackBody.PicInfo != "" {
		if err := file.UpdatePicInfo(callbackBody.PicInfo); err != nil {
			util.Log().Debug("无法更新回调文件的图片信息：%s", err)
		}
	}

	return serializer.Response{
		Code: 0,
	}
}

// PreProcess 对OneDrive客户端回调进行预处理验证
func (service *OneDriveCallback) PreProcess(c *gin.Context) serializer.Response {
	// 创建文件系统
	fs, err := filesystem.NewFileSystemFromCallback(c)
	if err != nil {
		return serializer.Err(serializer.CodePolicyNotAllowed, err.Error(), err)
	}
	defer fs.Recycle()

	// 获取回调会话
	callbackSessionRaw, _ := c.Get("callbackSession")
	callbackSession := callbackSessionRaw.(*serializer.UploadSession)

	// 获取文件信息
	info, err := fs.Handler.(onedrive.Driver).Client.Meta(context.Background(), service.ID, "")
	if err != nil {
		return serializer.Err(serializer.CodeUploadFailed, "文件元信息查询失败", err)
	}

	// 验证与回调会话中是否一致
	actualPath := strings.TrimPrefix(callbackSession.SavePath, "/")
	isSizeCheckFailed := callbackSession.Size != info.Size

	// SharePoint 会对 Office 文档增加 meta data 导致文件大小不一致，这里增加 10 KB 宽容
	// See: https://github.com/OneDrive/onedrive-api-docs/issues/935
	if strings.Contains(fs.Policy.OptionsSerialized.OdDriver, "sharepoint.com") && isSizeCheckFailed && (info.Size > callbackSession.Size) && (info.Size-callbackSession.Size <= 10240) {
		isSizeCheckFailed = false
	}

	if isSizeCheckFailed || info.GetSourcePath() != actualPath {
		fs.Handler.(onedrive.Driver).Client.Delete(context.Background(), []string{info.GetSourcePath()})
		return serializer.Err(serializer.CodeUploadFailed, "文件信息不一致", err)
	}
	service.Meta = info
	return ProcessCallback(service, c)
}

// PreProcess 对COS客户端回调进行预处理
func (service *COSCallback) PreProcess(c *gin.Context) serializer.Response {
	// 创建文件系统
	fs, err := filesystem.NewFileSystemFromCallback(c)
	if err != nil {
		return serializer.Err(serializer.CodePolicyNotAllowed, err.Error(), err)
	}
	defer fs.Recycle()

	// 获取回调会话
	callbackSessionRaw, _ := c.Get("callbackSession")
	callbackSession := callbackSessionRaw.(*serializer.UploadSession)

	// 获取文件信息
	info, err := fs.Handler.(cos.Driver).Meta(context.Background(), callbackSession.SavePath)
	if err != nil {
		return serializer.Err(serializer.CodeUploadFailed, "文件信息不一致", err)
	}

	// 验证实际文件信息与回调会话中是否一致
	if callbackSession.Size != info.Size || callbackSession.Key != info.CallbackKey {
		return serializer.Err(serializer.CodeUploadFailed, "文件信息不一致", err)
	}

	return ProcessCallback(service, c)
}

// PreProcess 对S3客户端回调进行预处理
func (service *S3Callback) PreProcess(c *gin.Context) serializer.Response {
	// 创建文件系统
	fs, err := filesystem.NewFileSystemFromCallback(c)
	if err != nil {
		return serializer.Err(serializer.CodePolicyNotAllowed, err.Error(), err)
	}
	defer fs.Recycle()

	// 获取回调会话
	callbackSessionRaw, _ := c.Get("callbackSession")
	callbackSession := callbackSessionRaw.(*serializer.UploadSession)

	// 获取文件信息
	info, err := fs.Handler.(s3.Driver).Meta(context.Background(), callbackSession.SavePath)
	if err != nil {
		return serializer.Err(serializer.CodeUploadFailed, "文件信息不一致", err)
	}

	// 验证实际文件信息与回调会话中是否一致
	if callbackSession.Size != info.Size || service.Etag != info.Etag {
		return serializer.Err(serializer.CodeUploadFailed, "文件信息不一致", err)
	}

	return ProcessCallback(service, c)
}
