package qbittorrent

import (
	"bytes"
	"errors"
	"github.com/alist-org/alist/v3/pkg/utils"
	"io"
	"mime/multipart"
	"net/http"
	"net/http/cookiejar"
	"net/url"
)

type Client interface {
	AddFromLink(link string, savePath string, id string) error
	GetInfo(id string) (TorrentInfo, error)
	GetFiles(id string) ([]FileInfo, error)
	Delete(id string, deleteFiles bool) error
}

type client struct {
	url    *url.URL
	client http.Client
	Client
}

func New(webuiUrl string) (Client, error) {
	u, err := url.Parse(webuiUrl)
	if err != nil {
		return nil, err
	}

	jar, err := cookiejar.New(nil)
	if err != nil {
		return nil, err
	}
	var c = &client{
		url:    u,
		client: http.Client{Jar: jar},
	}

	err = c.checkAuthorization()
	if err != nil {
		return nil, err
	}
	return c, nil
}

func (c *client) checkAuthorization() error {
	// check authorization
	if c.authorized() {
		return nil
	}

	// check authorization after logging in
	err := c.login()
	if err != nil {
		return err
	}
	if c.authorized() {
		return nil
	}
	return errors.New("unauthorized qbittorrent url")
}

func (c *client) authorized() bool {
	resp, err := c.post("/api/v2/app/version", nil)
	if err != nil {
		return false
	}
	return resp.StatusCode == 200 // the status code will be 403 if not authorized
}

func (c *client) login() error {
	// prepare HTTP request
	v := url.Values{}
	v.Set("username", c.url.User.Username())
	passwd, _ := c.url.User.Password()
	v.Set("password", passwd)
	resp, err := c.post("/api/v2/auth/login", v)
	if err != nil {
		return err
	}

	// check result
	body := make([]byte, 2)
	_, err = resp.Body.Read(body)
	if err != nil {
		return err
	}
	if string(body) != "Ok" {
		return errors.New("failed to login into qBittorrent webui with url: " + c.url.String())
	}
	return nil
}

func (c *client) post(path string, data url.Values) (*http.Response, error) {
	u := c.url.JoinPath(path)
	u.User = nil // remove userinfo for requests

	req, err := http.NewRequest("POST", u.String(), bytes.NewReader([]byte(data.Encode())))
	if err != nil {
		return nil, err
	}
	if data != nil {
		req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
	}

	resp, err := c.client.Do(req)
	if err != nil {
		return nil, err
	}
	if resp.Cookies() != nil {
		c.client.Jar.SetCookies(u, resp.Cookies())
	}
	return resp, nil
}

func (c *client) AddFromLink(link string, savePath string, id string) error {
	err := c.checkAuthorization()
	if err != nil {
		return err
	}

	buf := new(bytes.Buffer)
	writer := multipart.NewWriter(buf)

	addField := func(name string, value string) {
		if err != nil {
			return
		}
		err = writer.WriteField(name, value)
	}
	addField("urls", link)
	addField("savepath", savePath)
	addField("tags", "alist-"+id)
	if err != nil {
		return err
	}

	err = writer.Close()
	if err != nil {
		return err
	}

	u := c.url.JoinPath("/api/v2/torrents/add")
	u.User = nil // remove userinfo for requests
	req, err := http.NewRequest("POST", u.String(), buf)
	if err != nil {
		return err
	}
	req.Header.Add("Content-Type", writer.FormDataContentType())

	resp, err := c.client.Do(req)
	if err != nil {
		return err
	}

	// check result
	body := make([]byte, 2)
	_, err = resp.Body.Read(body)
	if err != nil {
		return err
	}
	if resp.StatusCode != 200 || string(body) != "Ok" {
		return errors.New("failed to add qBittorrent task: " + link)
	}
	return nil
}

type TorrentStatus string

