package quqi

import (
	"bufio"
	"context"
	"encoding/base64"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	stdpath "path"
	"strings"
	"time"

	"github.com/alist-org/alist/v3/drivers/base"
	"github.com/alist-org/alist/v3/internal/errs"
	"github.com/alist-org/alist/v3/internal/model"
	"github.com/alist-org/alist/v3/internal/stream"
	"github.com/alist-org/alist/v3/pkg/http_range"
	"github.com/alist-org/alist/v3/pkg/utils"
	"github.com/go-resty/resty/v2"
	"github.com/minio/sio"
)

// do others that not defined in Driver interface
func (d *Quqi) request(host string, path string, method string, callback base.ReqCallback, resp interface{}) (*resty.Response, error) {
	var (
		reqUrl = url.URL{
			Scheme: "https",
			Host:   "quqi.com",
			Path:   path,
		}
		req    = base.RestyClient.R()
		result BaseRes
	)

	if host != "" {
		reqUrl.Host = host
	}
	req.SetHeaders(map[string]string{
		"Origin": "https://quqi.com",
		"Cookie": d.Cookie,
	})

	if d.GroupID != "" {
		req.SetQueryParam("quqiid", d.GroupID)
	}

	if callback != nil {
		callback(req)
	}

	res, err := req.Execute(method, reqUrl.String())
	if err != nil {
		return nil, err
	}
	// resty.Request.SetResult cannot parse result correctly sometimes
	err = utils.Json.Unmarshal(res.Body(), &result)
	if err != nil {
		return nil, err
	}
	if result.Code != 0 {
		return nil, errors.New(result.Message)
	}
	if resp != nil {
		err = utils.Json.Unmarshal(res.Body(), resp)
		if err != nil {
			return nil, err
		}
	}
	return res, nil
}

func (d *Quqi) login() error {
	if d.Addition.Cookie != "" {
		d.Cookie = d.Addition.Cookie
	}
	if d.checkLogin() {
		return nil
	}
	if d.Cookie != "" {
		return errors.New("cookie is invalid")
	}
	if d.Phone == "" {
		return errors.New("phone number is empty")
	}
	if d.Password == "" {
		return errs.EmptyPassword
	}

	resp, err := d.request("", "/auth/person/v2/login/password", resty.MethodPost, func(req *resty.Request) {
		req.SetFormData(map[string]string{
			"phone":    d.Phone,
			"password": base64.StdEncoding.EncodeToString([]byte(d.Password)),
		})
	}, nil)
	if err != nil {
		return err
	}

	var cookies []string
	for _, cookie := range resp.RawResponse.Cookies() {
		cookies = append(cookies, fmt.Sprintf("%s=%s", cookie.Name, cookie.Value))
	}
	d.Cookie = strings.Join(cookies, ";")

	return nil
}

func (d *Quqi) checkLogin() bool {
	if _, err := d.request("", "/auth/account/baseInfo", resty.MethodGet, nil, nil); err != nil {
		return false
	}
	return true
}

// rawExt 保留扩展名大小写
func rawExt(name string) string {
	ext := stdpath.Ext(name)
	if strings.HasPrefix(ext, ".") {
		ext = ext[1:]
	}

	return ext
}

// decryptKey 获取密码
func decryptKey(encodeKey string) []byte {
	// 移除非法字符
	u := strings.ReplaceAll(encodeKey, "[^A-Za-z0-9+\\/]", "")

	// 计算输出字节数组的长度
	o := len(u)
	a := 32

	// 创建输出字节数组
	c := make([]byte, a)

	// 编码循环
	s := uint32(0) // 累加器
	f := 0         // 输出数组索引
	for l := 0; l < o; l++ {
		r := l & 3 // 取模4,得到当前字符在四字节块中的位置
		i := u[l]  // 当前字符的ASCII码

		// 编码当前字符
		switch {
		case i >= 65 && i < 91: // 大写字母
			s |= uint32(i-65) << uint32(6*(3-r))
		case i >= 97 && i < 123: // 小写字母
			s |= uint32(i-71) << uint32(6*(3-r))
		case i >= 48 && i < 58: // 数字
			s |= uint32(i+4) << uint32(6*(3-r))
		case i == 43: // 加号
			s |= uint32(62) << uint32(6*(3-r))
		case i == 47: // 斜杠
			s |= uint32(63) << uint32(6*(3-r))
		}

		// 如果累加器已经包含了四个字符,或者是最后一个字符,则写入输出数组
		if r == 3 || l == o-1 {
			for e := 0; e < 3 && f < a; e, f = e+1, f+1 {
				c[f] = byte(s >> (16 >> e & 24) & 255)
			}
			s = 0
		}
	}

	return c
}

func (d *Quqi) linkFromPreview(id string) (*model.Link, error) {
	var getDocResp GetDocRes
	if _, err := d.request("", "/api/doc/getDoc", resty.MethodPost, func(req *resty.Request) {
		req.SetFormData(map[string]string{
			"quqi_id":   d.GroupID,
			"tree_id":   "1",
			"node_id":   id,
			"client_id": d.ClientID,
		})
	}, &getDocResp); err != nil {
		return nil, err
	}
	if getDocResp.Data.OriginPath == "" {
		return nil, errors.New("cannot get link from preview")
	}
	return &model.Link{
		URL: getDocResp.Data.OriginPath,
		Header: http.Header{
			"Origin": []string{"https://quqi.com"},
			"Cookie": []string{d.Cookie},
		},
	}, nil
}

