package _123_open

import (
	"context"
	"crypto/md5"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"

	"github.com/OpenListTeam/OpenList/v4/drivers/base"
	"github.com/OpenListTeam/OpenList/v4/internal/op"
	"github.com/go-resty/resty/v2"
	"github.com/google/uuid"
	log "github.com/sirupsen/logrus"
)

var ( // 不同情况下获取的AccessTokenQPS限制不同 如下模块化易于拓展
	Api = "https://open-api.123pan.com"

	AccessToken    = InitApiInfo(Api+"/api/v1/access_token", 1)
	RefreshToken   = InitApiInfo(Api+"/api/v1/oauth2/access_token", 1)
	UserInfo       = InitApiInfo(Api+"/api/v1/user/info", 1)
	FileList       = InitApiInfo(Api+"/api/v2/file/list", 3)
	DownloadInfo   = InitApiInfo(Api+"/api/v1/file/download_info", 5)
	DirectLink     = InitApiInfo(Api+"/api/v1/direct-link/url", 5)
	Mkdir          = InitApiInfo(Api+"/upload/v1/file/mkdir", 2)
	Move           = InitApiInfo(Api+"/api/v1/file/move", 1)
	Rename         = InitApiInfo(Api+"/api/v1/file/name", 1)
	Trash          = InitApiInfo(Api+"/api/v1/file/trash", 2)
	UploadCreate   = InitApiInfo(Api+"/upload/v2/file/create", 2)
	UploadComplete = InitApiInfo(Api+"/upload/v2/file/upload_complete", 0)

	OfflineDownload        = InitApiInfo(Api+"/api/v1/offline/download", 1)
	OfflineDownloadProcess = InitApiInfo(Api+"/api/v1/offline/download/process", 5)
)

func (d *Open123) Request(apiInfo *ApiInfo, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
	retryToken := true
	for {
		req := base.RestyClient.R()
		req.SetHeaders(map[string]string{
			"authorization": "Bearer " + d.AccessToken,
			"platform":      "open_platform",
			"Content-Type":  "application/json",
		})

		if callback != nil {
			callback(req)
		}
		if resp != nil {
			req.SetResult(resp)
		}

		log.Debugf("API: %s, QPS: %d, NowLen: %d", apiInfo.url, apiInfo.qps, apiInfo.NowLen())

		apiInfo.Require()
		defer apiInfo.Release()
		res, err := req.Execute(method, apiInfo.url)
		if err != nil {
			return nil, err
		}
		body := res.Body()

		// 解析为通用响应
		var baseResp BaseResp
		if err = json.Unmarshal(body, &baseResp); err != nil {
			return nil, err
		}

		if baseResp.Code == 0 {
			return body, nil
		} else if baseResp.Code == 401 && retryToken {
			retryToken = false
			if err := d.flushAccessToken(); err != nil {
				return nil, err
			}
		} else if baseResp.Code == 429 {
			time.Sleep(500 * time.Millisecond)
			log.Warningf("API: %s, QPS: %d, 请求太频繁，对应API提示过多请减小QPS", apiInfo.url, apiInfo.qps)
		} else {
			return nil, errors.New(baseResp.Message)
		}
	}
}

func (d *Open123) flushAccessToken() error {
	if d.ClientID != "" {
		if d.RefreshToken != "" {
			var resp RefreshTokenResp
			_, err := d.Request(RefreshToken, http.MethodPost, func(req *resty.Request) {
				req.SetQueryParam("client_id", d.ClientID)
				if d.ClientSecret != "" {
					req.SetQueryParam("client_secret", d.ClientSecret)
				}
				req.SetQueryParam("grant_type", "refresh_token")
				req.SetQueryParam("refresh_token", d.RefreshToken)
			}, &resp)
			if err != nil {
				return err
			}
			d.AccessToken = resp.AccessToken
			d.RefreshToken = resp.RefreshToken
			op.MustSaveDriverStorage(d)
		} else if d.ClientSecret != "" {
			var resp AccessTokenResp
			_, err := d.Request(AccessToken, http.MethodPost, func(req *resty.Request) {
				req.SetBody(base.Json{
					"clientID":     d.ClientID,
					"clientSecret": d.ClientSecret,
				})
			}, &resp)
			if err != nil {
				return err
			}
			d.AccessToken = resp.Data.AccessToken
			op.MustSaveDriverStorage(d)
		}
	}
	return nil
}

func (d *Open123) SignURL(originURL, privateKey string, uid uint64, validDuration time.Duration) (newURL string, err error) {
	// 生成Unix时间戳
	ts := time.Now().Add(validDuration).Unix()

	// 生成随机数（建议使用UUID，不能包含中划线（-））
	rand := strings.ReplaceAll(uuid.New().String(), "-", "")

	// 解析URL
	objURL, err := url.Parse(originURL)
	if err != nil {
		return "", err
	}

	// 待签名字符串，格式：path-timestamp-rand-uid-privateKey
	unsignedStr := fmt.Sprintf("%s-%d-%s-%d-%s", objURL.Path, ts, rand, uid, privateKey)
	md5Hash := md5.Sum([]byte(unsignedStr))
	// 生成鉴权参数，格式：timestamp-rand-uid-md5hash
	authKey := fmt.Sprintf("%d-%s-%d-%x", ts, rand, uid, md5Hash)

	// 添加鉴权参数到URL查询参数
	v := objURL.Query()
	v.Add("auth_key", authKey)
	objURL.RawQuery = v.Encode()

	return objURL.String(), nil
}

