package upyun

import (
	"context"
	"crypto/hmac"
	"crypto/md5"
	"crypto/sha1"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"path"
	"strconv"
	"strings"
	"sync"
	"time"

	model "github.com/cloudreve/Cloudreve/v3/models"
	"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/fsctx"
	"github.com/cloudreve/Cloudreve/v3/pkg/filesystem/response"
	"github.com/cloudreve/Cloudreve/v3/pkg/request"
	"github.com/cloudreve/Cloudreve/v3/pkg/serializer"
	"github.com/upyun/go-sdk/upyun"
)

// UploadPolicy 又拍云上传策略
type UploadPolicy struct {
	Bucket             string `json:"bucket"`
	SaveKey            string `json:"save-key"`
	Expiration         int64  `json:"expiration"`
	CallbackURL        string `json:"notify-url"`
	ContentLength      uint64 `json:"content-length"`
	ContentLengthRange string `json:"content-length-range,omitempty"`
	AllowFileType      string `json:"allow-file-type,omitempty"`
}

// Driver 又拍云策略适配器
type Driver struct {
	Policy *model.Policy
}

func (handler Driver) List(ctx context.Context, base string, recursive bool) ([]response.Object, error) {
	base = strings.TrimPrefix(base, "/")

	// 用于接受SDK返回对象的chan
	objChan := make(chan *upyun.FileInfo)
	objects := []*upyun.FileInfo{}

	// 列取配置
	listConf := &upyun.GetObjectsConfig{
		Path:         "/" + base,
		ObjectsChan:  objChan,
		MaxListTries: 1,
	}
	// 递归列取时不限制递归次数
	if recursive {
		listConf.MaxListLevel = -1
	}

	// 启动一个goroutine收集列取的对象信
	wg := &sync.WaitGroup{}
	wg.Add(1)
	go func(input chan *upyun.FileInfo, output *[]*upyun.FileInfo, wg *sync.WaitGroup) {
		defer wg.Done()
		for {
			file, ok := <-input
			if !ok {
				return
			}
			*output = append(*output, file)
		}
	}(objChan, &objects, wg)

	up := upyun.NewUpYun(&upyun.UpYunConfig{
		Bucket:   handler.Policy.BucketName,
		Operator: handler.Policy.AccessKey,
		Password: handler.Policy.SecretKey,
	})

	err := up.List(listConf)
	if err != nil {
		return nil, err
	}

	wg.Wait()

	// 汇总处理列取结果
	res := make([]response.Object, 0, len(objects))
	for _, object := range objects {
		res = append(res, response.Object{
			Name:         path.Base(object.Name),
			RelativePath: object.Name,
			Source:       path.Join(base, object.Name),
			Size:         uint64(object.Size),
			IsDir:        object.IsDir,
			LastModify:   object.Time,
		})
	}

	return res, nil
}

// Get 获取文件
func (handler Driver) Get(ctx context.Context, path string) (response.RSCloser, error) {
	// 获取文件源地址
	downloadURL, err := handler.Source(
		ctx,
		path,
		url.URL{},
		int64(model.GetIntSetting("preview_timeout", 60)),
		false,
		0,
	)
	if err != nil {
		return nil, err
	}

	// 获取文件数据流
	client := request.NewClient()
	resp, err := client.Request(
		"GET",
		downloadURL,
		nil,
		request.WithContext(ctx),
		request.WithHeader(
			http.Header{"Cache-Control": {"no-cache", "no-store", "must-revalidate"}},
		),
		request.WithTimeout(time.Duration(0)),
	).CheckHTTPResponse(200).GetRSCloser()
	if err != nil {
		return nil, err
	}

	resp.SetFirstFakeChunk()

	// 尝试自主获取文件大小
	if file, ok := ctx.Value(fsctx.FileModelCtx).(model.File); ok {
		resp.SetContentLength(int64(file.Size))
	}

	return resp, nil

}

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

	up := upyun.NewUpYun(&upyun.UpYunConfig{
		Bucket:   handler.Policy.BucketName,
		Operator: handler.Policy.AccessKey,
		Password: handler.Policy.SecretKey,
	})
	err := up.Put(&upyun.PutObjectConfig{
		Path:   file.Info().SavePath,
		Reader: file,
	})

	return err
}

// Delete 删除一个或多个文件，
// 返回未删除的文件，及遇到的最后一个错误
func (handler Driver) Delete(ctx context.Context, files []string) ([]string, error) {
	up := upyun.NewUpYun(&upyun.UpYunConfig{
		Bucket:   handler.Policy.BucketName,
		Operator: handler.Policy.AccessKey,
		Password: handler.Policy.SecretKey,
	})

	var (
		failed       = make([]string, 0, len(files))
		lastErr      error
		currentIndex = 0
		indexLock    sync.Mutex
		failedLock   sync.Mutex
		wg           sync.WaitGroup
		routineNum   = 4
	)
	wg.Add(routineNum)

	// upyun不支持批量操作，这里开四个协程并行操作
	for i := 0; i < routineNum; i++ {
		go func() {
			for {
				// 取得待删除文件
				indexLock.Lock()
				if currentIndex >= len(files) {
					// 所有文件处理完成
					wg.Done()
					indexLock.Unlock()
					return
				}
				path := files[currentIndex]
				currentIndex++
				indexLock.Unlock()

				// 发送异步删除请求
				err := up.Delete(&upyun.DeleteObjectConfig{
					Path:  path,
					Async: true,
				})

				// 处理错误
				if err != nil {
					failedLock.Lock()
					lastErr = err
					failed = append(failed, path)
					failedLock.Unlock()
				}
			}
		}()
	}

	wg.Wait()

	return failed, lastErr
}