const (
	ERROR              TorrentStatus = "error"
	MISSINGFILES       TorrentStatus = "missingFiles"
	UPLOADING          TorrentStatus = "uploading"
	PAUSEDUP           TorrentStatus = "pausedUP"
	QUEUEDUP           TorrentStatus = "queuedUP"
	STALLEDUP          TorrentStatus = "stalledUP"
	CHECKINGUP         TorrentStatus = "checkingUP"
	FORCEDUP           TorrentStatus = "forcedUP"
	ALLOCATING         TorrentStatus = "allocating"
	DOWNLOADING        TorrentStatus = "downloading"
	METADL             TorrentStatus = "metaDL"
	PAUSEDDL           TorrentStatus = "pausedDL"
	QUEUEDDL           TorrentStatus = "queuedDL"
	STALLEDDL          TorrentStatus = "stalledDL"
	CHECKINGDL         TorrentStatus = "checkingDL"
	FORCEDDL           TorrentStatus = "forcedDL"
	CHECKINGRESUMEDATA TorrentStatus = "checkingResumeData"
	MOVING             TorrentStatus = "moving"
	UNKNOWN            TorrentStatus = "unknown"
)

// https://github.com/DGuang21/PTGo/blob/main/app/client/client_distributer.go
type TorrentInfo struct {
	AddedOn           int           `json:"added_on"`           // 将 torrent 添加到客户端的时间（Unix Epoch）
	AmountLeft        int64         `json:"amount_left"`        // 剩余大小（字节）
	AutoTmm           bool          `json:"auto_tmm"`           // 此 torrent 是否由 Automatic Torrent Management 管理
	Availability      float64       `json:"availability"`       // 当前百分比
	Category          string        `json:"category"`           //
	Completed         int64         `json:"completed"`          // 完成的传输数据量（字节）
	CompletionOn      int           `json:"completion_on"`      // Torrent 完成的时间（Unix Epoch）
	ContentPath       string        `json:"content_path"`       // torrent 内容的绝对路径（多文件 torrent 的根路径，单文件 torrent 的绝对文件路径）
	DlLimit           int           `json:"dl_limit"`           // Torrent 下载速度限制（字节/秒）
	Dlspeed           int           `json:"dlspeed"`            // Torrent 下载速度（字节/秒）
	Downloaded        int64         `json:"downloaded"`         // 已经下载大小
	DownloadedSession int64         `json:"downloaded_session"` // 此会话下载的数据量
	Eta               int           `json:"eta"`                //
	FLPiecePrio       bool          `json:"f_l_piece_prio"`     // 如果第一个最后一块被优先考虑，则为true
	ForceStart        bool          `json:"force_start"`        // 如果为此 torrent 启用了强制启动，则为true
	Hash              string        `json:"hash"`               //
	LastActivity      int           `json:"last_activity"`      // 上次活跃的时间（Unix Epoch）
	MagnetURI         string        `json:"magnet_uri"`         // 与此 torrent 对应的 Magnet URI
	MaxRatio          int           `json:"max_ratio"`          // 种子/上传停止种子前的最大共享比率
	MaxSeedingTime    int           `json:"max_seeding_time"`   // 停止种子种子前的最长种子时间（秒）
	Name              string        `json:"name"`               //
	NumComplete       int           `json:"num_complete"`       //
	NumIncomplete     int           `json:"num_incomplete"`     //
	NumLeechs         int           `json:"num_leechs"`         // 连接到的 leechers 的数量
	NumSeeds          int           `json:"num_seeds"`          // 连接到的种子数
	Priority          int           `json:"priority"`           // 速度优先。如果队列被禁用或 torrent 处于种子模式，则返回 -1
	Progress          float64       `json:"progress"`           // 进度
	Ratio             float64       `json:"ratio"`              // Torrent 共享比率
	RatioLimit        int           `json:"ratio_limit"`        //
	SavePath          string        `json:"save_path"`
	SeedingTime       int           `json:"seeding_time"`       // Torrent 完成用时（秒）
	SeedingTimeLimit  int           `json:"seeding_time_limit"` // max_seeding_time
	SeenComplete      int           `json:"seen_complete"`      // 上次 torrent 完成的时间
	SeqDl             bool          `json:"seq_dl"`             // 如果启用顺序下载，则为true
	Size              int64         `json:"size"`               //
	State             TorrentStatus `json:"state"`              // 参见https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-torrent-list
	SuperSeeding      bool          `json:"super_seeding"`      // 如果启用超级播种，则为true
	Tags              string        `json:"tags"`               // Torrent 的逗号连接标签列表
	TimeActive        int           `json:"time_active"`        // 总活动时间（秒）
	TotalSize         int64         `json:"total_size"`         // 此 torrent 中所有文件的总大小（字节）（包括未选择的文件）
	Tracker           string        `json:"tracker"`            // 第一个具有工作状态的tracker。如果没有tracker在工作，则返回空字符串。
	TrackersCount     int           `json:"trackers_count"`     //
	UpLimit           int           `json:"up_limit"`           // 上传限制
	Uploaded          int64         `json:"uploaded"`           // 累计上传
	UploadedSession   int64         `json:"uploaded_session"`   // 当前session累计上传
	Upspeed           int           `json:"upspeed"`            // 上传速度（字节/秒）
}

