package onedrive

import (
	"context"
	"errors"
	"fmt"
	"os"
	"path"
	"path/filepath"
	"strings"
	"time"

	"github.com/cloudreve/Cloudreve/v4/ent"
	"github.com/cloudreve/Cloudreve/v4/inventory/types"
	"github.com/cloudreve/Cloudreve/v4/pkg/boolset"
	"github.com/cloudreve/Cloudreve/v4/pkg/cluster/routes"
	"github.com/cloudreve/Cloudreve/v4/pkg/conf"
	"github.com/cloudreve/Cloudreve/v4/pkg/credmanager"
	"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver"
	"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/fs"
	"github.com/cloudreve/Cloudreve/v4/pkg/logging"
	"github.com/cloudreve/Cloudreve/v4/pkg/request"
	"github.com/cloudreve/Cloudreve/v4/pkg/serializer"
	"github.com/cloudreve/Cloudreve/v4/pkg/setting"
)

// Driver OneDrive 适配器
type (
	Driver struct {
		policy    *ent.StoragePolicy
		client    Client
		settings  setting.Provider
		config    conf.ConfigProvider
		l         logging.Logger
		chunkSize int64
	}
	ListPathRealRootCtx struct{}
)

var (
	features = &boolset.BooleanSet{}
)

const (
	streamSaverParam = "stream_saver"
)

func init() {
	boolset.Sets(map[driver.HandlerCapability]bool{
		driver.HandlerCapabilityUploadSentinelRequired: true,
	}, features)
}

// NewDriver 从存储策略初始化新的Driver实例
func New(ctx context.Context, policy *ent.StoragePolicy, settings setting.Provider,
	config conf.ConfigProvider, l logging.Logger, cred credmanager.CredManager) (*Driver, error) {
	chunkSize := policy.Settings.ChunkSize
	if policy.Settings.ChunkSize == 0 {
		chunkSize = 50 << 20 // 50MB
	}

	c := NewClient(policy, request.NewClient(config, request.WithLogger(l)), cred, l, settings, chunkSize)

	return &Driver{
		policy:    policy,
		client:    c,
		settings:  settings,
		l:         l,
		config:    config,
		chunkSize: chunkSize,
	}, nil
}

// List 列取项目
func (handler *Driver) List(ctx context.Context, base string, onProgress driver.ListProgressFunc, recursive bool) ([]fs.PhysicalObject, error) {
	base = strings.TrimPrefix(base, "/")
	// 列取子项目
	objects, _ := handler.client.ListChildren(ctx, base)

	// 获取真实的列取起始根目录
	rootPath := base
	if realBase, ok := ctx.Value(ListPathRealRootCtx{}).(string); ok {
		rootPath = realBase
	} else {
		ctx = context.WithValue(ctx, ListPathRealRootCtx{}, base)
	}

	// 整理结果
	res := make([]fs.PhysicalObject, 0, len(objects))
	for _, object := range objects {
		source := path.Join(base, object.Name)
		rel, err := filepath.Rel(rootPath, source)
		if err != nil {
			continue
		}
		res = append(res, fs.PhysicalObject{
			Name:         object.Name,
			RelativePath: filepath.ToSlash(rel),
			Source:       source,
			Size:         object.Size,
			IsDir:        object.Folder != nil,
			LastModify:   time.Now(),
		})
	}

	onProgress(len(objects))

	// 递归列取子目录
	if recursive {
		for _, object := range objects {
			if object.Folder != nil {
				sub, _ := handler.List(ctx, path.Join(base, object.Name), onProgress, recursive)
				res = append(res, sub...)
			}
		}
	}

	return res, nil
}

func (handler *Driver) Open(ctx context.Context, path string) (*os.File, error) {
	return nil, errors.New("not implemented")
}

// Put 将文件流保存到指定目录
func (handler *Driver) Put(ctx context.Context, file *fs.UploadRequest) error {
	defer file.Close()

	return handler.client.Upload(ctx, file)
}

// Delete 删除一个或多个文件，
// 返回未删除的文件，及遇到的最后一个错误
func (handler *Driver) Delete(ctx context.Context, files ...string) ([]string, error) {
	return handler.client.BatchDelete(ctx, files)
}

// Thumb 获取文件缩略图
func (handler *Driver) Thumb(ctx context.Context, expire *time.Time, ext string, e fs.Entity) (string, error) {
	res, err := handler.client.GetThumbURL(ctx, e.Source())
	if err != nil {
		var apiErr *RespError
		if errors.As(err, &apiErr); err == ErrThumbSizeNotFound || (apiErr != nil && apiErr.APIError.Code == notFoundError) {
			// OneDrive cannot generate thumbnail for this file
			return "", fmt.Errorf("thumb not supported in OneDrive: %w", err)
		}
	}

	return res, nil
}