// Thumb 获取文件缩略图
func (handler Driver) Thumb(ctx context.Context, path string) (*response.ContentResponse, error) {
	var (
		thumbSize = [2]uint{400, 300}
		ok        = false
	)
	if thumbSize, ok = ctx.Value(fsctx.ThumbSizeCtx).([2]uint); !ok {
		return nil, errors.New("failed to get thumbnail size")
	}

	thumbParam := fmt.Sprintf("!/fwfh/%dx%d", thumbSize[0], thumbSize[1])
	thumbURL, err := handler.Source(
		ctx,
		path+thumbParam,
		url.URL{},
		int64(model.GetIntSetting("preview_timeout", 60)),
		false,
		0,
	)
	if err != nil {
		return nil, err
	}

	return &response.ContentResponse{
		Redirect: true,
		URL:      thumbURL,
	}, nil
}

// Source 获取外链URL
func (handler Driver) Source(
	ctx context.Context,
	path string,
	baseURL url.URL,
	ttl int64,
	isDownload bool,
	speed int,
) (string, error) {
	// 尝试从上下文获取文件名
	fileName := ""
	if file, ok := ctx.Value(fsctx.FileModelCtx).(model.File); ok {
		fileName = file.Name
	}

	sourceURL, err := url.Parse(handler.Policy.BaseURL)
	if err != nil {
		return "", err
	}

	fileKey, err := url.Parse(url.PathEscape(path))
	if err != nil {
		return "", err
	}

	sourceURL = sourceURL.ResolveReference(fileKey)

	// 如果是下载文件URL
	if isDownload {
		query := sourceURL.Query()
		query.Add("_upd", fileName)
		sourceURL.RawQuery = query.Encode()
	}

	return handler.signURL(ctx, sourceURL, ttl)
}

func (handler Driver) signURL(ctx context.Context, path *url.URL, TTL int64) (string, error) {
	if !handler.Policy.IsPrivate {
		// 未开启Token防盗链时，直接返回
		return path.String(), nil
	}

	etime := time.Now().Add(time.Duration(TTL) * time.Second).Unix()
	signStr := fmt.Sprintf(
		"%s&%d&%s",
		handler.Policy.OptionsSerialized.Token,
		etime,
		path.Path,
	)
	signMd5 := fmt.Sprintf("%x", md5.Sum([]byte(signStr)))
	finalSign := signMd5[12:20] + strconv.FormatInt(etime, 10)

	// 将签名添加到URL中
	query := path.Query()
	query.Add("_upt", finalSign)
	path.RawQuery = query.Encode()

	return path.String(), nil
}

// Token 获取上传策略和认证Token
func (handler Driver) Token(ctx context.Context, ttl int64, uploadSession *serializer.UploadSession, file fsctx.FileHeader) (*serializer.UploadCredential, error) {
	// 生成回调地址
	siteURL := model.GetSiteURL()
	apiBaseURI, _ := url.Parse("/api/v3/callback/upyun/" + uploadSession.Key)
	apiURL := siteURL.ResolveReference(apiBaseURI)

	// 上传策略
	fileInfo := file.Info()
	putPolicy := UploadPolicy{
		Bucket: handler.Policy.BucketName,
		// TODO escape
		SaveKey:            fileInfo.SavePath,
		Expiration:         time.Now().Add(time.Duration(ttl) * time.Second).Unix(),
		CallbackURL:        apiURL.String(),
		ContentLength:      fileInfo.Size,
		ContentLengthRange: fmt.Sprintf("0,%d", fileInfo.Size),
		AllowFileType:      strings.Join(handler.Policy.OptionsSerialized.FileType, ","),
	}

	// 生成上传凭证
	policyJSON, err := json.Marshal(putPolicy)
	if err != nil {
		return nil, err
	}
	policyEncoded := base64.StdEncoding.EncodeToString(policyJSON)

	// 生成签名
	elements := []string{"POST", "/" + handler.Policy.BucketName, policyEncoded}
	signStr := handler.Sign(ctx, elements)

	return &serializer.UploadCredential{
		SessionID:  uploadSession.Key,
		Policy:     policyEncoded,
		Credential: signStr,
		UploadURLs: []string{"https://v0.api.upyun.com/" + handler.Policy.BucketName},
	}, nil
}

// 取消上传凭证
func (handler Driver) CancelToken(ctx context.Context, uploadSession *serializer.UploadSession) error {
	return nil
}

// Sign 计算又拍云的签名头
func (handler Driver) Sign(ctx context.Context, elements []string) string {
	password := fmt.Sprintf("%x", md5.Sum([]byte(handler.Policy.SecretKey)))
	mac := hmac.New(sha1.New, []byte(password))
	value := strings.Join(elements, "&")
	mac.Write([]byte(value))
	signStr := base64.StdEncoding.EncodeToString((mac.Sum(nil)))
	return fmt.Sprintf("UPYUN %s:%s", handler.Policy.AccessKey, signStr)
}