type InfoNotFoundError struct {
	Id  string
	Err error
}

func (i InfoNotFoundError) Error() string {
	return "there should be exactly one task with tag \"alist-" + i.Id + "\""
}

func NewInfoNotFoundError(id string) InfoNotFoundError {
	return InfoNotFoundError{Id: id}
}

func (c *client) GetInfo(id string) (TorrentInfo, error) {
	var infos []TorrentInfo

	err := c.checkAuthorization()
	if err != nil {
		return TorrentInfo{}, err
	}

	v := url.Values{}
	v.Set("tag", "alist-"+id)
	response, err := c.post("/api/v2/torrents/info", v)
	if err != nil {
		return TorrentInfo{}, err
	}

	body, err := io.ReadAll(response.Body)
	if err != nil {
		return TorrentInfo{}, err
	}
	err = utils.Json.Unmarshal(body, &infos)
	if err != nil {
		return TorrentInfo{}, err
	}
	if len(infos) != 1 {
		return TorrentInfo{}, NewInfoNotFoundError(id)
	}
	return infos[0], nil
}

type FileInfo struct {
	Index        int     `json:"index"`
	Name         string  `json:"name"`
	Size         int64   `json:"size"`
	Progress     float32 `json:"progress"`
	Priority     int     `json:"priority"`
	IsSeed       bool    `json:"is_seed"`
	PieceRange   []int   `json:"piece_range"`
	Availability float32 `json:"availability"`
}

func (c *client) GetFiles(id string) ([]FileInfo, error) {
	var infos []FileInfo

	err := c.checkAuthorization()
	if err != nil {
		return []FileInfo{}, err
	}

	tInfo, err := c.GetInfo(id)
	if err != nil {
		return []FileInfo{}, err
	}

	v := url.Values{}
	v.Set("hash", tInfo.Hash)
	response, err := c.post("/api/v2/torrents/files", v)
	if err != nil {
		return []FileInfo{}, err
	}

	body, err := io.ReadAll(response.Body)
	if err != nil {
		return []FileInfo{}, err
	}
	err = utils.Json.Unmarshal(body, &infos)
	if err != nil {
		return []FileInfo{}, err
	}
	return infos, nil
}

func (c *client) Delete(id string, deleteFiles bool) error {
	err := c.checkAuthorization()
	if err != nil {
		return err
	}

	info, err := c.GetInfo(id)
	if err != nil {
		return err
	}
	v := url.Values{}
	v.Set("hashes", info.Hash)
	if deleteFiles {
		v.Set("deleteFiles", "true")
	} else {
		v.Set("deleteFiles", "false")
	}
	response, err := c.post("/api/v2/torrents/delete", v)
	if err != nil {
		return err
	}
	if response.StatusCode != 200 {
		return errors.New("failed to delete qbittorrent task")
	}

	v = url.Values{}
	v.Set("tags", "alist-"+id)
	response, err = c.post("/api/v2/torrents/deleteTags", v)
	if err != nil {
		return err
	}
	if response.StatusCode != 200 {
		return errors.New("failed to delete qbittorrent tag")
	}
	return nil
}