// Source 获取外链URL
func (handler *Driver) Source(ctx context.Context, e fs.Entity, args *driver.GetSourceArgs) (string, error) {
	// 缓存不存在，重新获取
	res, err := handler.client.Meta(ctx, "", e.Source())
	if err != nil {
		return "", err
	}

	return res.DownloadURL, nil
}

// Token 获取上传会话URL
func (handler *Driver) Token(ctx context.Context, uploadSession *fs.UploadSession, file *fs.UploadRequest) (*fs.UploadCredential, error) {
	// 生成回调地址
	siteURL := handler.settings.SiteURL(setting.UseFirstSiteUrl(ctx))
	uploadSession.Callback = routes.MasterSlaveCallbackUrl(siteURL, types.PolicyTypeOd, uploadSession.Props.UploadSessionID, uploadSession.CallbackSecret).String()

	uploadURL, err := handler.client.CreateUploadSession(ctx, file.Props.SavePath, WithConflictBehavior("fail"))
	if err != nil {
		return nil, err
	}

	// 监控回调及上传
	//go handler.client.MonitorUpload(uploadURL, uploadSession.Key, fileInfo.SavePath, fileInfo.Size, ttl)

	uploadSession.ChunkSize = handler.chunkSize
	uploadSession.UploadURL = uploadURL
	return &fs.UploadCredential{
		ChunkSize:  handler.chunkSize,
		UploadURLs: []string{uploadURL},
	}, nil
}

// 取消上传凭证
func (handler *Driver) CancelToken(ctx context.Context, uploadSession *fs.UploadSession) error {
	err := handler.client.DeleteUploadSession(ctx, uploadSession.UploadURL)
	// Create empty placeholder file to stop upload
	if err == nil {
		_, err := handler.client.SimpleUpload(ctx, uploadSession.Props.SavePath, strings.NewReader(""), 0, WithConflictBehavior("replace"))
		if err != nil {
			handler.l.Warning("Failed to create placeholder file %q:%s", uploadSession.Props.SavePath, err)
		}
	}

	return err
}

func (handler *Driver) CompleteUpload(ctx context.Context, session *fs.UploadSession) error {
	if session.SentinelTaskID == 0 {
		return nil
	}

	// Make sure uploaded file size is correct
	res, err := handler.client.Meta(ctx, "", session.Props.SavePath)
	if err != nil {
		// Create empty placeholder file to stop further upload

		return fmt.Errorf("failed to get uploaded file size: %w", err)
	}

	isSharePoint := strings.Contains(handler.policy.Settings.OdDriver, "sharepoint.com") ||
		strings.Contains(handler.policy.Settings.OdDriver, "sharepoint.cn")
	sizeMismatch := res.Size != session.Props.Size
	// SharePoint 会对 Office 文档增加 meta data 导致文件大小不一致，这里增加 1 MB 宽容
	// See: https://github.com/OneDrive/onedrive-api-docs/issues/935
	if isSharePoint && sizeMismatch && (res.Size > session.Props.Size) && (res.Size-session.Props.Size <= 1048576) {
		sizeMismatch = false
	}

	if sizeMismatch {
		return serializer.NewError(
			serializer.CodeMetaMismatch,
			fmt.Sprintf("File size not match, expected: %d, actual: %d", session.Props.Size, res.Size),
			nil,
		)
	}

	return nil
}

func (handler *Driver) Capabilities() *driver.Capabilities {
	return &driver.Capabilities{
		StaticFeatures:         features,
		ThumbSupportedExts:     handler.policy.Settings.ThumbExts,
		ThumbSupportAllExts:    handler.policy.Settings.ThumbSupportAllExts,
		ThumbMaxSize:           handler.policy.Settings.ThumbMaxSize,
		ThumbProxy:             handler.policy.Settings.ThumbGeneratorProxy,
		MediaMetaProxy:         handler.policy.Settings.MediaMetaGeneratorProxy,
		BrowserRelayedDownload: handler.policy.Settings.StreamSaver,
	}
}

func (handler *Driver) MediaMeta(ctx context.Context, path, ext string) ([]driver.MediaMeta, error) {
	return nil, errors.New("not implemented")
}

func (handler *Driver) LocalPath(ctx context.Context, path string) string {
	return ""
}