func (d *Open123) getUserInfo(ctx context.Context) (*UserInfoResp, error) {
	var resp UserInfoResp

	if _, err := d.Request(UserInfo, http.MethodGet, func(req *resty.Request) {
		req.SetContext(ctx)
	}, &resp); err != nil {
		return nil, err
	}

	return &resp, nil
}

func (d *Open123) getUID(ctx context.Context) (uint64, error) {
	if d.UID != 0 {
		return d.UID, nil
	}
	resp, err := d.getUserInfo(ctx)
	if err != nil {
		return 0, err
	}
	d.UID = resp.Data.UID
	return resp.Data.UID, nil
}

func (d *Open123) getFiles(parentFileId int64, limit int, lastFileId int64) (*FileListResp, error) {
	var resp FileListResp

	_, err := d.Request(FileList, http.MethodGet, func(req *resty.Request) {
		req.SetQueryParams(
			map[string]string{
				"parentFileId": strconv.FormatInt(parentFileId, 10),
				"limit":        strconv.Itoa(limit),
				"lastFileId":   strconv.FormatInt(lastFileId, 10),
				"trashed":      "false",
				"searchMode":   "",
				"searchData":   "",
			})
	}, &resp)
	if err != nil {
		return nil, err
	}

	return &resp, nil
}

func (d *Open123) getDownloadInfo(fileId int64) (*DownloadInfoResp, error) {
	var resp DownloadInfoResp

	_, err := d.Request(DownloadInfo, http.MethodGet, func(req *resty.Request) {
		req.SetQueryParams(map[string]string{
			"fileId": strconv.FormatInt(fileId, 10),
		})
	}, &resp)
	if err != nil {
		return nil, err
	}

	return &resp, nil
}

func (d *Open123) getDirectLink(fileId int64) (*DirectLinkResp, error) {
	var resp DirectLinkResp

	_, err := d.Request(DirectLink, http.MethodGet, func(req *resty.Request) {
		req.SetQueryParams(map[string]string{
			"fileID": strconv.FormatInt(fileId, 10),
		})
	}, &resp)
	if err != nil {
		return nil, err
	}

	return &resp, nil
}

func (d *Open123) mkdir(parentID int64, name string) error {
	_, err := d.Request(Mkdir, http.MethodPost, func(req *resty.Request) {
		req.SetBody(base.Json{
			"parentID": strconv.FormatInt(parentID, 10),
			"name":     name,
		})
	}, nil)
	if err != nil {
		return err
	}

	return nil
}

func (d *Open123) move(fileID, toParentFileID int64) error {
	_, err := d.Request(Move, http.MethodPost, func(req *resty.Request) {
		req.SetBody(base.Json{
			"fileIDs":        []int64{fileID},
			"toParentFileID": toParentFileID,
		})
	}, nil)
	if err != nil {
		return err
	}

	return nil
}

func (d *Open123) rename(fileId int64, fileName string) error {
	_, err := d.Request(Rename, http.MethodPut, func(req *resty.Request) {
		req.SetBody(base.Json{
			"fileId":   fileId,
			"fileName": fileName,
		})
	}, nil)
	if err != nil {
		return err
	}

	return nil
}

func (d *Open123) trash(fileId int64) error {
	_, err := d.Request(Trash, http.MethodPost, func(req *resty.Request) {
		req.SetBody(base.Json{
			"fileIDs": []int64{fileId},
		})
	}, nil)
	if err != nil {
		return err
	}

	return nil
}

func (d *Open123) createOfflineDownloadTask(ctx context.Context, url string, dirID, callback string) (taskID int, err error) {
	body := base.Json{
		"url":   url,
		"dirID": dirID,
	}
	if len(callback) > 0 {
		body["callBackUrl"] = callback
	}
	var resp OfflineDownloadResp
	_, err = d.Request(OfflineDownload, http.MethodPost, func(req *resty.Request) {
		req.SetBody(body)
	}, &resp)
	if err != nil {
		return 0, err
	}
	return resp.Data.TaskID, nil
}

func (d *Open123) queryOfflineDownloadStatus(ctx context.Context, taskID int) (process float64, status int, err error) {
	var resp OfflineDownloadProcessResp
	_, err = d.Request(OfflineDownloadProcess, http.MethodGet, func(req *resty.Request) {
		req.SetQueryParams(map[string]string{
			"taskID": strconv.Itoa(taskID),
		})
	}, &resp)
	if err != nil {
		return .0, 0, err
	}
	return resp.Data.Process, resp.Data.Status, nil
}