func (d *Quqi) linkFromDownload(id string) (*model.Link, error) {
	var getDownloadResp GetDownloadResp
	if _, err := d.request("", "/api/doc/getDownload", resty.MethodGet, func(req *resty.Request) {
		req.SetQueryParams(map[string]string{
			"quqi_id":     d.GroupID,
			"tree_id":     "1",
			"node_id":     id,
			"url_type":    "undefined",
			"entry_type":  "undefined",
			"client_id":   d.ClientID,
			"no_redirect": "1",
		})
	}, &getDownloadResp); err != nil {
		return nil, err
	}
	if getDownloadResp.Data.Url == "" {
		return nil, errors.New("cannot get link from download")
	}

	return &model.Link{
		URL: getDownloadResp.Data.Url,
		Header: http.Header{
			"Origin": []string{"https://quqi.com"},
			"Cookie": []string{d.Cookie},
		},
	}, nil
}

func (d *Quqi) linkFromCDN(id string) (*model.Link, error) {
	downloadLink, err := d.linkFromDownload(id)
	if err != nil {
		return nil, err
	}

	var urlExchangeResp UrlExchangeResp
	if _, err = d.request("api.quqi.com", "/preview/downloadInfo/url/exchange", resty.MethodGet, func(req *resty.Request) {
		req.SetQueryParam("url", downloadLink.URL)
	}, &urlExchangeResp); err != nil {
		return nil, err
	}
	if urlExchangeResp.Data.Url == "" {
		return nil, errors.New("cannot get link from cdn")
	}

	// 假设存在未加密的情况
	if !urlExchangeResp.Data.IsEncrypted {
		return &model.Link{
			URL: urlExchangeResp.Data.Url,
			Header: http.Header{
				"Origin": []string{"https://quqi.com"},
				"Cookie": []string{d.Cookie},
			},
		}, nil
	}

	// 根据sio(https://github.com/minio/sio/blob/master/DARE.md)描述及实际测试,得出以下结论:
	// 1. 加密后大小(encrypted_size)-原始文件大小(size) = 加密包的头大小+身份验证标识 = (16+16) * N  ->  N为加密包的数量
	// 2. 原始文件大小(size)+64*1024-1 / (64*1024) = N  ->  每个包的有效负载为64K
	remoteClosers := utils.EmptyClosers()
	payloadSize := int64(1 << 16)
	expiration := time.Until(time.Unix(urlExchangeResp.Data.ExpiredTime, 0))
	resultRangeReader := func(ctx context.Context, httpRange http_range.Range) (io.ReadCloser, error) {
		encryptedOffset := httpRange.Start / payloadSize * (payloadSize + 32)
		decryptedOffset := httpRange.Start % payloadSize
		encryptedLength := (httpRange.Length+httpRange.Start+payloadSize-1)/payloadSize*(payloadSize+32) - encryptedOffset
		if httpRange.Length < 0 {
			encryptedLength = httpRange.Length
		} else {
			if httpRange.Length+httpRange.Start >= urlExchangeResp.Data.Size || encryptedLength+encryptedOffset >= urlExchangeResp.Data.EncryptedSize {
				encryptedLength = -1
			}
		}
		//log.Debugf("size: %d\tencrypted_size: %d", urlExchangeResp.Data.Size, urlExchangeResp.Data.EncryptedSize)
		//log.Debugf("http range offset: %d, length: %d", httpRange.Start, httpRange.Length)
		//log.Debugf("encrypted offset: %d, length: %d, decrypted offset: %d", encryptedOffset, encryptedLength, decryptedOffset)

		rrc, err := stream.GetRangeReadCloserFromLink(urlExchangeResp.Data.EncryptedSize, &model.Link{
			URL: urlExchangeResp.Data.Url,
			Header: http.Header{
				"Origin": []string{"https://quqi.com"},
				"Cookie": []string{d.Cookie},
			},
		})
		if err != nil {
			return nil, err
		}

		rc, err := rrc.RangeRead(ctx, http_range.Range{Start: encryptedOffset, Length: encryptedLength})
		remoteClosers.AddClosers(rrc.GetClosers())
		if err != nil {
			return nil, err
		}

		decryptReader, err := sio.DecryptReader(rc, sio.Config{
			MinVersion:     sio.Version10,
			MaxVersion:     sio.Version20,
			CipherSuites:   []byte{sio.CHACHA20_POLY1305, sio.AES_256_GCM},
			Key:            decryptKey(urlExchangeResp.Data.EncryptedKey),
			SequenceNumber: uint32(httpRange.Start / payloadSize),
		})
		if err != nil {
			return nil, err
		}
		bufferReader := bufio.NewReader(decryptReader)
		bufferReader.Discard(int(decryptedOffset))

		return utils.NewReadCloser(bufferReader, func() error {
			return nil
		}), nil
	}

	return &model.Link{
		Header: http.Header{
			"Origin": []string{"https://quqi.com"},
			"Cookie": []string{d.Cookie},
		},
		RangeReadCloser: &model.RangeReadCloser{RangeReader: resultRangeReader, Closers: remoteClosers},
		Expiration:      &expiration,
	}, nil
}